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
1 change: 1 addition & 0 deletions src/app/api/demo-sources/materialize/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
45 changes: 45 additions & 0 deletions src/components/official-library-panel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
68 changes: 48 additions & 20 deletions src/components/official-library-panel.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 (
<article className="group relative flex min-w-0 flex-col items-center rounded-sm p-3 text-center transition-colors hover:bg-muted/60">
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
disabled={!canAdd || isAdding}
onClick={onAdd}
className="absolute right-3 top-1 inline-flex size-6 items-center justify-center rounded-md bg-background/95 text-muted-foreground opacity-100 shadow-xs transition-opacity hover:bg-background hover:text-foreground focus:opacity-100 disabled:cursor-not-allowed disabled:opacity-40 min-[1116px]:bg-transparent min-[1116px]:opacity-0 min-[1116px]:shadow-none min-[1116px]:group-hover:opacity-100"
aria-label={`Add ${item.title} to sources`}
>
{isAdding ? <Spinner className="size-3.5" /> : <Plus className="size-4" />}
</button>
</TooltipTrigger>
<TooltipContent side="top" className="bg-zinc-950 text-white">
add to sources
</TooltipContent>
</Tooltip>
</TooltipProvider>
{item.isAdded ? (
<span
aria-label={`${item.title} already added`}
className="absolute right-3 top-1 inline-flex h-6 items-center gap-1 rounded-md bg-primary/10 px-2 text-[11px] font-semibold text-primary"
>
<Check className="size-3" />
Added
</span>
) : (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
disabled={!canAdd || isAdding}
onClick={onAdd}
className="absolute right-3 top-1 inline-flex size-6 items-center justify-center rounded-md bg-background/95 text-muted-foreground opacity-100 shadow-xs transition-opacity hover:bg-background hover:text-foreground focus:opacity-100 disabled:cursor-not-allowed disabled:opacity-40 min-[1116px]:bg-transparent min-[1116px]:opacity-0 min-[1116px]:shadow-none min-[1116px]:group-hover:opacity-100"
aria-label={`Add ${item.title} to sources`}
>
{isAdding ? (
<Spinner className="size-3.5" />
) : (
<Plus className="size-4" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="top" className="bg-zinc-950 text-white">
add to sources
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<PdfFileIcon />
<h3 className="mt-3 max-w-[132px] truncate text-sm font-bold text-foreground">
{item.title}
Expand Down Expand Up @@ -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]),
);
Expand All @@ -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,
Expand All @@ -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,
Expand Down
44 changes: 28 additions & 16 deletions src/components/workspace-shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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<HTMLElement>('[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);
});

Expand Down
1 change: 0 additions & 1 deletion src/components/workspace-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ function WorkspaceShellContent({
const didMaterialize =
await sourceWorkflow.handleOfficialLibrarySourceAdd(demoSourceId)
if (didMaterialize) {
setContentView("chunks")
await chatWorkflow.handleRefreshActiveChatThread()
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/components/workspace-source-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
})
Expand All @@ -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")
})

Expand Down
1 change: 1 addition & 0 deletions src/domains/sources/route-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
Expand Down
1 change: 1 addition & 0 deletions src/domains/sources/view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
1 change: 1 addition & 0 deletions src/domains/sources/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading