From b27f816ea43e65d2ce9f22b795baea01fcc4cd26 Mon Sep 17 00:00:00 2001 From: suguanYang Date: Wed, 26 Aug 2026 12:16:25 +0800 Subject: [PATCH 1/5] feat: show type, page, section, and entities on parsing cards Make Parsed Results cards scannable by surfacing Knowhere type, page, hierarchical path, and extracted entity tags instead of parse-file paths and duplicate keywords. Co-authored-by: Cursor --- CONTEXT.md | 4 +- .../parsed-chunk-card-model.test.ts | 32 +++++-- src/components/parsed-chunk-card-model.ts | 57 ++++++++++-- src/components/parsed-chunk-card.test.ts | 54 ++++++++++- src/components/parsed-chunk-card.tsx | 89 +++++++++++++++---- src/domains/chunks/index.test.ts | 37 +++++++- src/domains/chunks/normalization.ts | 38 ++++++-- src/domains/chunks/types.ts | 7 +- 8 files changed, 278 insertions(+), 40 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 80a0498..1d3e95d 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -133,8 +133,8 @@ summary, keywords, and connection metadata. ## Parsed Chunk Card A Parsed Chunk Card renders one Parsed Chunk. It owns chunk source metadata, -content rendering, summaries, keywords, artifact references, and sanitized -table HTML for that card only. +content rendering, summaries, extracted entities, keywords, artifact +references, and sanitized table HTML for that card only. ## Chat Thread diff --git a/src/components/parsed-chunk-card-model.test.ts b/src/components/parsed-chunk-card-model.test.ts index 8384c5f..5cbfdd6 100644 --- a/src/components/parsed-chunk-card-model.test.ts +++ b/src/components/parsed-chunk-card-model.test.ts @@ -17,7 +17,7 @@ describe("parsedChunkCardModel", () => { expect(metadata).toEqual({ pageLabel: "Pages 2, 4, 8-9", - sectionLabel: "FINANCIAL SUMMARY", + sectionLabel: "Root / FINANCIAL SUMMARY", typeLabel: "Text", }) }) @@ -33,18 +33,19 @@ describe("parsedChunkCardModel", () => { expect(metadata).toEqual({ pageLabel: "Pages 4-6", - sectionLabel: "pages/4-6", + sectionLabel: "Root / pages/4-6", typeLabel: "Page", }) }) - it("uses Page N and the parse path for page-asset cards", () => { + it("uses Page N and the hierarchical section path for page-asset cards", () => { const metadata = parsedChunkCardModel.getSourceMetadata( makeChunk({ type: "page", pageNums: [4], filePath: "pages/page-000004.png", - sectionPath: "Page 4", + sectionPath: + "Default_Root/Micron Q1-26 Earnings Deck_R.pdf-->Safe harbor statement", pageAssets: [ { pageNumber: 4, @@ -57,11 +58,32 @@ describe("parsedChunkCardModel", () => { expect(metadata).toEqual({ pageLabel: "Page 4", - sectionLabel: "pages/page-000004.png", + sectionLabel: "Root / Safe harbor statement", typeLabel: "Page", }) }) + it("extracts unique entity tags from typed Knowhere entities", () => { + const tags = parsedChunkCardModel.getEntityTags( + makeChunk({ + entities: [ + { text: "Securities and Exchange Commission", type: "organization" }, + { text: "Form 10-K", type: "document" }, + { text: "Form 10-K", type: "document" }, + { text: " ", type: "organization" }, + ], + }), + ) + + expect(tags).toEqual([ + { + text: "Securities and Exchange Commission", + type: "organization", + }, + { text: "Form 10-K", type: "document" }, + ]) + }) + it("splits text content into text and reference parts with display-ready labels", () => { const parts = parsedChunkCardModel.getTextContentParts( makeChunk({ diff --git a/src/components/parsed-chunk-card-model.ts b/src/components/parsed-chunk-card-model.ts index 7b5d652..d8943d7 100644 --- a/src/components/parsed-chunk-card-model.ts +++ b/src/components/parsed-chunk-card-model.ts @@ -12,6 +12,11 @@ type ChunkSourceMetadata = { readonly typeLabel: string } +type ChunkEntityTag = { + readonly text: string + readonly type: string | null +} + type TextChunkContentPart = | { readonly type: "text" @@ -28,6 +33,7 @@ type TextChunkContentPart = type ParsedChunkCardModelModule = { readonly getChunkTypeLabel: (type: ParsedChunkView["type"]) => string + readonly getEntityTags: (chunk: ParsedChunkView) => readonly ChunkEntityTag[] readonly getFocusCardClasses: (isFocused: boolean) => string readonly getSanitizedTableHtml: (content: string) => string | null readonly getSourceMetadata: (chunk: ParsedChunkView) => ChunkSourceMetadata @@ -59,29 +65,63 @@ const tableAllowedAttributes = [ function getSourceMetadata(chunk: ParsedChunkView): ChunkSourceMetadata { if (chunk.type === "page") { const pageFromAssets = chunk.pageAssets?.[0]?.pageNumber - const parsePath = - getTrimmedParsePath(chunk.filePath) ?? - chunksPanelState.formatChunkSectionPath(chunk.sectionPath) return { pageLabel: pageFromAssets ? `Page ${pageFromAssets}` : formatPageNumbers(chunk.pageNums), - sectionLabel: parsePath, + sectionLabel: formatCardSectionPath(chunk), typeLabel: getChunkTypeLabel(chunk.type), } } return { pageLabel: formatPageNumbers(chunk.pageNums), - sectionLabel: chunksPanelState.formatChunkSectionPath(chunk.sectionPath), + sectionLabel: formatCardSectionPath(chunk), typeLabel: getChunkTypeLabel(chunk.type), } } -function getTrimmedParsePath(value: string | null | undefined): string | null { - const trimmed = value?.trim() ?? "" - return trimmed.length > 0 ? trimmed : null +function formatCardSectionPath(chunk: ParsedChunkView): string | null { + const formattedPath = chunksPanelState.formatChunkSectionPath(chunk.sectionPath) + if (formattedPath) { + return prefixKnowhereRoot(chunk.sectionPath, formattedPath) + } + + const parsePath = chunk.filePath?.trim() ?? "" + return parsePath.length > 0 ? parsePath : null +} + +function prefixKnowhereRoot( + sectionPath: ParsedChunkView["sectionPath"], + formattedPath: string, +): string { + const trimmedSectionPath = sectionPath?.trim() ?? "" + if (!trimmedSectionPath.startsWith("Default_Root")) return formattedPath + if (formattedPath === "Root" || formattedPath.startsWith("Root / ")) { + return formattedPath + } + + return `Root / ${formattedPath}` +} + +function getEntityTags(chunk: ParsedChunkView): readonly ChunkEntityTag[] { + const seenTexts = new Set() + const tags: ChunkEntityTag[] = [] + + for (const entity of chunk.entities ?? []) { + const text = entity.text.trim() + if (!text) continue + + const dedupeKey = text.toLowerCase() + if (seenTexts.has(dedupeKey)) continue + seenTexts.add(dedupeKey) + + const type = entity.type?.trim() || null + tags.push({ text, type }) + } + + return tags } function getTextContentParts( @@ -181,6 +221,7 @@ function formatPageNumbers( export const parsedChunkCardModel: ParsedChunkCardModelModule = { getChunkTypeLabel, + getEntityTags, getFocusCardClasses, getSanitizedTableHtml, getSourceMetadata, diff --git a/src/components/parsed-chunk-card.test.ts b/src/components/parsed-chunk-card.test.ts index 8eab32e..ba6d9b8 100644 --- a/src/components/parsed-chunk-card.test.ts +++ b/src/components/parsed-chunk-card.test.ts @@ -57,7 +57,7 @@ describe("ParsedChunkCard", () => { sourceTitle: "manual.pdf", sectionPath: "Default_Root/manual.pdf-->pages/4-6", pageNums: [4, 5, 6], - entities: [{ text: "refund", label: "topic" }], + entities: [{ text: "refund", type: "topic" }], }, isFocused: false, isOriginalPreviewAvailable: true, @@ -72,15 +72,67 @@ describe("ParsedChunkCard", () => { expect(screen.getByTestId("chunk-source-panel-page_1").textContent).toContain( "Pages 4-6", ); + expect(screen.getByTestId("chunk-source-panel-page_1").textContent).toContain( + "Root / pages/4-6", + ); expect(screen.getByTestId("chunk-content-panel-page_1").textContent).toContain( "The refund policy is summarized", ); + expect(screen.getByTestId("chunk-entities-panel-page_1").textContent).toContain( + "refund", + ); expect(screen.queryByTestId("chunk-summary-panel-page_1")).toBeNull(); expect( screen.getByRole("button", { name: "Open page 4 in original file" }), ).toBeTruthy(); }); + it("renders extracted entities as tags and hides duplicate keyword rows", () => { + render( + React.createElement(ParsedChunkCard, { + chunk: { + chunkId: "page_2", + type: "page", + content: "Safe harbor statement", + sourceTitle: "Micron Q1-26 Earnings Deck_R.pdf", + sectionPath: + "Default_Root/Micron Q1-26 Earnings Deck_R.pdf-->Safe harbor statement", + pageNums: [2], + keywords: [ + "Securities and Exchange Commission", + "Form 10-K", + "Forms ID-9", + ], + entities: [ + { text: "Securities and Exchange Commission", type: "organization" }, + { text: "Form 10-K", type: "document" }, + { text: "Forms ID-9", type: "document" }, + ], + pageAssets: [ + { + pageNumber: 2, + assetUrl: "https://assets.example/page-2.png", + contentType: "image/png", + }, + ], + }, + isFocused: false, + onReferenceClick: vi.fn(), + }), + ); + + const sourcePanel = screen.getByTestId("chunk-source-panel-page_2"); + expect(sourcePanel.textContent).toContain("Page 2"); + expect(sourcePanel.textContent).toContain("Root / Safe harbor statement"); + expect(screen.getByRole("img", { name: "Page 2" })).toBeTruthy(); + expect(screen.getByTestId("chunk-entities-panel-page_2").textContent).toContain( + "Securities and Exchange Commission", + ); + expect(screen.getByText("Form 10-K")).toBeTruthy(); + expect(screen.getByText("Forms ID-9")).toBeTruthy(); + expect(screen.queryByTestId("chunk-keywords-panel-page_2")).toBeNull(); + }); + it("renders page citation assets instead of page summary content", () => { render( React.createElement(ParsedChunkCard, { diff --git a/src/components/parsed-chunk-card.tsx b/src/components/parsed-chunk-card.tsx index e32a664..86198b3 100644 --- a/src/components/parsed-chunk-card.tsx +++ b/src/components/parsed-chunk-card.tsx @@ -25,6 +25,12 @@ const keywordPanelClassName = "rounded-lg border border-emerald-200/70 bg-emerald-50/70 p-3 shadow-[0_1px_0_rgba(16,185,129,0.08)] dark:border-emerald-400/20 dark:bg-emerald-950/20"; const keywordBadgeClassName = "rounded-md border border-emerald-200/80 bg-emerald-100/90 px-2 py-0.5 text-[11px] font-semibold text-emerald-800 shadow-[0_1px_0_rgba(16,185,129,0.10)] hover:bg-emerald-100 dark:border-emerald-400/25 dark:bg-emerald-400/10 dark:text-emerald-200"; +const entityPanelClassName = + "rounded-lg border border-primary/20 bg-primary/5 p-3"; +const entityBadgeClassName = + "rounded-md border border-primary/30 bg-background px-2 py-0.5 text-[11px] font-medium text-primary hover:bg-primary/5 dark:border-primary/40 dark:bg-background dark:text-primary"; +const sourceChipClassName = + "h-6 max-w-full rounded-md px-2 text-[11px] font-medium"; type TextChunkReferencePart = Extract< ReturnType[number], { readonly type: "reference" } @@ -182,30 +188,41 @@ function ChunkSourcePanel({ "grid size-9 shrink-0 place-items-center rounded-lg border shadow-inner", getChunkIconClasses(chunk.type), )} + aria-hidden="true" > {renderChunkIcon(chunk.type)} -
-
+
+ + {sourceMetadata.typeLabel} + + {sourceMetadata.pageLabel ? ( - {sourceMetadata.typeLabel} + {sourceMetadata.pageLabel} - {sourceMetadata.pageLabel ? ( - - {sourceMetadata.pageLabel} - - ) : null} -
+ ) : null} {sourceMetadata.sectionLabel ? ( -

+ {sourceMetadata.sectionLabel} -

+ ) : null}
@@ -313,11 +330,47 @@ function ChunkContentPanel({ ); } +function ChunkEntities({ + chunk, +}: { + readonly chunk: ParsedChunkView; +}): ReactNode { + const entities = parsedChunkCardModel.getEntityTags(chunk); + if (entities.length === 0) return null; + + return ( +
+ } + label="Entities" + className="text-primary" + iconClassName="text-primary" + /> +
+ {entities.map((entity) => ( + + {entity.text} + + ))} +
+
+ ); +} + function ChunkKeywords({ chunk, }: { readonly chunk: ParsedChunkView; }): ReactNode { + if (parsedChunkCardModel.getEntityTags(chunk).length > 0) return null; if (!chunk.keywords || chunk.keywords.length === 0) return null; return ( @@ -396,6 +449,7 @@ function TextChunkCard({ {renderTextChunkContent(chunk, onReferenceClick)} + ); @@ -449,6 +503,7 @@ function PageChunkCard({

)} + ); @@ -639,6 +694,7 @@ function ImageChunkCard({ )} + ); @@ -775,6 +831,7 @@ function TableChunkCard({ )} + ); diff --git a/src/domains/chunks/index.test.ts b/src/domains/chunks/index.test.ts index e4ab72c..873bd13 100644 --- a/src/domains/chunks/index.test.ts +++ b/src/domains/chunks/index.test.ts @@ -149,7 +149,7 @@ describe("toParsedChunkView", () => { metadata: { summary: "Refund eligibility is summarized across pages 4 to 6.", page_nums: [4, 5, 6], - entities: [{ text: "refund", label: "topic" }], + entities: [{ text: "refund", type: "topic" }], }, assetUrl: "https://assets.example/crop.pdf", }), @@ -164,11 +164,44 @@ describe("toParsedChunkView", () => { content: "Refund eligibility is summarized across pages 4 to 6.", readableContent: "Refund eligibility is summarized across pages 4 to 6.", pageNums: [4, 5, 6], - entities: [{ text: "refund", label: "topic" }], + entities: [{ text: "refund", type: "topic" }], assetUrl: "https://assets.example/crop.pdf", }); }); + it("normalizes Knowhere entity metadata into typed entity tags", () => { + const chunk = makeDocumentChunk({ + metadata: { + entities: [ + { text: "Form 10-K", type: "document" }, + { text: "Alphabet Inc.", label: "organization" }, + { text: " " }, + "Nasdaq", + ], + }, + }); + + expect(toParsedChunkView(chunk, "manual.pdf", "doc_123").entities).toEqual([ + { text: "Form 10-K", type: "document" }, + { text: "Alphabet Inc.", type: "organization" }, + { text: "Nasdaq" }, + ]); + }); + + it("parses JSON-string entity metadata from parser rows", () => { + const chunk = makeDocumentChunk({ + metadata: { + entities: JSON.stringify([ + { text: "Nasdaq", type: "organization" }, + ]), + }, + }); + + expect(toParsedChunkView(chunk, "manual.pdf", "doc_123").entities).toEqual([ + { text: "Nasdaq", type: "organization" }, + ]); + }); + it("maps usable page citation assets on page chunks", () => { const chunk = makeDocumentChunk({ id: "document_page_1", diff --git a/src/domains/chunks/normalization.ts b/src/domains/chunks/normalization.ts index 53fe88b..4e39ac5 100644 --- a/src/domains/chunks/normalization.ts +++ b/src/domains/chunks/normalization.ts @@ -2,6 +2,7 @@ import type { ChatCitationView } from "@/domains/chat/types" import type { ChunkType, ParsedChunkConnection, + ParsedChunkEntity, ParsedChunkView, } from "@/domains/chunks/types" @@ -304,15 +305,42 @@ function getStringArrayMetadata( return strings.length > 0 ? strings : undefined } -function getEntities( - value: unknown, -): readonly Readonly>[] | undefined { - if (!Array.isArray(value)) return undefined +function getEntities(value: unknown): readonly ParsedChunkEntity[] | undefined { + const parsedValue = parseEntityPayload(value) + if (!Array.isArray(parsedValue)) return undefined - const entities = value.filter(isRecord) + const entities = parsedValue.flatMap(toParsedChunkEntity) return entities.length > 0 ? entities : undefined } +function parseEntityPayload(value: unknown): unknown { + if (typeof value !== "string") return value + + const trimmed = value.trim() + if (!trimmed) return undefined + + try { + const parsed: unknown = JSON.parse(trimmed) + return parsed + } catch { + return undefined + } +} + +function toParsedChunkEntity(value: unknown): readonly ParsedChunkEntity[] { + if (typeof value === "string") { + const text = getString(value) + return text ? [{ text }] : [] + } + if (!isRecord(value)) return [] + + const text = getString(value["text"]) ?? getString(value["name"]) + if (!text) return [] + + const type = getString(value["type"]) ?? getString(value["label"]) + return type ? [{ text, type }] : [{ text }] +} + function getPageNumbers(value: unknown): number[] | undefined { if (!Array.isArray(value)) return undefined diff --git a/src/domains/chunks/types.ts b/src/domains/chunks/types.ts index fedb17a..23129a3 100644 --- a/src/domains/chunks/types.ts +++ b/src/domains/chunks/types.ts @@ -13,6 +13,11 @@ export type ParsedChunkConnection = { } } +export type ParsedChunkEntity = { + readonly text: string + readonly type?: string +} + /** * Parsed Content panel row. Mirrors the Knowhere document-chunk shape. */ @@ -36,7 +41,7 @@ export type ParsedChunkView = { readonly summary?: string readonly keywords?: readonly string[] readonly pageNums?: readonly number[] - readonly entities?: readonly Readonly>[] + readonly entities?: readonly ParsedChunkEntity[] readonly connections?: readonly ParsedChunkConnection[] /** Display-only attribution. */ readonly sourceTitle: string From e311b004abb809dee12b227fba47dcd89fba6cb5 Mon Sep 17 00:00:00 2001 From: suguanYang Date: Wed, 26 Aug 2026 14:52:16 +0800 Subject: [PATCH 2/5] fix: match parsing card header to Figma page pill and breadcrumb Render the card header as the Figma spec: a Page N pill plus Root / current-section breadcrumb, without the extra type icon and chips. Co-authored-by: Cursor --- src/components/chunks-panel.test.ts | 3 +- .../parsed-chunk-card-model.test.ts | 6 + src/components/parsed-chunk-card-model.ts | 39 +++-- src/components/parsed-chunk-card.test.ts | 10 +- src/components/parsed-chunk-card.tsx | 137 +++++++----------- 5 files changed, 92 insertions(+), 103 deletions(-) diff --git a/src/components/chunks-panel.test.ts b/src/components/chunks-panel.test.ts index a56da3f..43e7b60 100644 --- a/src/components/chunks-panel.test.ts +++ b/src/components/chunks-panel.test.ts @@ -1194,8 +1194,9 @@ describe("ChunksPanel", () => { expect(financialSourcePanel.textContent).not.toContain( "TSLA-Q4-2025-Update.pdf", ); + expect(storageSourcePanel.textContent).toContain("OPERATIONAL SUMMARY"); expect(storageSourcePanel.textContent).toContain( - "OPERATIONAL SUMMARY / Energy generation and storage", + "Energy generation and storage", ); expect(storageSourcePanel.textContent).not.toContain("Default_Root"); expect(storageSourcePanel.textContent).not.toContain( diff --git a/src/components/parsed-chunk-card-model.test.ts b/src/components/parsed-chunk-card-model.test.ts index 5cbfdd6..507a199 100644 --- a/src/components/parsed-chunk-card-model.test.ts +++ b/src/components/parsed-chunk-card-model.test.ts @@ -18,7 +18,9 @@ describe("parsedChunkCardModel", () => { expect(metadata).toEqual({ pageLabel: "Pages 2, 4, 8-9", sectionLabel: "Root / FINANCIAL SUMMARY", + sectionSegments: ["Root", "FINANCIAL SUMMARY"], typeLabel: "Text", + leadLabel: "Pages 2, 4, 8-9", }) }) @@ -34,7 +36,9 @@ describe("parsedChunkCardModel", () => { expect(metadata).toEqual({ pageLabel: "Pages 4-6", sectionLabel: "Root / pages/4-6", + sectionSegments: ["Root", "pages/4-6"], typeLabel: "Page", + leadLabel: "Pages 4-6", }) }) @@ -59,7 +63,9 @@ describe("parsedChunkCardModel", () => { expect(metadata).toEqual({ pageLabel: "Page 4", sectionLabel: "Root / Safe harbor statement", + sectionSegments: ["Root", "Safe harbor statement"], typeLabel: "Page", + leadLabel: "Page 4", }) }) diff --git a/src/components/parsed-chunk-card-model.ts b/src/components/parsed-chunk-card-model.ts index d8943d7..8a88cc0 100644 --- a/src/components/parsed-chunk-card-model.ts +++ b/src/components/parsed-chunk-card-model.ts @@ -9,7 +9,9 @@ import type { type ChunkSourceMetadata = { readonly pageLabel: string | null readonly sectionLabel: string | null + readonly sectionSegments: readonly string[] readonly typeLabel: string + readonly leadLabel: string } type ChunkEntityTag = { @@ -63,25 +65,34 @@ const tableAllowedAttributes = [ ] as const function getSourceMetadata(chunk: ParsedChunkView): ChunkSourceMetadata { - if (chunk.type === "page") { - const pageFromAssets = chunk.pageAssets?.[0]?.pageNumber - - return { - pageLabel: pageFromAssets - ? `Page ${pageFromAssets}` - : formatPageNumbers(chunk.pageNums), - sectionLabel: formatCardSectionPath(chunk), - typeLabel: getChunkTypeLabel(chunk.type), - } - } + const typeLabel = getChunkTypeLabel(chunk.type) + const pageFromAssets = + chunk.type === "page" ? chunk.pageAssets?.[0]?.pageNumber : undefined + const pageLabel = pageFromAssets + ? `Page ${pageFromAssets}` + : formatPageNumbers(chunk.pageNums) + const sectionLabel = formatCardSectionPath(chunk) return { - pageLabel: formatPageNumbers(chunk.pageNums), - sectionLabel: formatCardSectionPath(chunk), - typeLabel: getChunkTypeLabel(chunk.type), + pageLabel, + sectionLabel, + sectionSegments: getSectionPathSegments(sectionLabel), + typeLabel, + leadLabel: pageLabel ?? typeLabel, } } +function getSectionPathSegments( + sectionLabel: string | null, +): readonly string[] { + if (!sectionLabel) return [] + + return sectionLabel + .split(" / ") + .map((segment) => segment.trim()) + .filter((segment) => segment.length > 0) +} + function formatCardSectionPath(chunk: ParsedChunkView): string | null { const formattedPath = chunksPanelState.formatChunkSectionPath(chunk.sectionPath) if (formattedPath) { diff --git a/src/components/parsed-chunk-card.test.ts b/src/components/parsed-chunk-card.test.ts index ba6d9b8..20adff2 100644 --- a/src/components/parsed-chunk-card.test.ts +++ b/src/components/parsed-chunk-card.test.ts @@ -67,13 +67,13 @@ describe("ParsedChunkCard", () => { ); expect(screen.getByTestId("chunk-source-panel-page_1").textContent).toContain( - "Page", + "Pages 4-6", ); expect(screen.getByTestId("chunk-source-panel-page_1").textContent).toContain( - "Pages 4-6", + "Root", ); expect(screen.getByTestId("chunk-source-panel-page_1").textContent).toContain( - "Root / pages/4-6", + "pages/4-6", ); expect(screen.getByTestId("chunk-content-panel-page_1").textContent).toContain( "The refund policy is summarized", @@ -123,7 +123,9 @@ describe("ParsedChunkCard", () => { const sourcePanel = screen.getByTestId("chunk-source-panel-page_2"); expect(sourcePanel.textContent).toContain("Page 2"); - expect(sourcePanel.textContent).toContain("Root / Safe harbor statement"); + expect(sourcePanel.textContent).toContain("Root"); + expect(sourcePanel.textContent).toContain("Safe harbor statement"); + expect(sourcePanel.textContent).not.toContain("PAGE"); expect(screen.getByRole("img", { name: "Page 2" })).toBeTruthy(); expect(screen.getByTestId("chunk-entities-panel-page_2").textContent).toContain( "Securities and Exchange Commission", diff --git a/src/components/parsed-chunk-card.tsx b/src/components/parsed-chunk-card.tsx index 86198b3..2d614de 100644 --- a/src/components/parsed-chunk-card.tsx +++ b/src/components/parsed-chunk-card.tsx @@ -29,8 +29,6 @@ const entityPanelClassName = "rounded-lg border border-primary/20 bg-primary/5 p-3"; const entityBadgeClassName = "rounded-md border border-primary/30 bg-background px-2 py-0.5 text-[11px] font-medium text-primary hover:bg-primary/5 dark:border-primary/40 dark:bg-background dark:text-primary"; -const sourceChipClassName = - "h-6 max-w-full rounded-md px-2 text-[11px] font-medium"; type TextChunkReferencePart = Extract< ReturnType[number], { readonly type: "reference" } @@ -148,16 +146,16 @@ function ChunkCardFrame({ return ( + - {children} @@ -175,68 +173,59 @@ function ChunkSourcePanel({ }): ReactNode { const sourceMetadata = parsedChunkCardModel.getSourceMetadata(chunk); const firstPageNumber = getFirstValidPageNumber(chunk); + const lastSegmentIndex = sourceMetadata.sectionSegments.length - 1; return (
-
-
- -
- - {sourceMetadata.typeLabel} - - {sourceMetadata.pageLabel ? ( - - {sourceMetadata.pageLabel} - - ) : null} - {sourceMetadata.sectionLabel ? ( - + {sourceMetadata.leadLabel} + + {sourceMetadata.sectionSegments.length > 0 ? ( +
-
- {onChunkClick && - firstPageNumber !== null && - !hasPageCitationAssets(chunk) ? ( - - ) : null} -
+ {index > 0 ? ( + + / + + ) : null} + + {segment} + + + ); + })} + + ) : ( +
+ )} + {onChunkClick && + firstPageNumber !== null && + !hasPageCitationAssets(chunk) ? ( + + ) : null}
); } @@ -836,23 +825,3 @@ function TableChunkCard({ ); } - -function renderChunkIcon(type: ParsedChunkView["type"]): ReactNode { - if (type === "page") return ; - if (type === "image") return ; - if (type === "table") return ; - return ; -} - -function getChunkIconClasses(type: ParsedChunkView["type"]): string { - if (type === "page") { - return "border-amber-500/20 bg-amber-500/10 text-amber-700 dark:text-amber-300"; - } - if (type === "image") { - return "border-violet-500/15 bg-violet-500/10 text-violet-600 dark:text-violet-300"; - } - if (type === "table") { - return "border-primary/15 bg-primary/10 text-primary"; - } - return "border-border bg-muted/60 text-muted-foreground"; -} From f1e1dc553686da6a2138905521558aaf9b0c350c Mon Sep 17 00:00:00 2001 From: suguanYang Date: Wed, 26 Aug 2026 15:31:21 +0800 Subject: [PATCH 3/5] fix: drop page-image chrome that is not in Figma Show the rendered page directly under the header. Remove the PAGE IMAGE section label and the Page N / mime-type bar. Co-authored-by: Cursor --- src/components/chunks-panel.test.ts | 3 ++- src/components/parsed-chunk-card.test.ts | 5 ++++- src/components/parsed-chunk-card.tsx | 19 ++++++------------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/components/chunks-panel.test.ts b/src/components/chunks-panel.test.ts index 43e7b60..973e90c 100644 --- a/src/components/chunks-panel.test.ts +++ b/src/components/chunks-panel.test.ts @@ -119,7 +119,8 @@ describe("ChunksPanel", () => { expect( await screen.findByRole("img", { name: "Page 4" }), ).toBeTruthy(); - expect(screen.getByText("image/png")).toBeTruthy(); + expect(screen.queryByText("Page image")).toBeNull(); + expect(screen.queryByText("image/png")).toBeNull(); expect(screen.queryByText("Budget")).toBeNull(); expect(screen.queryByTestId("chunk-card-shell-table_1")).toBeNull(); expect( diff --git a/src/components/parsed-chunk-card.test.ts b/src/components/parsed-chunk-card.test.ts index 20adff2..5215444 100644 --- a/src/components/parsed-chunk-card.test.ts +++ b/src/components/parsed-chunk-card.test.ts @@ -127,6 +127,8 @@ describe("ParsedChunkCard", () => { expect(sourcePanel.textContent).toContain("Safe harbor statement"); expect(sourcePanel.textContent).not.toContain("PAGE"); expect(screen.getByRole("img", { name: "Page 2" })).toBeTruthy(); + expect(screen.queryByText("Page image")).toBeNull(); + expect(screen.queryByText("image/png")).toBeNull(); expect(screen.getByTestId("chunk-entities-panel-page_2").textContent).toContain( "Securities and Exchange Commission", ); @@ -167,7 +169,8 @@ describe("ParsedChunkCard", () => { expect(pageImage.getAttribute("src")).toBe( "https://assets.example/page-4.png", ); - expect(screen.getByText("image/png")).toBeTruthy(); + expect(screen.queryByText("Page image")).toBeNull(); + expect(screen.queryByText("image/png")).toBeNull(); expect(screen.queryByText(/summary should not be primary/i)).toBeNull(); expect( screen.queryByRole("button", { name: /original file/i }), diff --git a/src/components/parsed-chunk-card.tsx b/src/components/parsed-chunk-card.tsx index 2d614de..c262ddb 100644 --- a/src/components/parsed-chunk-card.tsx +++ b/src/components/parsed-chunk-card.tsx @@ -473,10 +473,7 @@ function PageChunkCard({ onChunkClick={onChunkClick} > {pageAssets.length > 0 ? ( - +
- +
) : ( - +

{chunk.readableContent ?? chunk.content}

- +
)} @@ -550,13 +547,9 @@ function PageCitationAssetImage({ const isImageLoaded = loadedAssetUrl === imageAssetUrl; return ( -
-
- Page {asset.pageNumber} - {asset.contentType} -
+
From c8e94e9cfd0208b6bcc626f360d332b8e5befcf7 Mon Sep 17 00:00:00 2001 From: suguanYang Date: Wed, 26 Aug 2026 15:35:36 +0800 Subject: [PATCH 4/5] fix: strip image-card content chrome and flatten entity tags Image previews no longer wrap in a Content/Summary panel. Entity tags match the Figma footer: link icon plus outline chips, without a tinted box. Co-authored-by: Cursor --- src/components/parsed-chunk-card.test.ts | 2 + src/components/parsed-chunk-card.tsx | 52 ++++++++++++------------ 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/components/parsed-chunk-card.test.ts b/src/components/parsed-chunk-card.test.ts index 5215444..97cd4b5 100644 --- a/src/components/parsed-chunk-card.test.ts +++ b/src/components/parsed-chunk-card.test.ts @@ -343,6 +343,8 @@ describe("ParsedChunkCard", () => { expect(image.getAttribute("src")).toBe( `/api/parsed-assets/inline?url=${encodeURIComponent(assetUrl)}`, ); + expect(screen.queryByText("Content")).toBeNull(); + expect(screen.queryByTestId("chunk-summary-panel-image_1")).toBeNull(); }); it("routes resolved artifact reference clicks to the target chunk", async () => { diff --git a/src/components/parsed-chunk-card.tsx b/src/components/parsed-chunk-card.tsx index c262ddb..539ec5c 100644 --- a/src/components/parsed-chunk-card.tsx +++ b/src/components/parsed-chunk-card.tsx @@ -6,6 +6,7 @@ import { FileText, ImageIcon, ImageOff, + Link2, Table2, Tags, TextQuote, @@ -25,10 +26,8 @@ const keywordPanelClassName = "rounded-lg border border-emerald-200/70 bg-emerald-50/70 p-3 shadow-[0_1px_0_rgba(16,185,129,0.08)] dark:border-emerald-400/20 dark:bg-emerald-950/20"; const keywordBadgeClassName = "rounded-md border border-emerald-200/80 bg-emerald-100/90 px-2 py-0.5 text-[11px] font-semibold text-emerald-800 shadow-[0_1px_0_rgba(16,185,129,0.10)] hover:bg-emerald-100 dark:border-emerald-400/25 dark:bg-emerald-400/10 dark:text-emerald-200"; -const entityPanelClassName = - "rounded-lg border border-primary/20 bg-primary/5 p-3"; const entityBadgeClassName = - "rounded-md border border-primary/30 bg-background px-2 py-0.5 text-[11px] font-medium text-primary hover:bg-primary/5 dark:border-primary/40 dark:bg-background dark:text-primary"; + "rounded-md border border-primary/35 bg-background px-2 py-0.5 text-[11px] font-medium text-primary"; type TextChunkReferencePart = Extract< ReturnType[number], { readonly type: "reference" } @@ -330,13 +329,10 @@ function ChunkEntities({ return (
} + icon={} label="Entities" - className="text-primary" - iconClassName="text-primary" />
{entities.map((entity) => ( @@ -635,10 +631,9 @@ function ImageChunkCard({ isOriginalPreviewAvailable={isOriginalPreviewAvailable} onChunkClick={onChunkClick} > - - - {inlineImageAssetUrl ? ( -
+ {inlineImageAssetUrl ? ( +
+
- ) : ( -
- -
-

- Image chunk -

-

- {chunk.summary - ? chunk.summary - : "Image content is not available in this view."} -

+
+ ) : ( + <> + + +
+ +
+

+ Image chunk +

+

+ {chunk.summary + ? chunk.summary + : "Image content is not available in this view."} +

+
-
- )} - + + + )} From f2c5c7eaaa4d742b17108ebd16c50c93bf0d4df2 Mon Sep 17 00:00:00 2001 From: suguanYang Date: Wed, 26 Aug 2026 16:03:00 +0800 Subject: [PATCH 5/5] fix: drop remaining non-Figma chrome from parsing cards Summary, Content, and Keywords panels and the Open original button are gone so cards match Figma: header, body, and entity tags. Co-authored-by: Cursor --- CONTEXT.md | 4 +- src/components/chunks-panel.test.ts | 37 +-- src/components/chunks-panel.tsx | 28 -- .../parsed-chunk-card-model.test.ts | 14 + src/components/parsed-chunk-card-model.ts | 13 + src/components/parsed-chunk-card.test.ts | 91 +----- src/components/parsed-chunk-card.tsx | 278 ++---------------- 7 files changed, 85 insertions(+), 380 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 1d3e95d..55f83eb 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -133,8 +133,8 @@ summary, keywords, and connection metadata. ## Parsed Chunk Card A Parsed Chunk Card renders one Parsed Chunk. It owns chunk source metadata, -content rendering, summaries, extracted entities, keywords, artifact -references, and sanitized table HTML for that card only. +content rendering, extracted entities, artifact references, and sanitized +table HTML for that card only. ## Chat Thread diff --git a/src/components/chunks-panel.test.ts b/src/components/chunks-panel.test.ts index 973e90c..3eb138a 100644 --- a/src/components/chunks-panel.test.ts +++ b/src/components/chunks-panel.test.ts @@ -886,11 +886,10 @@ describe("ChunksPanel", () => { selectListView(); const openOriginalButton = screen.getByRole("button", { - name: "Open original file", + name: "Original", }); - expect(openOriginalButton.className).toContain("font-normal"); - expect(openOriginalButton.className).not.toContain("font-semibold"); + expect(openOriginalButton).toBeTruthy(); await user.click(openOriginalButton); @@ -942,7 +941,7 @@ describe("ChunksPanel", () => { ); }); - it("opens the original PDF preview at the clicked chunk page", async () => { + it("opens the original PDF preview from the Original tab", async () => { mockVisibleVirtualViewport(); const user = userEvent.setup(); vi.stubGlobal( @@ -974,14 +973,10 @@ describe("ChunksPanel", () => { ); selectListView(); - await user.click( - screen.getByRole("button", { name: "Open page 2 in original file" }), - ); + await user.click(screen.getByRole("button", { name: "Original" })); expect(screen.getByRole("heading", { name: "Original File" })).toBeTruthy(); - expect(screen.getByTestId("source-original-preview").getAttribute( - "data-target-page", - )).toBe("2"); + expect(screen.getByTestId("source-original-preview")).toBeTruthy(); }); it("keeps the original PDF preview mounted when switching back to parsed chunks", async () => { @@ -1016,9 +1011,7 @@ describe("ChunksPanel", () => { ); selectListView(); - await user.click( - screen.getByRole("button", { name: "Open page 2 in original file" }), - ); + await user.click(screen.getByRole("button", { name: "Original" })); const mountedOriginalPreview = screen.getByTestId("source-original-preview"); @@ -1205,7 +1198,7 @@ describe("ChunksPanel", () => { ); }); - it("renders text chunks with structured source, summary, content, and keyword sections", () => { + it("renders text chunks with source, unlabeled content, and entity tags", () => { mockVisibleVirtualViewport(); render( @@ -1233,20 +1226,18 @@ describe("ChunksPanel", () => { expect( screen.getByTestId("chunk-source-panel-text_1").textContent, ).not.toContain("TSLA-Q4-2025-UPDATE.PDF"); - expect(screen.getByTestId("chunk-summary-panel-text_1").textContent).toContain( - "Tesla continues to use its North American footprint", - ); + expect(screen.queryByTestId("chunk-summary-panel-text_1")).toBeNull(); + expect(screen.queryByText("Summary")).toBeNull(); + expect(screen.queryByText("Content")).toBeNull(); + expect(screen.queryByText("Keywords")).toBeNull(); expect(screen.getByTestId("chunk-content-panel-text_1").textContent).toContain( "Tesla is adding Supercharging and AI training capacity.", ); - expect(screen.getByTestId("chunk-keywords-panel-text_1").textContent).toContain( + expect(screen.getByTestId("chunk-entities-panel-text_1").textContent).toContain( "AI training capacity", ); - expect( - screen.getByTestId("chunk-keywords-panel-text_1").className, - ).toContain("bg-emerald-50/70"); - expect(screen.getByText("Robotaxi").className).toContain("bg-emerald-100/90"); - expect(screen.getByText("Robotaxi").className).toContain("text-emerald-800"); + expect(screen.getByText("Robotaxi").className).toContain("text-primary"); + expect(screen.getByText("Robotaxi").className).toContain("border-primary/35"); }); it("allows horizontal scrolling for wide chunk content", async () => { diff --git a/src/components/chunks-panel.tsx b/src/components/chunks-panel.tsx index 84298bd..e9286dd 100644 --- a/src/components/chunks-panel.tsx +++ b/src/components/chunks-panel.tsx @@ -41,7 +41,6 @@ import type { ChatImageHighlightBox } from "@/domains/chat/types"; import { chunksPanelState } from "@/components/chunks-panel-state"; import { MAX_UPLOAD_MB } from "@/domains/sources/validation"; 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, @@ -122,12 +121,6 @@ export function ChunksPanel({ ); const effectiveVisibleView = isPageAssetSource ? "parsed" : undefined; const originalPreviewCacheKey = selectedSourceFile?.url ?? null; - const isOriginalPreviewAvailable = - !isPageAssetSource && - sourceOriginalPreviewModel.canPreviewOriginalFile( - selectedSource, - selectedSourceFile, - ); const [mountedOriginalPreviewKey, setMountedOriginalPreviewKey] = useState< string | null >(null); @@ -142,11 +135,9 @@ export function ChunksPanel({ useState(sectionTreeDefaultZoomPercent); const { activeFocusedChunkId, - handleChunkSelected: selectChunk, handleOriginalViewSelected: selectOriginalView, handleParsedViewSelected, handleViewportScroll, - hasOriginalFile, hasOriginalView, measureVirtualChunkElement, originalTargetPageNumber, @@ -183,13 +174,6 @@ export function ChunksPanel({ } }, [originalPreviewCacheKey]); - const handleChunkSelected = useCallback( - (chunk: ParsedChunkView): void => { - rememberOriginalPreview(); - selectChunk(chunk); - }, - [rememberOriginalPreview, selectChunk], - ); const handleOriginalViewSelected = useCallback((): void => { rememberOriginalPreview(); selectOriginalView(); @@ -438,13 +422,7 @@ export function ChunksPanel({ focusedPageNumber={focusedPageNumber} focusedPageRequestId={focusedPageRequestId} highlightRegions={focusedHighlightRegions} - isOriginalPreviewAvailable={isOriginalPreviewAvailable} measureElement={measureVirtualChunkElement} - onChunkClick={ - hasOriginalFile && !isPageAssetSource - ? handleChunkSelected - : undefined - } onReferenceClick={requestChunkFocus} selectedSourceFile={selectedSourceFile} /> @@ -1223,9 +1201,7 @@ function VirtualChunkRow({ focusedPageNumber, focusedPageRequestId, highlightRegions, - isOriginalPreviewAvailable, measureElement, - onChunkClick, onReferenceClick, selectedSourceFile, }: { @@ -1236,9 +1212,7 @@ function VirtualChunkRow({ focusedPageNumber: number | null; focusedPageRequestId: number; highlightRegions: readonly ChatImageHighlightBox[]; - isOriginalPreviewAvailable: boolean; measureElement: (node: HTMLDivElement | null) => void; - onChunkClick?: (chunk: ParsedChunkView) => void; onReferenceClick: (chunkId: string) => void; selectedSourceFile: SourceOriginalFileView | null; }): ReactNode { @@ -1268,8 +1242,6 @@ function VirtualChunkRow({ focusedPageNumber={focusedPageNumber} focusedPageRequestId={focusedPageRequestId} highlightRegions={highlightRegions} - isOriginalPreviewAvailable={isOriginalPreviewAvailable} - onChunkClick={onChunkClick} onReferenceClick={onReferenceClick} sourceOriginalFile={selectedSourceFile} /> diff --git a/src/components/parsed-chunk-card-model.test.ts b/src/components/parsed-chunk-card-model.test.ts index 507a199..dd559b4 100644 --- a/src/components/parsed-chunk-card-model.test.ts +++ b/src/components/parsed-chunk-card-model.test.ts @@ -72,6 +72,7 @@ describe("parsedChunkCardModel", () => { it("extracts unique entity tags from typed Knowhere entities", () => { const tags = parsedChunkCardModel.getEntityTags( makeChunk({ + keywords: ["Robotaxi"], entities: [ { text: "Securities and Exchange Commission", type: "organization" }, { text: "Form 10-K", type: "document" }, @@ -90,6 +91,19 @@ describe("parsedChunkCardModel", () => { ]) }) + it("falls back to keyword texts as entity tags when entities are missing", () => { + const tags = parsedChunkCardModel.getEntityTags( + makeChunk({ + keywords: ["Robotaxi", "Supercharging", "Robotaxi", " "], + }), + ) + + expect(tags).toEqual([ + { text: "Robotaxi", type: null }, + { text: "Supercharging", type: null }, + ]) + }) + it("splits text content into text and reference parts with display-ready labels", () => { const parts = parsedChunkCardModel.getTextContentParts( makeChunk({ diff --git a/src/components/parsed-chunk-card-model.ts b/src/components/parsed-chunk-card-model.ts index 8a88cc0..49696d4 100644 --- a/src/components/parsed-chunk-card-model.ts +++ b/src/components/parsed-chunk-card-model.ts @@ -132,6 +132,19 @@ function getEntityTags(chunk: ParsedChunkView): readonly ChunkEntityTag[] { tags.push({ text, type }) } + if (tags.length > 0) return tags + + for (const keyword of chunk.keywords ?? []) { + const text = keyword.trim() + if (!text) continue + + const dedupeKey = text.toLowerCase() + if (seenTexts.has(dedupeKey)) continue + seenTexts.add(dedupeKey) + + tags.push({ text, type: null }) + } + return tags } diff --git a/src/components/parsed-chunk-card.test.ts b/src/components/parsed-chunk-card.test.ts index 97cd4b5..544bc61 100644 --- a/src/components/parsed-chunk-card.test.ts +++ b/src/components/parsed-chunk-card.test.ts @@ -11,7 +11,7 @@ describe("ParsedChunkCard", () => { cleanup(); }); - it("renders text chunks with source, summary, content, and keywords", () => { + it("renders text chunks with source, content, and entity tags", () => { render( React.createElement(ParsedChunkCard, { chunk: { @@ -31,13 +31,14 @@ describe("ParsedChunkCard", () => { expect(screen.getByTestId("chunk-source-panel-text_1").textContent).toContain( "Capacity", ); - expect( - screen.getByTestId("chunk-summary-panel-text_1").textContent, - ).toContain("Tesla continues to add capacity."); + expect(screen.queryByTestId("chunk-summary-panel-text_1")).toBeNull(); + expect(screen.queryByText("Summary")).toBeNull(); + expect(screen.queryByText("Content")).toBeNull(); + expect(screen.queryByText("Keywords")).toBeNull(); expect(screen.getByTestId("chunk-content-panel-text_1").textContent).toContain( "Tesla is adding Supercharging and AI training capacity.", ); - expect(screen.getByTestId("chunk-keywords-panel-text_1").textContent).toContain( + expect(screen.getByTestId("chunk-entities-panel-text_1").textContent).toContain( "AI training capacity", ); expect(screen.getByTestId("chunk-card-shell-text_1").className).toContain( @@ -60,8 +61,6 @@ describe("ParsedChunkCard", () => { entities: [{ text: "refund", type: "topic" }], }, isFocused: false, - isOriginalPreviewAvailable: true, - onChunkClick: vi.fn(), onReferenceClick: vi.fn(), }), ); @@ -83,8 +82,8 @@ describe("ParsedChunkCard", () => { ); expect(screen.queryByTestId("chunk-summary-panel-page_1")).toBeNull(); expect( - screen.getByRole("button", { name: "Open page 4 in original file" }), - ).toBeTruthy(); + screen.queryByRole("button", { name: /original file/i }), + ).toBeNull(); }); it("renders extracted entities as tags and hides duplicate keyword rows", () => { @@ -158,8 +157,6 @@ describe("ParsedChunkCard", () => { ], }, isFocused: false, - isOriginalPreviewAvailable: true, - onChunkClick: vi.fn(), onReferenceClick: vi.fn(), }), ); @@ -377,51 +374,17 @@ describe("ParsedChunkCard", () => { expect(onReferenceClick).toHaveBeenCalledWith("image_1"); }); - it("shows an explicit original preview button when chunk preview is available", async () => { - const user = userEvent.setup(); - const chunk = { - chunkId: "text_1", - type: "text" as const, - content: "Revenue details live on the second page.", - sourceTitle: "report.pdf", - pageNums: [2], - }; - const onChunkClick = vi.fn(); - - render( - React.createElement(ParsedChunkCard, { - chunk, - isFocused: false, - isOriginalPreviewAvailable: true, - onChunkClick, - onReferenceClick: vi.fn(), - }), - ); - - 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("hides the original file button when a chunk has no page numbers", () => { + it("does not show an original file button on parsed cards", () => { render( React.createElement(ParsedChunkCard, { chunk: { chunkId: "text_1", type: "text", - content: "Revenue details do not include page metadata.", + content: "Revenue details live on the second page.", sourceTitle: "report.pdf", + pageNums: [2], }, isFocused: false, - isOriginalPreviewAvailable: true, - onChunkClick: vi.fn(), onReferenceClick: vi.fn(), }), ); @@ -431,38 +394,6 @@ describe("ParsedChunkCard", () => { ).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, { diff --git a/src/components/parsed-chunk-card.tsx b/src/components/parsed-chunk-card.tsx index 539ec5c..28f8bc0 100644 --- a/src/components/parsed-chunk-card.tsx +++ b/src/components/parsed-chunk-card.tsx @@ -1,19 +1,9 @@ "use client"; import { useMemo, useState, type MouseEvent, type ReactNode } from "react"; -import { - FileSearch, - FileText, - ImageIcon, - ImageOff, - Link2, - Table2, - Tags, - TextQuote, -} from "lucide-react"; +import { ImageIcon, ImageOff, Link2, Table2 } from "lucide-react"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { parsedChunkCardModel } from "@/components/parsed-chunk-card-model"; import { CitationRegionHighlight } from "@/components/citation-region-highlight"; @@ -22,10 +12,6 @@ import type { ParsedChunkView } from "@/domains/chunks/types"; import type { SourceOriginalFileView } from "@/domains/sources/types"; import { cn } from "@/lib/utils"; -const keywordPanelClassName = - "rounded-lg border border-emerald-200/70 bg-emerald-50/70 p-3 shadow-[0_1px_0_rgba(16,185,129,0.08)] dark:border-emerald-400/20 dark:bg-emerald-950/20"; -const keywordBadgeClassName = - "rounded-md border border-emerald-200/80 bg-emerald-100/90 px-2 py-0.5 text-[11px] font-semibold text-emerald-800 shadow-[0_1px_0_rgba(16,185,129,0.10)] hover:bg-emerald-100 dark:border-emerald-400/25 dark:bg-emerald-400/10 dark:text-emerald-200"; const entityBadgeClassName = "rounded-md border border-primary/35 bg-background px-2 py-0.5 text-[11px] font-medium text-primary"; type TextChunkReferencePart = Extract< @@ -40,8 +26,6 @@ export function ParsedChunkCard({ focusedPageNumber = null, focusedPageRequestId = 0, highlightRegions = [], - isOriginalPreviewAvailable = false, - onChunkClick, onReferenceClick, sourceOriginalFile = null, }: { @@ -51,8 +35,6 @@ export function ParsedChunkCard({ readonly focusedPageNumber?: number | null; readonly focusedPageRequestId?: number; readonly highlightRegions?: readonly ChatImageHighlightBox[]; - readonly isOriginalPreviewAvailable?: boolean; - readonly onChunkClick?: (chunk: ParsedChunkView) => void; readonly onReferenceClick: (chunkId: string) => void; readonly sourceOriginalFile?: SourceOriginalFileView | null; }): ReactNode { @@ -66,8 +48,6 @@ export function ParsedChunkCard({ focusedPageNumber={focusedPageNumber} focusedPageRequestId={focusedPageRequestId} highlightRegions={highlightRegions} - isOriginalPreviewAvailable={isOriginalPreviewAvailable} - onChunkClick={onChunkClick} /> ); @@ -80,8 +60,6 @@ export function ParsedChunkCard({ isFocused={isFocused} focusedPageRequestId={focusedPageRequestId} highlightRegions={highlightRegions} - isOriginalPreviewAvailable={isOriginalPreviewAvailable} - onChunkClick={onChunkClick} sourceOriginalFile={sourceOriginalFile} /> @@ -90,12 +68,7 @@ export function ParsedChunkCard({ if (chunk.type === "table") { return ( - + ); } @@ -104,8 +77,6 @@ export function ParsedChunkCard({ @@ -132,14 +103,10 @@ function ChunkCardShell({ function ChunkCardFrame({ chunk, isFocused, - isOriginalPreviewAvailable, - onChunkClick, children, }: { readonly chunk: ParsedChunkView; readonly isFocused: boolean; - readonly isOriginalPreviewAvailable: boolean; - readonly onChunkClick?: (chunk: ParsedChunkView) => void; readonly children: ReactNode; }): ReactNode { return ( @@ -149,11 +116,7 @@ function ChunkCardFrame({ parsedChunkCardModel.getFocusCardClasses(isFocused), )} > - + {children} @@ -163,15 +126,10 @@ function ChunkCardFrame({ function ChunkSourcePanel({ chunk, - isOriginalPreviewAvailable, - onChunkClick, }: { readonly chunk: ParsedChunkView; - readonly isOriginalPreviewAvailable: boolean; - readonly onChunkClick?: (chunk: ParsedChunkView) => void; }): ReactNode { const sourceMetadata = parsedChunkCardModel.getSourceMetadata(chunk); - const firstPageNumber = getFirstValidPageNumber(chunk); const lastSegmentIndex = sourceMetadata.sectionSegments.length - 1; return ( @@ -215,105 +173,23 @@ function ChunkSourcePanel({ ) : (
)} - {onChunkClick && - firstPageNumber !== null && - !hasPageCitationAssets(chunk) ? ( - - ) : null} -
- ); -} - -function getFirstValidPageNumber(chunk: ParsedChunkView): number | null { - const pageNums = chunk.pageNums ?? []; - const validPageNums = pageNums.filter( - (pageNum) => Number.isFinite(pageNum) && pageNum > 0, - ); - if (validPageNums.length === 0) return null; - - return Math.min(...validPageNums); -} - -function OpenOriginalButton({ - chunk, - firstPageNumber, - isOriginalPreviewAvailable, - onChunkClick, -}: { - readonly chunk: ParsedChunkView; - readonly firstPageNumber: number; - readonly isOriginalPreviewAvailable: boolean; - readonly onChunkClick: (chunk: ParsedChunkView) => void; -}): ReactNode { - return ( - - ); -} - -function getOpenOriginalButtonLabel( - firstPageNumber: number, - isOriginalPreviewAvailable: boolean, -): string { - if (!isOriginalPreviewAvailable) return "Open original file"; - - return `Open page ${firstPageNumber} in original file`; -} - -function ChunkSummaryPanel({ - chunk, -}: { - readonly chunk: ParsedChunkView; -}): ReactNode { - if (!chunk.summary) return null; - - return ( -
- } label="Summary" /> -

- {chunk.summary} -

); } function ChunkContentPanel({ chunk, - label = "Content", children, }: { readonly chunk: ParsedChunkView; - readonly label?: string; readonly children: ReactNode; }): ReactNode { return (
- } label={label} /> -
{children}
+ {children}
); } @@ -350,59 +226,16 @@ function ChunkEntities({ ); } -function ChunkKeywords({ - chunk, -}: { - readonly chunk: ParsedChunkView; -}): ReactNode { - if (parsedChunkCardModel.getEntityTags(chunk).length > 0) return null; - if (!chunk.keywords || chunk.keywords.length === 0) return null; - - return ( -
- } - label="Keywords" - className="text-emerald-800 dark:text-emerald-200" - iconClassName="text-emerald-600 dark:text-emerald-300" - /> -
- {chunk.keywords.map((keyword) => ( - - {keyword} - - ))} -
-
- ); -} - function SectionLabel({ icon, label, - className, - iconClassName, }: { readonly icon: ReactNode; readonly label: string; - readonly className?: string; - readonly iconClassName?: string; }): ReactNode { return ( -
- {icon} +
+ {icon} {label}
); @@ -411,31 +244,20 @@ function SectionLabel({ function TextChunkCard({ chunk, isFocused, - isOriginalPreviewAvailable, - onChunkClick, onReferenceClick, }: { readonly chunk: ParsedChunkView; readonly isFocused: boolean; - readonly isOriginalPreviewAvailable: boolean; - readonly onChunkClick?: (chunk: ParsedChunkView) => void; readonly onReferenceClick: (chunkId: string) => void; }): ReactNode { return ( - - +
           {renderTextChunkContent(chunk, onReferenceClick)}
         
-
); } @@ -447,8 +269,6 @@ function PageChunkCard({ focusedPageNumber, focusedPageRequestId, highlightRegions, - isOriginalPreviewAvailable, - onChunkClick, }: { readonly chunk: ParsedChunkView; readonly isFocused: boolean; @@ -456,20 +276,13 @@ function PageChunkCard({ readonly focusedPageNumber: number | null; readonly focusedPageRequestId: number; readonly highlightRegions: readonly ChatImageHighlightBox[]; - readonly isOriginalPreviewAvailable: boolean; - readonly onChunkClick?: (chunk: ParsedChunkView) => void; }): ReactNode { const pageAssets = chunk.pageAssets ?? []; return ( - + {pageAssets.length > 0 ? ( -
+ -
+ ) : ( -
+

{chunk.readableContent ?? chunk.content}

-
+ )} -
); } @@ -605,16 +417,12 @@ function ImageChunkCard({ isFocused, focusedPageRequestId, highlightRegions, - isOriginalPreviewAvailable, - onChunkClick, sourceOriginalFile, }: { readonly chunk: ParsedChunkView; readonly isFocused: boolean; readonly focusedPageRequestId: number; readonly highlightRegions: readonly ChatImageHighlightBox[]; - readonly isOriginalPreviewAvailable: boolean; - readonly onChunkClick?: (chunk: ParsedChunkView) => void; readonly sourceOriginalFile: SourceOriginalFileView | null; }): ReactNode { const [loadedAssetUrl, setLoadedAssetUrl] = useState(null); @@ -625,14 +433,9 @@ function ImageChunkCard({ const isImageLoaded = loadedAssetUrl === inlineImageAssetUrl; return ( - + {inlineImageAssetUrl ? ( -
+
-
+ ) : ( - <> - - -
- -
-

- Image chunk -

-

- {chunk.summary - ? chunk.summary - : "Image content is not available in this view."} -

-
+ +
+ +
+

+ Image chunk +

+

+ {chunk.summary + ? chunk.summary + : "Image content is not available in this view."} +

- - +
+
)} - ); } @@ -715,10 +514,6 @@ function isNotebookBlobAssetUrl(assetUrl: string): boolean { } } -function hasPageCitationAssets(chunk: ParsedChunkView): boolean { - return (chunk.pageAssets?.length ?? 0) > 0; -} - function renderTextChunkContent( chunk: ParsedChunkView, onReferenceClick: (chunkId: string) => void, @@ -769,13 +564,9 @@ function ChunkReferenceButton({ function TableChunkCard({ chunk, isFocused, - isOriginalPreviewAvailable, - onChunkClick, }: { readonly chunk: ParsedChunkView; readonly isFocused: boolean; - readonly isOriginalPreviewAvailable: boolean; - readonly onChunkClick?: (chunk: ParsedChunkView) => void; }): ReactNode { const safeHtml = useMemo( () => parsedChunkCardModel.getSanitizedTableHtml(chunk.content), @@ -783,13 +574,7 @@ function TableChunkCard({ ); return ( - - + {safeHtml ? (
- ); }