From 41daf2e4f2d5fd3d11bbff3589f394b8ffa8806e Mon Sep 17 00:00:00 2001 From: suguanYang Date: Sun, 14 Jun 2026 17:00:52 +0000 Subject: [PATCH] Open citation targets in chunk list view --- src/components/chunks-panel.test.ts | 41 +++++++++++++++++++ src/components/chunks-panel.tsx | 21 +++++++--- .../workspace-citation-focus.test.ts | 1 + src/components/workspace-citation-focus.ts | 5 +++ src/components/workspace-shell-layout.tsx | 3 ++ src/components/workspace-shell.tsx | 1 + 6 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/components/chunks-panel.test.ts b/src/components/chunks-panel.test.ts index c3e87aa..e97763c 100644 --- a/src/components/chunks-panel.test.ts +++ b/src/components/chunks-panel.test.ts @@ -506,6 +506,47 @@ describe("ChunksPanel", () => { expect(screen.getByTestId("chunk-card-shell-robotics_chunk")).toBeTruthy(); }); + it("switches source-only citation navigation from tree to list view", async () => { + mockVisibleVirtualViewport(); + const chunks = [ + { + chunkId: "image_details_chunk", + type: "image", + content: "", + sectionPath: "images/image-81-__details_.jpg", + sourceTitle: "product-manual.pdf", + }, + ]; + const { rerender } = render( + React.createElement(C, { + chunks, + selectedSource: "product-manual.pdf", + citationListViewRequestId: 0, + }), + ); + + expect( + screen.getByRole("tree", { name: "Parsed chunk sections" }), + ).toBeTruthy(); + + rerender( + React.createElement(C, { + chunks, + selectedSource: "product-manual.pdf", + citationListViewRequestId: 1, + }), + ); + + await waitFor(() => { + expect( + screen.queryByRole("tree", { name: "Parsed chunk sections" }), + ).toBeNull(); + }); + expect( + screen.getByTestId("chunk-card-shell-image_details_chunk"), + ).toBeTruthy(); + }); + it("shows a large upload target when no document is selected", async () => { const user = userEvent.setup(); diff --git a/src/components/chunks-panel.tsx b/src/components/chunks-panel.tsx index 5be4120..7d5b853 100644 --- a/src/components/chunks-panel.tsx +++ b/src/components/chunks-panel.tsx @@ -51,6 +51,7 @@ export type ChunksPanelProps = { selectedSourceFile?: SourceOriginalFileView | null; focusedChunkId?: string | null; focusedChunkRequestId?: number; + citationListViewRequestId?: number; isLoading?: boolean; isLoadingMore?: boolean; isLoadingAllChunks?: boolean; @@ -64,6 +65,7 @@ export type ChunksPanelProps = { type ChunkDisplayMode = "list" | "tree"; type ChunkDisplayModeState = { + readonly handledCitationListViewRequestId: number; readonly handledFocusedChunkRequestId: number; readonly mode: ChunkDisplayMode; }; @@ -74,6 +76,7 @@ export function ChunksPanel({ selectedSourceFile = null, focusedChunkId = null, focusedChunkRequestId = 0, + citationListViewRequestId = 0, isLoading = false, isLoadingMore = false, isLoadingAllChunks = false, @@ -94,6 +97,7 @@ export function ChunksPanel({ >(null); const [chunkDisplayModeState, setChunkDisplayModeState] = useState(() => ({ + handledCitationListViewRequestId: citationListViewRequestId, handledFocusedChunkRequestId: focusedChunkId === null ? focusedChunkRequestId : -1, mode: "tree", @@ -152,27 +156,30 @@ export function ChunksPanel({ }, [rememberOriginalPreview, selectOriginalView]); const handleListModeSelected = useCallback((): void => { setChunkDisplayModeState({ + handledCitationListViewRequestId: citationListViewRequestId, handledFocusedChunkRequestId: focusedChunkRequestId, mode: "list", }); - }, [focusedChunkRequestId]); + }, [citationListViewRequestId, focusedChunkRequestId]); const handleTreeModeSelected = useCallback((): void => { setChunkDisplayModeState({ + handledCitationListViewRequestId: citationListViewRequestId, handledFocusedChunkRequestId: focusedChunkRequestId, mode: "tree", }); - }, [focusedChunkRequestId]); + }, [citationListViewRequestId, focusedChunkRequestId]); const handleTreeChunkFocus = useCallback( (chunkId: string | null): void => { requestChunkFocus(chunkId); if (chunkId !== null) { setChunkDisplayModeState({ + handledCitationListViewRequestId: citationListViewRequestId, handledFocusedChunkRequestId: focusedChunkRequestId, mode: "list", }); } }, - [focusedChunkRequestId, requestChunkFocus], + [citationListViewRequestId, focusedChunkRequestId, requestChunkFocus], ); const canZoomSectionTreeOut: boolean = sectionTreeZoomPercent > sectionTreeMinimumZoomPercent; @@ -218,9 +225,11 @@ export function ChunksPanel({ [], ); const chunkDisplayMode: ChunkDisplayMode = - focusedChunkId !== null && - chunkDisplayModeState.handledFocusedChunkRequestId !== - focusedChunkRequestId + (focusedChunkId !== null && + chunkDisplayModeState.handledFocusedChunkRequestId !== + focusedChunkRequestId) || + chunkDisplayModeState.handledCitationListViewRequestId !== + citationListViewRequestId ? "list" : chunkDisplayModeState.mode; const headerTitle = focusedChunkId ? "Referenced Chunks" : "Parsed Chunks"; diff --git a/src/components/workspace-citation-focus.test.ts b/src/components/workspace-citation-focus.test.ts index ac531d7..baa5a1a 100644 --- a/src/components/workspace-citation-focus.test.ts +++ b/src/components/workspace-citation-focus.test.ts @@ -164,6 +164,7 @@ describe("useWorkspaceCitationFocus", () => { expect(fetchChunks).not.toHaveBeenCalled(); expect(selectSource).toHaveBeenLastCalledWith("source_1"); expect(result.current.prefetchedChunksBySourceId).toEqual({}); + expect(result.current.citationListViewRequestId).toBe(1); expect(result.current.focusedChunk.chunkId).toBeNull(); expect(result.current.pendingCitationId).toBeNull(); }); diff --git a/src/components/workspace-citation-focus.ts b/src/components/workspace-citation-focus.ts index 1e7bb20..b0d0f66 100644 --- a/src/components/workspace-citation-focus.ts +++ b/src/components/workspace-citation-focus.ts @@ -27,6 +27,7 @@ type WorkspaceCitationFocusInput = { } type WorkspaceCitationFocus = { + readonly citationListViewRequestId: number readonly focusedChunk: FocusedChunkState readonly handleCitationClick: ( citation: ChatCitationView, @@ -60,6 +61,8 @@ export function useWorkspaceCitationFocus({ const [pendingCitationId, setPendingCitationId] = useState( null, ) + const [citationListViewRequestId, setCitationListViewRequestId] = + useState(0) const [fullChunkLoadingSourceId, setFullChunkLoadingSourceId] = useState< string | null >(null) @@ -184,6 +187,7 @@ export function useWorkspaceCitationFocus({ citation, ) if (!source) return + setCitationListViewRequestId((current) => current + 1) const loadedChunkId = workspaceCitationState.getLoadedCitationChunkId({ citation, @@ -260,6 +264,7 @@ export function useWorkspaceCitationFocus({ ) return { + citationListViewRequestId, focusedChunk, handleCitationClick, handleLoadAllChunks, diff --git a/src/components/workspace-shell-layout.tsx b/src/components/workspace-shell-layout.tsx index 3f1eecb..1afc5fb 100644 --- a/src/components/workspace-shell-layout.tsx +++ b/src/components/workspace-shell-layout.tsx @@ -56,6 +56,7 @@ export type WorkspaceShellLayoutProps = { readonly archivingThreadIds: readonly string[] readonly chat: WorkspaceChatState readonly chatThreads: readonly ChatThreadView[] + readonly citationListViewRequestId: number readonly dashboardUrl?: string readonly desktopPanelWidths: Readonly readonly focusedChunk: FocusedChunkState @@ -216,6 +217,7 @@ export function WorkspaceShellLayout( chunks={[...props.selectedChunks]} selectedSource={props.selectedSourceTitle} selectedSourceFile={props.selectedSourceFile} + citationListViewRequestId={props.citationListViewRequestId} focusedChunkId={props.focusedChunk.chunkId} focusedChunkRequestId={props.focusedChunk.requestId} isLoading={props.isSelectedChunksLoading} @@ -326,6 +328,7 @@ export function WorkspaceShellLayout( chunks={[...props.selectedChunks]} selectedSource={props.selectedSourceTitle} selectedSourceFile={props.selectedSourceFile} + citationListViewRequestId={props.citationListViewRequestId} focusedChunkId={props.focusedChunk.chunkId} focusedChunkRequestId={props.focusedChunk.requestId} isLoading={props.isSelectedChunksLoading} diff --git a/src/components/workspace-shell.tsx b/src/components/workspace-shell.tsx index 5bab04a..9e82794 100644 --- a/src/components/workspace-shell.tsx +++ b/src/components/workspace-shell.tsx @@ -128,6 +128,7 @@ function WorkspaceShellContent({ chatThreads={chatWorkflow.chatThreads} desktopPanelWidths={desktopPanelWidths} dashboardUrl={dashboardUrl} + citationListViewRequestId={citationFocus.citationListViewRequestId} focusedChunk={citationFocus.focusedChunk} hasMessages={hasMessages} hasMoreSelectedChunks={citationFocus.hasMoreSelectedChunks}