Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions apps/web/src/app/articles/hooks/useArticlesAdminConvex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import type { Id } from "@opencom/convex/dataModel";
import {
useWebAction,
useWebMutation,
useWebQuery,
webActionRef,
webMutationRef,
webQueryRef,
} from "@/lib/convex/hooks";
Expand Down Expand Up @@ -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<WorkspaceArgs, ArticleListItem[]>("articles:list");
const COLLECTIONS_LIST_HIERARCHY_QUERY_REF = webQueryRef<WorkspaceArgs, CollectionListItem[]>(
"collections:listHierarchy"
Expand Down Expand Up @@ -107,6 +121,9 @@ const GENERATE_ASSET_UPLOAD_URL_REF = webMutationRef<GenerateAssetUploadUrlArgs,
"articles:generateAssetUploadUrl"
);
const LOG_EXPORT_REF = webMutationRef<LogExportArgs, null>("auditLogs:logExport");
const BACKFILL_EMBEDDINGS_REF = webActionRef<BackfillEmbeddingsArgs, BackfillEmbeddingsResult>(
"embeddings:backfillExisting"
);

type UseArticlesAdminConvexOptions = {
workspaceId?: Id<"workspaces"> | null;
Expand All @@ -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"
Expand All @@ -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,
Expand All @@ -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),
};
}
83 changes: 64 additions & 19 deletions apps/web/src/app/articles/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -39,12 +39,8 @@ function ArticlesContent() {
const searchParams = useSearchParams();
const { activeWorkspace } = useAuth();
const [searchQuery, setSearchQuery] = useState("");
const [collectionFilter, setCollectionFilter] = useState<CollectionFilter>(
ALL_COLLECTION_FILTER
);
const [visibilityFilter, setVisibilityFilter] = useState<VisibilityFilter>(
ALL_VISIBILITY_FILTER
);
const [collectionFilter, setCollectionFilter] = useState<CollectionFilter>(ALL_COLLECTION_FILTER);
const [visibilityFilter, setVisibilityFilter] = useState<VisibilityFilter>(ALL_VISIBILITY_FILTER);
const [statusFilter, setStatusFilter] = useState<StatusFilter>(ALL_STATUS_FILTER);
const [importSourceName, setImportSourceName] = useState("");
const [importTargetCollectionId, setImportTargetCollectionId] = useState<
Expand All @@ -68,6 +64,9 @@ function ArticlesContent() {
const [deleteTarget, setDeleteTarget] = useState<DeleteArticleTarget | null>(null);
const [deleteError, setDeleteError] = useState<string | null>(null);
const [isDeletingArticle, setIsDeletingArticle] = useState(false);
const [isBackfillingEmbeddings, setIsBackfillingEmbeddings] = useState(false);
const [backfillNotice, setBackfillNotice] = useState<string | null>(null);
const [backfillError, setBackfillError] = useState<string | null>(null);
const folderInputRef = useRef<HTMLInputElement | null>(null);
const createQueryHandledRef = useRef(false);
const {
Expand All @@ -84,6 +83,7 @@ function ArticlesContent() {
restoreImportRun,
syncMarkdownFolder,
unpublishArticle,
backfillEmbeddings,
} = useArticlesAdminConvex({
workspaceId: activeWorkspace?._id,
isExporting,
Expand Down Expand Up @@ -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) => ({
Expand Down Expand Up @@ -517,18 +542,38 @@ function ArticlesContent() {
<h1 className="text-2xl font-bold">Articles</h1>
<p className="text-gray-500">Manage public and internal knowledge articles</p>
</div>
<div className="flex gap-2">
<Link href="/articles/collections">
<Button variant="outline">Manage Collections</Button>
</Link>
<Button variant="outline" onClick={() => void handleCreateArticle("internal")}>
<Plus className="h-4 w-4 mr-2" />
New Internal Article
</Button>
<Button onClick={() => void handleCreateArticle("public")}>
<Plus className="h-4 w-4 mr-2" />
New Article
</Button>
<div className="flex flex-col gap-2 items-end">
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => void handleBackfillEmbeddings()}
disabled={
isBackfillingEmbeddings ||
!activeWorkspace?._id ||
(activeWorkspace.role !== "owner" && activeWorkspace.role !== "admin")
}
>
<RefreshCw
className={`h-4 w-4 mr-2 ${isBackfillingEmbeddings ? "animate-spin" : ""}`}
/>
{isBackfillingEmbeddings ? "Refreshing..." : "Refresh Embeddings"}
</Button>
<Link href="/articles/collections">
<Button variant="outline">Manage Collections</Button>
</Link>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => void handleCreateArticle("internal")}>
<Plus className="h-4 w-4 mr-2" />
New Internal Article
</Button>
<Button onClick={() => void handleCreateArticle("public")}>
<Plus className="h-4 w-4 mr-2" />
New Article
</Button>
</div>
{backfillNotice && <p className="text-sm text-green-600">{backfillNotice}</p>}
{backfillError && <p className="text-sm text-red-600">{backfillError}</p>}
</div>
</div>

Expand Down
2 changes: 1 addition & 1 deletion packages/convex/convex/embeddings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading