diff --git a/frontend/src/lib/terms.ts b/frontend/src/lib/terms.ts index 148bc7bb..f0930455 100644 --- a/frontend/src/lib/terms.ts +++ b/frontend/src/lib/terms.ts @@ -3005,7 +3005,8 @@ export const CATALOG_ASSISTANT_TEXT = { `上次起草有 ${formatNumber(n)} 列模型拿不准、留空了${cols.length ? `,例如 ${cols.join('、')}` : ''}。可以先从这几列开始。`, suggestLabel: '可以这样开始', suggestBlank: (n: number) => `补上留空的 ${formatNumber(n)} 列`, - suggestBlankText: (cols: string[]) => `请帮我补上这几列的中文名和含义:${cols.join('、')}。拿不准的地方先问我。`, + suggestBlankText: (cols: string[], total: number) => + `请帮我补上这几列的中文名和含义:${cols.join('、')}${total > cols.length ? ` 等 ${formatNumber(total)} 列` : ''}。拿不准的地方先问我。`, suggestFacts: '核对有效记录条件和去重规则', suggestFactsText: '这张表的有效记录条件和去重规则应该怎么写?拿不准的地方先问我。', suggestPending: '看看哪些推断可以确认', diff --git a/frontend/src/pages/catalog/DraftDialog.tsx b/frontend/src/pages/catalog/DraftDialog.tsx index 06371021..d9970f06 100644 --- a/frontend/src/pages/catalog/DraftDialog.tsx +++ b/frontend/src/pages/catalog/DraftDialog.tsx @@ -533,13 +533,13 @@ function TaskRow({ task: t, jobError }: { task: CatalogDraftTask; jobError: stri const reason = jobError && t.error === jobError ? (t.model_error ?? '') : taskReason(t) const started = t.status !== 'pending' && t.status !== 'skipped' const parts = t.parts_total != null && t.parts_total > 1 && started - // 段被缩小、拆半后实际的块(比段多时才写);已经写进目录的段:起草中写着(停止也不丢),停下来时服务端的原因里 - // 没说的才补一句(没写全的段为什么没写出来,服务端的原因里有),写完了的不写 + // 段被缩小、拆半后实际的块(比段多时才写);已经写进目录的段:起草中、部分完成时写着(停止也不丢,部分完成的 + // 段进度是「都结束了」,看不出写进了几段),停下来时补一句剩下几段没起草。服务端的原因里已经写了的不重复,写完了的不写 const chunks = started ? chunksOf(t) : null const written = writtenOf(t) - const writtenText = written > 0 && t.status === 'running' ? DT.partsWritten(written) - : written > 0 && t.status === 'cancelled' && !reason.includes(DT.partsWritten(written)) - ? DT.partsWrittenStopped(written, Math.max(0, (t.parts_total ?? written) - written)) : '' + const writtenText = written <= 0 || reason.includes(DT.partsWritten(written)) ? '' + : t.status === 'running' || t.status === 'partial' ? DT.partsWritten(written) + : t.status === 'cancelled' ? DT.partsWrittenStopped(written, Math.max(0, (t.parts_total ?? written) - written)) : '' const changes = DT.taskAdded(t.added, t.updated) const blank = blankOf(t) const sample = t.blank_sample?.filter(Boolean) ?? [] diff --git a/frontend/src/pages/catalog/TableAssistant.tsx b/frontend/src/pages/catalog/TableAssistant.tsx index db116014..caf55712 100644 --- a/frontend/src/pages/catalog/TableAssistant.tsx +++ b/frontend/src/pages/catalog/TableAssistant.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' +import { memo, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' import type { KeyboardEvent as ReactKeyboardEvent } from 'react' import { ArrowUp, BookMarked, Database, Info, MessagesSquare, Minus, Plus, Square, Trash2, X } from 'lucide-react' import clsx from 'clsx' @@ -149,11 +149,12 @@ export function TableAssistant({ sourceId, table, tableLabel, detail, profileEna if (!streaming) send(text) } - const drop = (path: string) => setPending((cur) => dropPending(cur, path)) - const restore = (part: Extract, path: string) => { + // 两个回调保持不变:流式的正文每来一段只重画正在回答的那一轮 + const drop = useCallback((path: string) => setPending((cur) => dropPending(cur, path)), []) + const restore = useCallback((part: Extract, path: string) => { const change = part.changes.find((c) => c.path === path) if (change) setPending((cur) => mergeProposal(cur, [change], part.id, part.version)) - } + }, []) const clearAll = async () => { if (!pendingRef.current.length) return if (!(await confirmDialog({ title: T.clearTitle(pendingRef.current.length), confirmLabel: T.clearConfirm, danger: true }))) return @@ -259,7 +260,7 @@ export function TableAssistant({ sourceId, table, tableLabel, detail, profileEna
{lastBlank > 0 && blankCols.length > 0 && ( send(T.suggestBlankText(blankCols.slice(0, 20)))} /> + onClick={() => send(T.suggestBlankText(blankCols.slice(0, 20), blankCols.length))} /> )} send(T.suggestFactsText)} /> {hasProposed && send(T.suggestPendingText)} />} @@ -333,7 +334,7 @@ function SuggestButton({ kind, label, onClick }: { kind: string; label: string; // 一轮回答:正文、只读查询、修改提案按到来的先后排;末尾写这一轮的结局(出错、停止、用量) // --------------------------------------------------------------------------- -function AssistantTurnView({ turn, pending, saved, onDrop, onRestore }: { +const AssistantTurnView = memo(function AssistantTurnView({ turn, pending, saved, onDrop, onRestore }: { turn: AssistantTurn pending: PendingChange[] saved: ReadonlySet @@ -366,14 +367,14 @@ function AssistantTurnView({ turn, pending, saved, onDrop, onRestore }: { {turn.state === 'stopped' && (

{T.stopped}

)} - {turn.state === 'done' && (usage || turn.queries) && ( + {turn.state === 'done' && (!!usage || (turn.queries ?? 0) > 0) && (

{[usage ? T.usage(usage.input_tokens, usage.output_tokens) : '', turn.queries ? T.queries(turn.queries) : ''].filter(Boolean).join(' · ')}

)}
) -} +}) /** 助手跑过的一条只读查询:摘要写行数(失败写原因),展开看 SQL 和前几行 */ function QueryBlock({ part }: { part: Extract }) { diff --git a/scripts/check-catalog.mjs b/scripts/check-catalog.mjs index e4209b40..3c9bf300 100644 --- a/scripts/check-catalog.mjs +++ b/scripts/check-catalog.mjs @@ -1604,8 +1604,9 @@ await section('数据目录 · 起草:逐段写进目录、块进度与停止 check('……可以只重试这 2 张', (await page.locator('[data-draft-retry]').innerText().catch(() => '')).includes('(2 张)')) await page.locator('[data-draft-close]').click() - // 老服务端停下来时原因里没写:界面自己补一句 - const legacy = newJob(state, S1, { tables: ['store_sales'], use_model: true }) + // 老服务端停下来时原因里没写:界面自己补一句。部分完成的表段进度是「都结束了」,看不出写进了几段:也写一句 + const legacy = newJob(state, S1, { tables: ['store_sales', 'members'], use_model: true }) + settle(state, legacy, 'members', { status: 'partial', parts_total: 2, parts_done: 2, parts_written: 1, model_error: CALL_ERR }) settle(state, legacy, 'store_sales', { status: 'running', parts_done: 1, parts_written: 1 }) stopJob(legacy) legacy.tasks[0].model_error = null @@ -1615,6 +1616,8 @@ await section('数据目录 · 起草:逐段写进目录、块进度与停止 await draftBox(page).locator('[data-draft-last-open]').click() check('服务端的原因里没写已写进几段:界面补「已写进目录 1 段,剩下 2 段没起草」', await until(async () => (await text('store_sales', '[data-draft-task-written]')) === '已写进目录 1 段,剩下 2 段没起草'), await text('store_sales', '[data-draft-task-written]')) + check('……部分完成的表写「已写进目录 1 段」,原因照旧', (await text('members', '[data-draft-task-written]')) === '已写进目录 1 段' + && (await text('members', '[data-draft-task-reason]')).includes(CALL_ERR), await text('members', '[data-draft-task-written]')) await page.locator('[data-draft-close]').click() state.jobs.pop()