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 (
-
-
-
- {renderChunkIcon(chunk.type)}
-
-
-
- {sourceMetadata.typeLabel}
-
- {sourceMetadata.pageLabel ? (
-
- {sourceMetadata.pageLabel}
-
- ) : null}
- {sourceMetadata.sectionLabel ? (
-
+ {sourceMetadata.leadLabel}
+
+ {sourceMetadata.sectionSegments.length > 0 ? (
+
- ) : null}
-
-
- {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 ? (
-
);
}