Skip to content

Commit 494f952

Browse files
committed
fix: restore citation focus with default tree view
1 parent 4d43f4d commit 494f952

3 files changed

Lines changed: 141 additions & 48 deletions

File tree

src/components/chunks-panel.tsx

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ export type ChunksPanelProps = {
6262

6363
type ChunkDisplayMode = "list" | "tree";
6464

65+
type ChunkDisplayModeState = {
66+
readonly handledFocusedChunkRequestId: number;
67+
readonly mode: ChunkDisplayMode;
68+
};
69+
6570
export function ChunksPanel({
6671
chunks = [],
6772
selectedSource = null,
@@ -86,8 +91,12 @@ export function ChunksPanel({
8691
const [mountedOriginalPreviewKey, setMountedOriginalPreviewKey] = useState<
8792
string | null
8893
>(null);
89-
const [chunkDisplayMode, setChunkDisplayMode] =
90-
useState<ChunkDisplayMode>("tree");
94+
const [chunkDisplayModeState, setChunkDisplayModeState] =
95+
useState<ChunkDisplayModeState>(() => ({
96+
handledFocusedChunkRequestId:
97+
focusedChunkId === null ? focusedChunkRequestId : -1,
98+
mode: "tree",
99+
}));
91100
const [sectionTreeZoomPercent, setSectionTreeZoomPercent] =
92101
useState<number>(sectionTreeDefaultZoomPercent);
93102
const {
@@ -141,19 +150,28 @@ export function ChunksPanel({
141150
selectOriginalView();
142151
}, [rememberOriginalPreview, selectOriginalView]);
143152
const handleListModeSelected = useCallback((): void => {
144-
setChunkDisplayMode("list");
145-
}, []);
153+
setChunkDisplayModeState({
154+
handledFocusedChunkRequestId: focusedChunkRequestId,
155+
mode: "list",
156+
});
157+
}, [focusedChunkRequestId]);
146158
const handleTreeModeSelected = useCallback((): void => {
147-
setChunkDisplayMode("tree");
148-
}, []);
159+
setChunkDisplayModeState({
160+
handledFocusedChunkRequestId: focusedChunkRequestId,
161+
mode: "tree",
162+
});
163+
}, [focusedChunkRequestId]);
149164
const handleTreeChunkFocus = useCallback(
150165
(chunkId: string | null): void => {
151166
requestChunkFocus(chunkId);
152167
if (chunkId !== null) {
153-
setChunkDisplayMode("list");
168+
setChunkDisplayModeState({
169+
handledFocusedChunkRequestId: focusedChunkRequestId,
170+
mode: "list",
171+
});
154172
}
155173
},
156-
[requestChunkFocus],
174+
[focusedChunkRequestId, requestChunkFocus],
157175
);
158176
const canZoomSectionTreeOut: boolean =
159177
sectionTreeZoomPercent > sectionTreeMinimumZoomPercent;
@@ -198,6 +216,12 @@ export function ChunksPanel({
198216
},
199217
[],
200218
);
219+
const chunkDisplayMode: ChunkDisplayMode =
220+
focusedChunkId !== null &&
221+
chunkDisplayModeState.handledFocusedChunkRequestId !==
222+
focusedChunkRequestId
223+
? "list"
224+
: chunkDisplayModeState.mode;
201225
const headerTitle = focusedChunkId ? "Referenced Chunks" : "Parsed Chunks";
202226
const shouldMountOriginalPreview =
203227
visibleView === "original" ||

src/components/workspace-citation-focus.test.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,39 @@ describe("useWorkspaceCitationFocus", () => {
6969
expect(result.current.pendingCitationId).toBeNull();
7070
});
7171

72-
it("clears prefetched chunks and focus when the selected source changes", () => {
72+
it("clears prefetched chunks and focus when selecting a different source", () => {
73+
const selectSource = vi.fn();
74+
const otherSource: SourceView = {
75+
id: "source_2",
76+
title: "Other.pdf",
77+
mimeType: "application/pdf",
78+
status: "ready",
79+
documentId: "document_2",
80+
};
81+
const { result } = renderHook(() =>
82+
useWorkspaceCitationFocus({
83+
fetchChunks: vi.fn(async () => []),
84+
initialPrefetchedChunksBySourceId: { source_1: [prefetchedChunk] },
85+
onSelectSource: selectSource,
86+
selectedSourceId: "source_2",
87+
sources: [readySource, otherSource],
88+
}),
89+
{ wrapper: createSWRWrapper },
90+
);
91+
92+
act(() => {
93+
result.current.handleSourceSelected("source_1");
94+
});
95+
96+
expect(selectSource).toHaveBeenCalledWith("source_1");
97+
expect(result.current.prefetchedChunksBySourceId).toEqual({});
98+
expect(result.current.focusedChunk).toEqual({
99+
chunkId: null,
100+
requestId: 1,
101+
});
102+
});
103+
104+
it("keeps prefetched chunks when reselecting the selected source", () => {
73105
const selectSource = vi.fn();
74106
const { result } = renderHook(() =>
75107
useWorkspaceCitationFocus({
@@ -87,7 +119,9 @@ describe("useWorkspaceCitationFocus", () => {
87119
});
88120

89121
expect(selectSource).toHaveBeenCalledWith("source_1");
90-
expect(result.current.prefetchedChunksBySourceId).toEqual({});
122+
expect(result.current.prefetchedChunksBySourceId).toEqual({
123+
source_1: [prefetchedChunk],
124+
});
91125
expect(result.current.focusedChunk).toEqual({
92126
chunkId: null,
93127
requestId: 1,

src/components/workspace-citation-focus.ts

Lines changed: 73 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client"
22

3-
import { useCallback, useState } from "react"
3+
import { useCallback, useRef, useState } from "react"
44

55
import { workspaceCitationState } from "@/components/workspace-citation-state"
66
import { useWorkspaceSelectedChunks } from "@/components/workspace-selected-chunks"
@@ -14,6 +14,9 @@ type FocusedChunkState = {
1414
}
1515

1616
type PrefetchedChunksBySourceId = Readonly<Record<string, ParsedChunkView[]>>
17+
type PrefetchedChunksUpdater = (
18+
current: PrefetchedChunksBySourceId,
19+
) => PrefetchedChunksBySourceId
1720

1821
type WorkspaceCitationFocusInput = {
1922
readonly fetchChunks: (sourceId: string) => Promise<ParsedChunkView[]>
@@ -60,8 +63,15 @@ export function useWorkspaceCitationFocus({
6063
const [fullChunkLoadingSourceId, setFullChunkLoadingSourceId] = useState<
6164
string | null
6265
>(null)
66+
const fullChunkRequestsBySourceIdRef = useRef<
67+
Map<string, Promise<ParsedChunkView[]>>
68+
>(new Map())
69+
const fullChunkRequestedSourceIdsRef = useRef<Set<string>>(new Set())
6370
const [prefetchedChunksBySourceId, setPrefetchedChunksBySourceId] =
6471
useState<PrefetchedChunksBySourceId>(initialPrefetchedChunksBySourceId)
72+
const prefetchedChunksBySourceIdRef = useRef<PrefetchedChunksBySourceId>(
73+
initialPrefetchedChunksBySourceId,
74+
)
6575
const {
6676
hasMoreSelectedChunks,
6777
handleLoadMoreChunks,
@@ -85,48 +95,79 @@ export function useWorkspaceCitationFocus({
8595
[],
8696
)
8797

98+
const updatePrefetchedChunksBySourceId = useCallback(
99+
(updater: PrefetchedChunksUpdater): void => {
100+
const next = updater(prefetchedChunksBySourceIdRef.current)
101+
prefetchedChunksBySourceIdRef.current = next
102+
setPrefetchedChunksBySourceId(next)
103+
},
104+
[],
105+
)
106+
88107
const handleSourceSelected = useCallback(
89108
(sourceId: string | null): void => {
90109
onSelectSource(sourceId)
91-
if (sourceId) {
92-
setPrefetchedChunksBySourceId((current) =>
110+
if (sourceId && sourceId !== selectedSourceId) {
111+
fullChunkRequestedSourceIdsRef.current.delete(sourceId)
112+
updatePrefetchedChunksBySourceId((current) =>
93113
workspaceCitationState.removePrefetchedChunks(current, sourceId),
94114
)
95115
}
96116
requestChunkFocus(null)
97117
},
98-
[onSelectSource, requestChunkFocus],
118+
[
119+
onSelectSource,
120+
requestChunkFocus,
121+
selectedSourceId,
122+
updatePrefetchedChunksBySourceId,
123+
],
124+
)
125+
126+
const loadAllChunksForSource = useCallback(
127+
(sourceId: string): Promise<ParsedChunkView[]> => {
128+
const existingRequest =
129+
fullChunkRequestsBySourceIdRef.current.get(sourceId)
130+
if (existingRequest) return existingRequest
131+
132+
setFullChunkLoadingSourceId(sourceId)
133+
const request = fetchChunks(sourceId)
134+
.then((chunks) => {
135+
updatePrefetchedChunksBySourceId((current) =>
136+
workspaceCitationState.upsertPrefetchedChunks(
137+
current,
138+
sourceId,
139+
chunks,
140+
),
141+
)
142+
return chunks
143+
})
144+
.finally(() => {
145+
fullChunkRequestsBySourceIdRef.current.delete(sourceId)
146+
setFullChunkLoadingSourceId((current) =>
147+
current === sourceId ? null : current,
148+
)
149+
})
150+
151+
fullChunkRequestsBySourceIdRef.current.set(sourceId, request)
152+
return request
153+
},
154+
[fetchChunks, updatePrefetchedChunksBySourceId],
99155
)
100156

101157
const handleLoadAllChunks = useCallback((): void => {
102158
if (
103159
!selectedSourceId ||
104-
prefetchedChunksBySourceId[selectedSourceId] ||
105-
fullChunkLoadingSourceId === selectedSourceId
160+
prefetchedChunksBySourceIdRef.current[selectedSourceId] ||
161+
fullChunkRequestedSourceIdsRef.current.has(selectedSourceId) ||
162+
fullChunkRequestsBySourceIdRef.current.has(selectedSourceId)
106163
) {
107164
return
108165
}
109166

110-
setFullChunkLoadingSourceId(selectedSourceId)
111-
void fetchChunks(selectedSourceId)
112-
.then((chunks) => {
113-
setPrefetchedChunksBySourceId((current) =>
114-
workspaceCitationState.upsertPrefetchedChunks(
115-
current,
116-
selectedSourceId,
117-
chunks,
118-
),
119-
)
120-
})
121-
.finally(() => {
122-
setFullChunkLoadingSourceId((current) =>
123-
current === selectedSourceId ? null : current,
124-
)
125-
})
167+
fullChunkRequestedSourceIdsRef.current.add(selectedSourceId)
168+
void loadAllChunksForSource(selectedSourceId)
126169
}, [
127-
fetchChunks,
128-
fullChunkLoadingSourceId,
129-
prefetchedChunksBySourceId,
170+
loadAllChunksForSource,
130171
selectedSourceId,
131172
])
132173

@@ -157,15 +198,16 @@ export function useWorkspaceCitationFocus({
157198
}
158199

159200
if (!workspaceCitationState.hasExactCitationTargetHint(citation)) {
160-
setPrefetchedChunksBySourceId((current) =>
201+
fullChunkRequestedSourceIdsRef.current.delete(source.id)
202+
updatePrefetchedChunksBySourceId((current) =>
161203
workspaceCitationState.removePrefetchedChunks(current, source.id),
162204
)
163205
if (selectedSourceId !== source.id) onSelectSource(source.id)
164206
requestChunkFocus(null)
165207
return
166208
}
167209

168-
const cachedChunks = prefetchedChunksBySourceId[source.id]
210+
const cachedChunks = prefetchedChunksBySourceIdRef.current[source.id]
169211
if (cachedChunks) {
170212
const cachedFocusId =
171213
workspaceCitationState.getLoadedCitationChunkId({
@@ -175,7 +217,7 @@ export function useWorkspaceCitationFocus({
175217
selectedChunks: cachedChunks,
176218
hasMoreSelectedChunks: false,
177219
})
178-
setPrefetchedChunksBySourceId((current) =>
220+
updatePrefetchedChunksBySourceId((current) =>
179221
workspaceCitationState.upsertPrefetchedChunks(
180222
current,
181223
source.id,
@@ -188,14 +230,7 @@ export function useWorkspaceCitationFocus({
188230
}
189231

190232
requestChunkFocus(null)
191-
const chunks = await fetchChunks(source.id)
192-
setPrefetchedChunksBySourceId((current) =>
193-
workspaceCitationState.upsertPrefetchedChunks(
194-
current,
195-
source.id,
196-
chunks,
197-
),
198-
)
233+
const chunks = await loadAllChunksForSource(source.id)
199234
const prefetchedChunkId =
200235
workspaceCitationState.getLoadedCitationChunkId({
201236
citation,
@@ -213,14 +248,14 @@ export function useWorkspaceCitationFocus({
213248
}
214249
},
215250
[
216-
fetchChunks,
217251
hasMoreSelectedChunks,
252+
loadAllChunksForSource,
218253
onSelectSource,
219-
prefetchedChunksBySourceId,
220254
requestChunkFocus,
221255
selectedChunks,
222256
selectedSourceId,
223257
sources,
258+
updatePrefetchedChunksBySourceId,
224259
],
225260
)
226261

0 commit comments

Comments
 (0)