diff --git a/apps/web/src/app/articles/hooks/useArticlesAdminConvex.ts b/apps/web/src/app/articles/hooks/useArticlesAdminConvex.ts index 6b0bd0c..63d403f 100644 --- a/apps/web/src/app/articles/hooks/useArticlesAdminConvex.ts +++ b/apps/web/src/app/articles/hooks/useArticlesAdminConvex.ts @@ -2,8 +2,10 @@ import type { Id } from "@opencom/convex/dataModel"; import { + useWebAction, useWebMutation, useWebQuery, + webActionRef, webMutationRef, webQueryRef, } from "@/lib/convex/hooks"; @@ -79,6 +81,18 @@ type LogExportArgs = WorkspaceArgs & { recordCount: number; }; +type BackfillEmbeddingsArgs = WorkspaceArgs & { + contentTypes?: ("article" | "internalArticle" | "snippet")[]; + batchSize?: number; + model?: string; +}; + +type BackfillEmbeddingsResult = { + total: number; + processed: number; + skipped: number; +}; + const ARTICLES_LIST_QUERY_REF = webQueryRef("articles:list"); const COLLECTIONS_LIST_HIERARCHY_QUERY_REF = webQueryRef( "collections:listHierarchy" @@ -107,6 +121,9 @@ const GENERATE_ASSET_UPLOAD_URL_REF = webMutationRef("auditLogs:logExport"); +const BACKFILL_EMBEDDINGS_REF = webActionRef( + "embeddings:backfillExisting" +); type UseArticlesAdminConvexOptions = { workspaceId?: Id<"workspaces"> | null; @@ -120,10 +137,7 @@ export function useArticlesAdminConvex({ exportSourceId, }: UseArticlesAdminConvexOptions) { return { - articles: useWebQuery( - ARTICLES_LIST_QUERY_REF, - workspaceId ? { workspaceId } : "skip" - ), + articles: useWebQuery(ARTICLES_LIST_QUERY_REF, workspaceId ? { workspaceId } : "skip"), collections: useWebQuery( COLLECTIONS_LIST_HIERARCHY_QUERY_REF, workspaceId ? { workspaceId } : "skip" @@ -135,10 +149,7 @@ export function useArticlesAdminConvex({ IMPORT_HISTORY_QUERY_REF, workspaceId ? { workspaceId, limit: 10 } : "skip" ), - importSources: useWebQuery( - IMPORT_SOURCES_QUERY_REF, - workspaceId ? { workspaceId } : "skip" - ), + importSources: useWebQuery(IMPORT_SOURCES_QUERY_REF, workspaceId ? { workspaceId } : "skip"), logExport: useWebMutation(LOG_EXPORT_REF), markdownExport: useWebQuery( EXPORT_MARKDOWN_QUERY_REF, @@ -154,5 +165,6 @@ export function useArticlesAdminConvex({ restoreImportRun: useWebMutation(RESTORE_IMPORT_RUN_REF), syncMarkdownFolder: useWebMutation(SYNC_MARKDOWN_FOLDER_REF), unpublishArticle: useWebMutation(UNPUBLISH_ARTICLE_REF), + backfillEmbeddings: useWebAction(BACKFILL_EMBEDDINGS_REF), }; } diff --git a/apps/web/src/app/articles/page.tsx b/apps/web/src/app/articles/page.tsx index c2a3134..5a762b2 100644 --- a/apps/web/src/app/articles/page.tsx +++ b/apps/web/src/app/articles/page.tsx @@ -5,7 +5,7 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useAuth } from "@/contexts/AuthContext"; import { AppLayout } from "@/components/AppLayout"; import { Button } from "@opencom/ui"; -import { Plus } from "lucide-react"; +import { Plus, RefreshCw } from "lucide-react"; import Link from "next/link"; import type { Id } from "@opencom/convex/dataModel"; import { strToU8, zipSync } from "fflate"; @@ -39,12 +39,8 @@ function ArticlesContent() { const searchParams = useSearchParams(); const { activeWorkspace } = useAuth(); const [searchQuery, setSearchQuery] = useState(""); - const [collectionFilter, setCollectionFilter] = useState( - ALL_COLLECTION_FILTER - ); - const [visibilityFilter, setVisibilityFilter] = useState( - ALL_VISIBILITY_FILTER - ); + const [collectionFilter, setCollectionFilter] = useState(ALL_COLLECTION_FILTER); + const [visibilityFilter, setVisibilityFilter] = useState(ALL_VISIBILITY_FILTER); const [statusFilter, setStatusFilter] = useState(ALL_STATUS_FILTER); const [importSourceName, setImportSourceName] = useState(""); const [importTargetCollectionId, setImportTargetCollectionId] = useState< @@ -68,6 +64,9 @@ function ArticlesContent() { const [deleteTarget, setDeleteTarget] = useState(null); const [deleteError, setDeleteError] = useState(null); const [isDeletingArticle, setIsDeletingArticle] = useState(false); + const [isBackfillingEmbeddings, setIsBackfillingEmbeddings] = useState(false); + const [backfillNotice, setBackfillNotice] = useState(null); + const [backfillError, setBackfillError] = useState(null); const folderInputRef = useRef(null); const createQueryHandledRef = useRef(false); const { @@ -84,6 +83,7 @@ function ArticlesContent() { restoreImportRun, syncMarkdownFolder, unpublishArticle, + backfillEmbeddings, } = useArticlesAdminConvex({ workspaceId: activeWorkspace?._id, isExporting, @@ -159,6 +159,31 @@ function ArticlesContent() { } }; + const handleBackfillEmbeddings = async () => { + if (!activeWorkspace?._id) { + return; + } + + setIsBackfillingEmbeddings(true); + setBackfillError(null); + setBackfillNotice(null); + + try { + const result = await backfillEmbeddings({ + workspaceId: activeWorkspace._id, + contentTypes: ["article", "internalArticle"], + }); + setBackfillNotice( + `Embeddings backfilled: ${result.processed} processed, ${result.skipped} skipped (already existed)` + ); + } catch (error) { + console.error("Failed to backfill embeddings:", error); + setBackfillError(error instanceof Error ? error.message : "Failed to backfill embeddings."); + } finally { + setIsBackfillingEmbeddings(false); + } + }; + const buildImportPayload = async () => Promise.all( selectedImportItems.map(async (item) => ({ @@ -517,18 +542,38 @@ function ArticlesContent() {

Articles

Manage public and internal knowledge articles

-
- - - - - +
+
+ + + + +
+
+ + +
+ {backfillNotice &&

{backfillNotice}

} + {backfillError &&

{backfillError}

}
diff --git a/packages/convex/convex/embeddings.ts b/packages/convex/convex/embeddings.ts index fc812af..82d6f7d 100644 --- a/packages/convex/convex/embeddings.ts +++ b/packages/convex/convex/embeddings.ts @@ -439,7 +439,7 @@ export const backfillExisting = authAction({ batchSize: v.optional(v.number()), model: v.optional(v.string()), }, - permission: "articles.read", + permission: "articles.publish", handler: async (ctx, args) => { const contentTypes = args.contentTypes || ["article", "internalArticle", "snippet"]; const batchSize = args.batchSize || 50;