diff --git a/src/app/api/demo-sources/materialize/route.test.ts b/src/app/api/demo-sources/materialize/route.test.ts index a0a3187..3e46ae0 100644 --- a/src/app/api/demo-sources/materialize/route.test.ts +++ b/src/app/api/demo-sources/materialize/route.test.ts @@ -80,6 +80,7 @@ describe("POST /api/demo-sources/materialize", () => { title: "TSLA-Q4-2025-Update.pdf", mimeType: "application/pdf", status: "ready", + demoSourceId: "demo-tsla-q4-2025", documentId: "doc_user_copy", originalFile: { url: "/api/demo-sources/demo-tsla-q4-2025/original", diff --git a/src/components/official-library-panel.test.ts b/src/components/official-library-panel.test.ts index 8131eb0..454cc9d 100644 --- a/src/components/official-library-panel.test.ts +++ b/src/components/official-library-panel.test.ts @@ -77,6 +77,51 @@ describe("OfficialLibraryPanel", () => { expect(addButton.className).toContain("min-[1116px]:opacity-0"); }); + it("marks already added library documents and removes duplicate add actions", () => { + const onOfficialLibrarySourceAdd = vi.fn(); + + render( + React.createElement(OfficialLibraryPanel, { + sources: [ + { + id: "source_spacex", + kind: "workspace", + demoSourceId: "demo-spacex-s1", + title: "spacex-s1.pdf", + status: "ready", + mimeType: "application/pdf", + documentId: "doc_user_copy", + }, + ], + officialLibrarySources: [ + { + librarySourceId: "financial-spacex-s1", + categoryId: "financial-reports", + categoryLabel: "Financial Reports", + title: "spacex-s1.pdf", + sourceUrl: "https://example.com/spacex-s1.pdf", + mimeType: "application/pdf", + status: "ready", + demoSourceId: "demo-spacex-s1", + chunkCount: 922, + }, + ], + onOfficialLibrarySourceAdd, + }), + ); + + fireEvent.click( + screen.getByRole("button", { name: "Open Financial Reports" }), + ); + + expect(screen.getByLabelText("spacex-s1.pdf already added")).toBeTruthy(); + expect(screen.getByText("Added")).toBeTruthy(); + expect( + screen.queryByRole("button", { name: "Add spacex-s1.pdf to sources" }), + ).toBeNull(); + expect(onOfficialLibrarySourceAdd).not.toHaveBeenCalled(); + }); + it("opens to the all-categories view", () => { render( React.createElement(OfficialLibraryPanel, { diff --git a/src/components/official-library-panel.tsx b/src/components/official-library-panel.tsx index 8ab70f8..4787bdf 100644 --- a/src/components/official-library-panel.tsx +++ b/src/components/official-library-panel.tsx @@ -1,7 +1,7 @@ "use client"; import { type CSSProperties, type ReactElement, useMemo, useState } from "react"; -import { ChevronRight, FileText, Plus, RotateCcw } from "lucide-react"; +import { Check, ChevronRight, FileText, Plus, RotateCcw } from "lucide-react"; import Image from "next/image"; import { ScrollArea } from "@/components/ui/scroll-area"; @@ -31,6 +31,7 @@ type LibraryItem = { readonly demoSourceId?: string; readonly librarySourceId: string; readonly mimeType: string; + readonly isAdded: boolean; readonly sourceUrl: string; readonly status: "ready" | "planned"; readonly title: string; @@ -188,28 +189,42 @@ function OfficialLibraryCard({ readonly item: LibraryItem; readonly onAdd?: () => void; }): ReactElement { - const canAdd = item.status === "ready" && Boolean(onAdd); + const canAdd = item.status === "ready" && Boolean(onAdd) && !item.isAdded; return (
- - - - - - - add to sources - - - + {item.isAdded ? ( + + + Added + + ) : ( + + + + + + + add to sources + + + + )}

{item.title} @@ -257,6 +272,11 @@ function getLibraryItems( sources: readonly SourceView[], officialLibrarySources: readonly OfficialLibrarySourceView[], ): LibraryItem[] { + const addedDemoSourceIdSet = new Set( + sources + .filter((source) => source.kind !== "demo") + .flatMap((source) => (source.demoSourceId ? [source.demoSourceId] : [])), + ); const metadataByLibrarySourceId = new Map( officialLibrarySources.map((source) => [source.librarySourceId, source]), ); @@ -268,6 +288,9 @@ function getLibraryItems( categoryLabel: source.categoryLabel, chunkCount: source.chunkCount, demoSourceId: source.demoSourceId, + isAdded: + source.demoSourceId !== undefined && + addedDemoSourceIdSet.has(source.demoSourceId), librarySourceId: source.librarySourceId, mimeType: source.mimeType, sourceUrl: source.sourceUrl, @@ -289,6 +312,11 @@ function getLibraryItems( getCategoryLabel(source.officialLibrary.categoryId), chunkCount: source.chunkCount ?? metadata?.chunkCount, demoSourceId: source.demoSourceId ?? metadata?.demoSourceId, + isAdded: + (source.demoSourceId !== undefined && + addedDemoSourceIdSet.has(source.demoSourceId)) || + (metadata?.demoSourceId !== undefined && + addedDemoSourceIdSet.has(metadata.demoSourceId)), librarySourceId: source.officialLibrary.librarySourceId, mimeType: source.mimeType, sourceUrl: source.officialLibrary.sourceUrl, diff --git a/src/components/workspace-shell.test.ts b/src/components/workspace-shell.test.ts index 20abcc5..9d70378 100644 --- a/src/components/workspace-shell.test.ts +++ b/src/components/workspace-shell.test.ts @@ -1044,6 +1044,7 @@ describe("WorkspaceShell", () => { title: "spacex-s1.pdf", status: "ready", mimeType: "application/pdf", + demoSourceId: "demo-spacex-s1", documentId: "doc_user_copy", chunkCount: 1, }, @@ -1103,6 +1104,19 @@ describe("WorkspaceShell", () => { render( React.createElement(C, { + officialLibrarySources: [ + { + librarySourceId: "financial-spacex-s1", + categoryId: "financial-reports", + categoryLabel: "Financial Reports", + title: "spacex-s1.pdf", + sourceUrl: "https://example.com/spacex-s1.pdf", + mimeType: "application/pdf", + status: "ready", + demoSourceId: "demo-spacex-s1", + chunkCount: 922, + }, + ], sources: [ { id: "demo-spacex-s1", @@ -1176,23 +1190,21 @@ describe("WorkspaceShell", () => { const desktopChatPanel = within(screen.getByTestId("desktop-chat-panel")); await desktopChatPanel.findByText("Refreshed materialized answer."); expect(desktopChatPanel.queryByText("Seeded canonical answer.")).toBeNull(); - - await user.click( - desktopChatPanel.getByRole("button", { - name: "Open source spacex-s1.pdf ยท Overview", - }), + const refreshedLibraryPanel = within( + within(screen.getByTestId("desktop-chunks-panel")).getByTestId( + "official-library-panel", + ), ); - - await waitFor(() => { - const topRow = screen - .getByTestId("desktop-chunks-panel") - .querySelector('[data-index="0"]'); - - expect(topRow?.getAttribute("data-chunk-id")).toBe( - "source_spacex:chunk_1", - ); - expect(topRow?.getAttribute("data-focused-chunk")).toBe("true"); - }); + expect( + refreshedLibraryPanel.getByRole("heading", { name: "Library" }), + ).toBeTruthy(); + expect(refreshedLibraryPanel.getByLabelText("spacex-s1.pdf already added")) + .toBeTruthy(); + expect( + refreshedLibraryPanel.queryByRole("button", { + name: "Add spacex-s1.pdf to sources", + }), + ).toBeNull(); expect(countFetches(fetch, "/api/chat/threads/thread_1")).toBe(1); }); diff --git a/src/components/workspace-shell.tsx b/src/components/workspace-shell.tsx index da9edcd..566b482 100644 --- a/src/components/workspace-shell.tsx +++ b/src/components/workspace-shell.tsx @@ -137,7 +137,6 @@ function WorkspaceShellContent({ const didMaterialize = await sourceWorkflow.handleOfficialLibrarySourceAdd(demoSourceId) if (didMaterialize) { - setContentView("chunks") await chatWorkflow.handleRefreshActiveChatThread() } } diff --git a/src/components/workspace-source-workflow.test.ts b/src/components/workspace-source-workflow.test.ts index 239107e..c98b24c 100644 --- a/src/components/workspace-source-workflow.test.ts +++ b/src/components/workspace-source-workflow.test.ts @@ -134,6 +134,7 @@ describe("useWorkspaceSourceWorkflow", () => { const materializedSource = makeSource({ id: "source_spacex", kind: "workspace", + demoSourceId: "demo-spacex-s1", title: "spacex-s1.pdf", documentId: "doc_spacex", }) @@ -157,6 +158,9 @@ describe("useWorkspaceSourceWorkflow", () => { expect(result.current.sources.map((source) => source.id)).toEqual([ "source_spacex", ]) + expect(result.current.sources[0]).toMatchObject({ + demoSourceId: "demo-spacex-s1", + }) expect(result.current.selectedSourceId).toBe("source_spacex") }) diff --git a/src/domains/sources/route-service.test.ts b/src/domains/sources/route-service.test.ts index 68e50bd..9679620 100644 --- a/src/domains/sources/route-service.test.ts +++ b/src/domains/sources/route-service.test.ts @@ -378,6 +378,7 @@ describe("source route service", () => { expect.objectContaining({ id: "source_demo", kind: "workspace", + demoSourceId: "demo-tsla-q4-2025", documentId: "doc_user_copy", chunkCount: 70, }), diff --git a/src/domains/sources/view.test.ts b/src/domains/sources/view.test.ts index 93698dc..912d4e8 100644 --- a/src/domains/sources/view.test.ts +++ b/src/domains/sources/view.test.ts @@ -87,6 +87,7 @@ describe("toSourceView", () => { ), ).toMatchObject({ title: "TSLA-Q4-2025-Update.pdf", + demoSourceId: "demo-tsla-q4-2025", documentId: "doc_user_copy", chunkCount: 70, originalFile: { diff --git a/src/domains/sources/view.ts b/src/domains/sources/view.ts index 18f1b5a..43e96d4 100644 --- a/src/domains/sources/view.ts +++ b/src/domains/sources/view.ts @@ -29,6 +29,7 @@ export function toSourceView( title: source.title, mimeType: source.mimeType, status: toSourceStatus(source.status), + ...(source.demoKey ? { demoSourceId: source.demoKey } : {}), documentId: source.knowhereDocumentId ?? undefined, ...(originalFile ? { originalFile } : {}), ...(options.chunkCount !== undefined