Chunk nav fixed

This commit is contained in:
Pavel committed 2026-09-28 19:02:50 +04:00
1 parent f4331cd3a3
commit b12f5b6c81
9 files changed
+87 -25

No files matched your search

+11 -2
View File
@@ -507,6 +507,8 @@ describe('Chunks', () => {
await act(async () => buttonByText('modals.chunk.save')!.click());
expect(drawer()).toBeNull();
expect(container.querySelector('h2')?.textContent).toBe('Rewritten');
expect(container.textContent).toContain('chunkPositionUnplaced');
expect(buttonByLabel('settings.sources.nextChunk')!.disabled).toBe(true);
});
it('deleting the only chunk on the last page lands on the page before', async () => {
@@ -789,7 +791,7 @@ describe('Chunks', () => {
expect(container.querySelector('h1')?.textContent).toBe('Second');
});
it('an edited chunk the store moved off the probed positions keeps what is shown', async () => {
it('an edited chunk the store moved off the probed positions keeps its text but not its place', async () => {
serveStore(1);
await render({ embedded: true });
const tiles = container.querySelectorAll<HTMLButtonElement>(
@@ -799,7 +801,14 @@ describe('Chunks', () => {
service.getDocumentChunks.mockClear();
await editOpenChunk('# First edited');
expect(container.querySelector('h1')?.textContent).toBe('First edited');
expect(position()).toEqual([1, 3]);
// Its position is unknown: no number is claimed and paging is off, so
// previous / next can't step from a place the chunk no longer holds.
expect(container.textContent).not.toMatch(/chunkPosition \{/);
expect(container.textContent).toContain('chunkPositionUnplaced');
expect(buttonByLabel('settings.sources.previousChunk')!.disabled).toBe(
true,
);
expect(buttonByLabel('settings.sources.nextChunk')!.disabled).toBe(true);
// Only the open and last positions are probed: the whole filtered list
// (one page of `total`) is never fetched.
const sizes = service.getDocumentChunks.mock.calls.map(
+55 -23
View File
@@ -111,6 +111,9 @@ const Chunks: React.FC<ChunksProps> = ({
const [openChunk, setOpenChunk] = useState<ChunkType | null>(null);
// The open chunk's position in the whole filtered list (1-based).
const [openPosition, setOpenPosition] = useState(0);
// Set when a save leaves the open chunk off every probed position: its
// place in the list is unknown, so position labels and paging are off.
const [positionLost, setPositionLost] = useState(false);
// A previous / next fetch in flight.
const [stepping, setStepping] = useState(false);
const stepRef = useRef(0);
@@ -197,6 +200,7 @@ const Chunks: React.FC<ChunksProps> = ({
if (chunk) {
setOpenChunk(chunk);
setOpenPosition(position);
setPositionLost(false);
}
} catch (error) {
console.error(error);
@@ -214,7 +218,10 @@ const Chunks: React.FC<ChunksProps> = ({
* position (the chunk stayed), then the last one (a store that re-adds the
* copy at the end). Each probe matches by id, so no store ordering is
* assumed. When neither holds it (moved elsewhere, or a search it no longer
* matches), what is shown stays: the whole filtered list is never fetched.
* matches), the saved text stays but its position is unknown, so the
* position labels drop the number and previous / next are off until the
* reader closes; the whole filtered list is never fetched. A failed or
* superseded probe leaves what is shown.
*
* @param chunk The chunk just saved, with its new id.
*/
@@ -237,6 +244,7 @@ const Chunks: React.FC<ChunksProps> = ({
setTotalChunks(total);
setOpenChunk(stored);
setOpenPosition(at);
setPositionLost(false);
};
try {
const here = await fetchPage(openPosition, 1);
@@ -248,12 +256,17 @@ const Chunks: React.FC<ChunksProps> = ({
show(stored, openPosition, total);
return;
}
if (total < 1) return;
if (total === openPosition) return;
const last = await fetchPage(total, 1);
if (!last) return;
const lastChunk: ChunkType | undefined = last.chunks?.[0];
if (lastChunk?.doc_id === chunk.doc_id) show(lastChunk, total, total);
if (total >= 1 && total !== openPosition) {
const last = await fetchPage(total, 1);
if (!last) return;
const lastChunk: ChunkType | undefined = last.chunks?.[0];
if (lastChunk?.doc_id === chunk.doc_id) {
show(lastChunk, total, total);
return;
}
}
setTotalChunks(total);
setPositionLost(true);
} catch (error) {
console.error(error);
}
@@ -296,6 +309,7 @@ const Chunks: React.FC<ChunksProps> = ({
const showChunk = (chunk: ChunkType, position: number) => {
setOpenChunk(chunk);
setOpenPosition(position);
setPositionLost(false);
};
/**
@@ -459,8 +473,10 @@ const Chunks: React.FC<ChunksProps> = ({
setPage(1);
}, [path]);
const canGoPrevious = !!openChunk && !stepping && openPosition > 1;
const canGoNext = !!openChunk && !stepping && openPosition < totalChunks;
const canGoPrevious =
!!openChunk && !stepping && !positionLost && openPosition > 1;
const canGoNext =
!!openChunk && !stepping && !positionLost && openPosition < totalChunks;
// ← / → walk the chunks while nothing else owns the keys.
const goToChunkRef = useRef(goToChunk);
@@ -490,7 +506,13 @@ const Chunks: React.FC<ChunksProps> = ({
onSelect: openChunk ? () => closeChunk() : undefined,
},
...(openChunk
? [{ label: t('settings.sources.chunkCrumb', { n: openPosition }) }]
? [
{
label: positionLost
? t('settings.sources.chunkCrumbUnplaced')
: t('settings.sources.chunkCrumb', { n: openPosition }),
},
]
: []),
]}
byline={
@@ -609,11 +631,15 @@ const Chunks: React.FC<ChunksProps> = ({
<ReaderPanel
meta={
<span>
{t('settings.sources.chunkPosition', {
n: openPosition,
total: totalChunks,
tokens: formatChunkTokens(chunk.metadata),
})}
{positionLost
? t('settings.sources.chunkPositionUnplaced', {
tokens: formatChunkTokens(chunk.metadata),
})
: t('settings.sources.chunkPosition', {
n: openPosition,
total: totalChunks,
tokens: formatChunkTokens(chunk.metadata),
})}
</span>
}
actions={
@@ -723,14 +749,20 @@ const Chunks: React.FC<ChunksProps> = ({
: t('settings.sources.addChunk')
}
description={
editing
? t('settings.sources.editChunkDescription', {
file: fileLabel,
n: openPosition,
tokens: formatChunkTokens(openChunk?.metadata),
interpolation: { escapeValue: false },
})
: fileLabel || undefined
!editing
? fileLabel || undefined
: positionLost
? t('settings.sources.editChunkDescriptionUnplaced', {
file: fileLabel,
tokens: formatChunkTokens(openChunk?.metadata),
interpolation: { escapeValue: false },
})
: t('settings.sources.editChunkDescription', {
file: fileLabel,
n: openPosition,
tokens: formatChunkTokens(openChunk?.metadata),
interpolation: { escapeValue: false },
})
}
value={draft}
onChange={setDraft}
+3
View File
@@ -411,6 +411,9 @@
},
"chunkPosition": "Chunk {{n}} von {{total}} · {{tokens}} Tokens",
"chunkCrumb": "Chunk {{n}}",
"chunkCrumbUnplaced": "Bearbeiteter Chunk",
"chunkPositionUnplaced": "Bearbeiteter Chunk · {{tokens}} Tokens",
"editChunkDescriptionUnplaced": "{{file}} · bearbeiteter Chunk · {{tokens}} Tokens",
"chunkTileMeta": "#{{n}} · {{tokens}} Tokens",
"copyText": "Text kopieren",
"chunkTitleHint": "Der Name, unter dem Antworten diesen Chunk zitieren.",
+3
View File
@@ -416,6 +416,9 @@
},
"chunkPosition": "Chunk {{n}} of {{total}} · {{tokens}} tokens",
"chunkCrumb": "Chunk {{n}}",
"chunkCrumbUnplaced": "Edited chunk",
"chunkPositionUnplaced": "Edited chunk · {{tokens}} tokens",
"editChunkDescriptionUnplaced": "{{file}} · edited chunk · {{tokens}} tokens",
"chunkTileMeta": "#{{n}} · {{tokens}} tokens",
"copyText": "Copy text",
"chunkTitleHint": "The name answers cite this chunk by.",
+3
View File
@@ -411,6 +411,9 @@
},
"chunkPosition": "Fragmento {{n}} de {{total}} · {{tokens}} tokens",
"chunkCrumb": "Fragmento {{n}}",
"chunkCrumbUnplaced": "Fragmento editado",
"chunkPositionUnplaced": "Fragmento editado · {{tokens}} tokens",
"editChunkDescriptionUnplaced": "{{file}} · fragmento editado · {{tokens}} tokens",
"chunkTileMeta": "#{{n}} · {{tokens}} tokens",
"copyText": "Copiar texto",
"chunkTitleHint": "El nombre con el que las respuestas citan este fragmento.",
+3
View File
@@ -404,6 +404,9 @@
},
"chunkPosition": "チャンク {{n}} / {{total}} · {{tokens}} トークン",
"chunkCrumb": "チャンク {{n}}",
"chunkCrumbUnplaced": "編集済みチャンク",
"chunkPositionUnplaced": "編集済みチャンク · {{tokens}} トークン",
"editChunkDescriptionUnplaced": "{{file}} · 編集済みチャンク · {{tokens}} トークン",
"chunkTileMeta": "#{{n}} · {{tokens}} トークン",
"copyText": "テキストをコピー",
"chunkTitleHint": "回答がこのチャンクを引用するときの名前です。",
+3
View File
@@ -435,6 +435,9 @@
},
"chunkPosition": "Фрагмент {{n}} из {{total}} · токенов: {{tokens}}",
"chunkCrumb": "Фрагмент {{n}}",
"chunkCrumbUnplaced": "Изменённый фрагмент",
"chunkPositionUnplaced": "Изменённый фрагмент · токенов: {{tokens}}",
"editChunkDescriptionUnplaced": "{{file}} · изменённый фрагмент · токенов: {{tokens}}",
"chunkTileMeta": "#{{n}} · токенов: {{tokens}}",
"copyText": "Копировать текст",
"chunkTitleHint": "Название, под которым ответы ссылаются на этот фрагмент.",
+3
View File
@@ -404,6 +404,9 @@
},
"chunkPosition": "第 {{n}} 個文本塊,共 {{total}} 個 · {{tokens}} Token",
"chunkCrumb": "第 {{n}} 個文本塊",
"chunkCrumbUnplaced": "已編輯的文本塊",
"chunkPositionUnplaced": "已編輯的文本塊 · {{tokens}} Token",
"editChunkDescriptionUnplaced": "{{file}} · 已編輯的文本塊 · {{tokens}} Token",
"chunkTileMeta": "#{{n}} · {{tokens}} Token",
"copyText": "複製文字",
"chunkTitleHint": "回答引用此文本塊時使用的名稱。",
+3
View File
@@ -404,6 +404,9 @@
},
"chunkPosition": "第 {{n}} 个文本块,共 {{total}} 个 · {{tokens}} 个令牌",
"chunkCrumb": "第 {{n}} 个文本块",
"chunkCrumbUnplaced": "已编辑的文本块",
"chunkPositionUnplaced": "已编辑的文本块 · {{tokens}} 个令牌",
"editChunkDescriptionUnplaced": "{{file}} · 已编辑的文本块 · {{tokens}} 个令牌",
"chunkTileMeta": "#{{n}} · {{tokens}} 个令牌",
"copyText": "复制文本",
"chunkTitleHint": "回答引用此文本块时使用的名称。",