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
72 changes: 36 additions & 36 deletions public/demo-sources/tsla-q4-2025/chunks.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions public/demo-sources/tsla-q4-2025/doc_nav.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": "1.0",
"file_name": "TSLA-Q4-2025-Update(1).pdf",
"file_name": "TSLA-Q4-2025-Update.pdf",
"stats": {
"total_chunks": 70,
"text_chunks": 36,
Expand All @@ -11,7 +11,7 @@
"sections": [
{
"title": "Root",
"path": "Default_Root/TSLA-Q4-2025-Update(1).pdf-->HIGHLIGHTS",
"path": "Default_Root/TSLA-Q4-2025-Update.pdf-->HIGHLIGHTS",
"level": 1,
"summary": "[tables/table-0 Tesla 2025 Results.html]",
"chunk_count": 36,
Expand Down
2 changes: 1 addition & 1 deletion public/demo-sources/tsla-q4-2025/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"version": "2.0",
"job_id": "job_b7886be54d96",
"data_id": null,
"source_file_name": "TSLA-Q4-2025-Update(1).pdf",
"source_file_name": "TSLA-Q4-2025-Update.pdf",
"processing_date": "2026-05-12T02:42:26.779662Z",
"processing": {
"page_count": 35,
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/sources/[sourceId]/chunks/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe("GET /api/sources/[sourceId]/chunks", () => {
chunks: [
{
documentId: "demo-doc-tsla-q4-2025",
sourceTitle: "TSLA-Q4-2025-Update(1).pdf",
sourceTitle: "TSLA-Q4-2025-Update.pdf",
},
],
pagination: {
Expand Down
2 changes: 1 addition & 1 deletion src/components/chat-message-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ function MessageBubble({
type="button"
disabled={!onCitationClick || isPending}
onClick={() => onCitationClick?.(cite, citationId)}
className="inline-flex max-w-full cursor-pointer items-center gap-1.5 whitespace-normal rounded-lg border border-border bg-muted px-2 py-0 text-left text-[10px] font-medium text-muted-foreground shadow-2xs transition-colors hover:bg-muted/70 focus:outline-none focus:ring-4 focus:ring-ring/15 focus:ring-offset-2 focus:ring-offset-background disabled:cursor-wait disabled:opacity-75"
className="inline-flex max-w-full cursor-pointer items-center gap-1 whitespace-normal rounded-sm px-0.5 py-0 text-left text-[11px] font-semibold text-primary underline decoration-primary/45 underline-offset-4 transition-colors hover:text-primary/80 hover:decoration-primary focus:outline-none focus:ring-4 focus:ring-ring/15 focus:ring-offset-2 focus:ring-offset-background disabled:cursor-wait disabled:opacity-75"
aria-label={`Open source ${label}`}
>
{isPending && <Spinner className="size-3" />}
Expand Down
13 changes: 9 additions & 4 deletions src/components/chat-panel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ describe("ChatPanel", () => {
);
});

it("renders citation links as buttons with per-citation loading feedback", async () => {
it("renders citation links as button-backed links with per-citation loading feedback", async () => {
const user = userEvent.setup();
const onCitationClick = vi.fn();

Expand Down Expand Up @@ -136,12 +136,16 @@ describe("ChatPanel", () => {

expect(within(citationButton).getByRole("status", { name: "Loading" }))
.toBeTruthy();
expect(citationButton.className).toContain("underline");
expect(citationButton.className).toContain("text-primary");
expect(citationButton.className).not.toContain("bg-muted");
expect(citationButton.className).not.toContain("border-border");

await user.click(citationButton);
expect(onCitationClick).not.toHaveBeenCalled();
});

it("uses less-rounded corners for wrapped parsed chunk source links", () => {
it("keeps long citation links wrapped without making them look like chips", () => {
render(
React.createElement(C, {
messages: [
Expand Down Expand Up @@ -171,8 +175,9 @@ describe("ChatPanel", () => {
});

expect(sourceLink.className).toContain("whitespace-normal");
expect(sourceLink.className).toContain("rounded-lg");
expect(sourceLink.className).not.toContain("rounded-full");
expect(sourceLink.className).toContain("underline");
expect(sourceLink.className).not.toContain("rounded-lg");
expect(sourceLink.className).not.toContain("bg-muted");
});

it("shows button-level loading for chat API actions", async () => {
Expand Down
28 changes: 24 additions & 4 deletions src/components/chunks-panel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,20 @@ describe("ChunksPanel", () => {
});

it("switches to a download-only original file state for unsupported previews", async () => {
mockVisibleVirtualViewport();
const user = userEvent.setup();

render(
React.createElement(C, {
chunks: [],
chunks: [
{
chunkId: "chunk_1",
type: "text",
content: "Legacy report details live in the original file.",
sourceTitle: "brief.doc",
pageNums: [2],
},
],
selectedSource: "brief.doc",
selectedSourceFile: {
url: "https://store.public.blob.vercel-storage.com/source-uploads/upload_1/document.doc",
Expand All @@ -77,7 +86,14 @@ describe("ChunksPanel", () => {
}),
);

await user.click(screen.getByRole("button", { name: "Original" }));
const openOriginalButton = screen.getByRole("button", {
name: "Open original file",
});

expect(openOriginalButton.className).toContain("font-normal");
expect(openOriginalButton.className).not.toContain("font-semibold");

await user.click(openOriginalButton);

const downloadLink = screen.getByRole("link", {
name: "Download original file",
Expand Down Expand Up @@ -141,7 +157,9 @@ describe("ChunksPanel", () => {
}),
);

await user.click(screen.getByRole("button", { name: "Open page 2" }));
await user.click(
screen.getByRole("button", { name: "Open page 2 in original file" }),
);

expect(screen.getByRole("heading", { name: "Original File" })).toBeTruthy();
expect(screen.getByTestId("source-original-preview").getAttribute(
Expand Down Expand Up @@ -180,7 +198,9 @@ describe("ChunksPanel", () => {
}),
);

await user.click(screen.getByRole("button", { name: "Open page 2" }));
await user.click(
screen.getByRole("button", { name: "Open page 2 in original file" }),
);

const mountedOriginalPreview = screen.getByTestId("source-original-preview");

Expand Down
10 changes: 10 additions & 0 deletions src/components/chunks-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { SourceOriginalPreview } from "@/components/source-original-preview";
import { useChunksPanelWorkflow } from "@/components/chunks-panel-workflow";
import { ParsedChunkCard } from "@/components/parsed-chunk-card";
import { useSourceOriginalPreviewWarmup } from "@/components/source-original-preview-warmup";
import { sourceOriginalPreviewModel } from "@/components/source-original-preview-model";
import type { ParsedChunkView } from "@/domains/chunks/types";
import type { SourceOriginalFileView } from "@/domains/sources/types";
import { cn } from "@/lib/utils";
Expand Down Expand Up @@ -41,6 +42,11 @@ export function ChunksPanel({
onLoadMore,
}: Partial<ChunksPanelProps> = {}) {
const originalPreviewCacheKey = selectedSourceFile?.url ?? null;
const isOriginalPreviewAvailable =
sourceOriginalPreviewModel.canPreviewOriginalFile(
selectedSource,
selectedSourceFile,
);
const [mountedOriginalPreviewKey, setMountedOriginalPreviewKey] = useState<
string | null
>(null);
Expand Down Expand Up @@ -185,6 +191,7 @@ export function ChunksPanel({
virtualItem={virtualItem}
chunk={visibleChunks[virtualItem.index]}
focusedChunkId={activeFocusedChunkId}
isOriginalPreviewAvailable={isOriginalPreviewAvailable}
measureElement={measureVirtualChunkElement}
onChunkClick={
hasOriginalFile ? handleChunkSelected : undefined
Expand Down Expand Up @@ -283,13 +290,15 @@ function VirtualChunkRow({
virtualItem,
chunk,
focusedChunkId,
isOriginalPreviewAvailable,
measureElement,
onChunkClick,
onReferenceClick,
}: {
virtualItem: VirtualItem;
chunk: ParsedChunkView | undefined;
focusedChunkId: string | null;
isOriginalPreviewAvailable: boolean;
measureElement: (node: HTMLDivElement | null) => void;
onChunkClick?: (chunk: ParsedChunkView) => void;
onReferenceClick: (chunkId: string) => void;
Expand All @@ -316,6 +325,7 @@ function VirtualChunkRow({
<ParsedChunkCard
chunk={chunk}
isFocused={chunk.chunkId === focusedChunkId}
isOriginalPreviewAvailable={isOriginalPreviewAvailable}
onChunkClick={onChunkClick}
onReferenceClick={onReferenceClick}
/>
Expand Down
41 changes: 40 additions & 1 deletion src/components/parsed-chunk-card.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,56 @@ describe("ParsedChunkCard", () => {
React.createElement(ParsedChunkCard, {
chunk,
isFocused: false,
isOriginalPreviewAvailable: true,
onChunkClick,
onReferenceClick: vi.fn(),
}),
);

await user.click(screen.getByRole("button", { name: "Open page 2" }));
const openOriginalButton = screen.getByRole("button", {
name: "Open page 2 in original file",
});

expect(openOriginalButton.className).toContain("font-semibold");
expect(openOriginalButton.className).toContain("text-primary");

await user.click(openOriginalButton);
expect(onChunkClick).toHaveBeenCalledWith(chunk);
expect(screen.getByTestId("chunk-card-shell-text_1").getAttribute("role")).toBeNull();
});

it("keeps original file buttons quiet when preview is not supported", async () => {
const user = userEvent.setup();
const chunk = {
chunkId: "text_1",
type: "text" as const,
content: "Legacy report details.",
sourceTitle: "report.doc",
pageNums: [2],
};
const onChunkClick = vi.fn();

render(
React.createElement(ParsedChunkCard, {
chunk,
isFocused: false,
isOriginalPreviewAvailable: false,
onChunkClick,
onReferenceClick: vi.fn(),
}),
);

const openOriginalButton = screen.getByRole("button", {
name: "Open original file",
});

expect(openOriginalButton.className).toContain("font-normal");
expect(openOriginalButton.className).not.toContain("font-semibold");

await user.click(openOriginalButton);
expect(onChunkClick).toHaveBeenCalledWith(chunk);
});

it("sanitizes table HTML before rendering", () => {
render(
React.createElement(ParsedChunkCard, {
Expand Down
Loading
Loading