From b91a652efe030b9e3b4465aa5ee5a518603cd224 Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Tue, 13 Oct 2026 20:30:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=9B=AE=E5=BD=95=E8=B5=B7?= =?UTF-8?q?=E8=8D=89=EF=BC=88=E5=89=8D=E7=AB=AF=EF=BC=89=EF=BC=9A=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E6=8B=BF=E4=B8=8D=E5=87=86=E3=80=81=E7=95=99=E7=A9=BA?= =?UTF-8?q?=E7=9A=84=E5=88=97=EF=BC=88=E4=B8=AD=E6=96=87=E5=90=8D=E5=92=8C?= =?UTF-8?q?=E5=90=AB=E4=B9=89=E9=83=BD=E6=B2=A1=E7=BB=99=EF=BC=89=E5=8D=95?= =?UTF-8?q?=E7=8B=AC=E5=86=99=E5=87=BA=E6=9D=A5=EF=BC=8C=E4=B8=8D=E7=AE=97?= =?UTF-8?q?=E6=B2=A1=E5=86=99=E5=85=A8=E3=80=81=E8=A1=A8=E7=85=A7=E6=A0=B7?= =?UTF-8?q?=E6=98=AF=E5=B7=B2=E5=AE=8C=E6=88=90=E3=80=82=E8=B5=B7=E8=8D=89?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=A1=86=E9=80=90=E8=A1=A8=E5=86=99=E3=80=8C?= =?UTF-8?q?12=20=E5=88=97=E6=A8=A1=E5=9E=8B=E6=8B=BF=E4=B8=8D=E5=87=86?= =?UTF-8?q?=E3=80=81=E7=95=99=E7=A9=BA=E4=BA=86=E3=80=8D=EF=BC=8C=E5=88=97?= =?UTF-8?q?=E5=90=8D=E6=82=AC=E5=81=9C=E7=9C=8B=E5=BE=97=E5=88=B0=E3=80=81?= =?UTF-8?q?=E4=B9=9F=E5=8F=AF=E4=BB=A5=E5=B1=95=E5=BC=80=EF=BC=88=E6=9C=80?= =?UTF-8?q?=E5=A4=9A=205=20=E4=B8=AA=EF=BC=8C=E6=B2=A1=E5=88=97=E5=85=A8?= =?UTF-8?q?=E5=86=99=E4=B8=80=E5=85=B1=E5=87=A0=E5=88=97=EF=BC=89=EF=BC=9B?= =?UTF-8?q?=E7=BB=93=E6=9D=9F=E5=90=8E=E5=86=99=E5=90=88=E8=AE=A1=E3=80=8C?= =?UTF-8?q?=E5=85=B1=2013=20=E5=88=97=E6=A8=A1=E5=9E=8B=E6=8B=BF=E4=B8=8D?= =?UTF-8?q?=E5=87=86=E3=80=81=E7=95=99=E7=A9=BA=E4=BA=86=EF=BC=882=20?= =?UTF-8?q?=E5=BC=A0=E8=A1=A8=EF=BC=89=E3=80=8D=E5=92=8C=E5=8A=9E=E6=B3=95?= =?UTF-8?q?=E2=80=94=E2=80=94=E5=9C=A8=E8=B5=B7=E8=8D=89=E8=A6=81=E6=B1=82?= =?UTF-8?q?=E9=87=8C=E8=A1=A5=E5=85=85=E7=BC=A9=E5=86=99=E5=92=8C=E6=9C=AF?= =?UTF-8?q?=E8=AF=AD=E7=9A=84=E5=90=AB=E4=B9=89=EF=BC=8C=E5=86=8D=E9=87=8D?= =?UTF-8?q?=E6=96=B0=E8=B5=B7=E8=8D=89=E8=BF=99=E5=87=A0=E5=BC=A0=E8=A1=A8?= =?UTF-8?q?=EF=BC=8C=E5=B7=B2=E6=9C=89=E7=9A=84=E5=86=85=E5=AE=B9=E4=B8=8D?= =?UTF-8?q?=E4=BC=9A=E4=B8=A2=EF=BC=9B=E3=80=8C=E9=87=8D=E6=96=B0=E8=B5=B7?= =?UTF-8?q?=E8=8D=89=E8=BF=99=20N=20=E5=BC=A0=E8=A1=A8=E3=80=8D=E5=9B=9E?= =?UTF-8?q?=E5=88=B0=E8=AE=BE=E7=BD=AE=EF=BC=8C=E8=8C=83=E5=9B=B4=E5=B0=B1?= =?UTF-8?q?=E6=98=AF=E8=BF=99=E5=87=A0=E5=BC=A0=E3=80=81=E5=8B=BE=E7=9D=80?= =?UTF-8?q?=E7=94=A8=E6=A8=A1=E5=9E=8B=E3=80=81=E7=84=A6=E7=82=B9=E5=9C=A8?= =?UTF-8?q?=E8=B5=B7=E8=8D=89=E8=A6=81=E6=B1=82=EF=BC=8C=E8=A1=A5=E5=AE=8C?= =?UTF-8?q?=E5=86=8D=E5=BC=80=E5=A7=8B=EF=BC=9B=E7=BB=93=E6=9D=9F=E5=90=8E?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=88=97=E5=87=BA=E7=9A=84=E8=A1=A8=E8=BF=9E?= =?UTF-8?q?=E5=90=8C=E6=9C=89=E7=95=99=E7=A9=BA=E5=88=97=E7=9A=84=EF=BC=88?= =?UTF-8?q?=E3=80=8C=E5=8F=AA=E7=9C=8B=E6=9C=AA=E5=86=99=E5=85=A8=E3=80=81?= =?UTF-8?q?=E6=9C=89=E7=95=99=E7=A9=BA=E5=88=97=E7=9A=84=E8=A1=A8=E3=80=8D?= =?UTF-8?q?=EF=BC=89=EF=BC=9B=E5=85=B3=E7=9D=80=E5=AF=B9=E8=AF=9D=E6=A1=86?= =?UTF-8?q?=E6=97=B6=E7=BB=93=E6=9D=9F=E7=9A=84=E6=8F=90=E7=A4=BA=E4=B9=9F?= =?UTF-8?q?=E6=8E=A5=E7=9D=80=E5=86=99=E7=95=99=E7=A9=BA=E7=9A=84=E5=88=97?= =?UTF-8?q?=EF=BC=9B=E8=AE=BE=E7=BD=AE=E9=98=B6=E6=AE=B5=E7=9A=84=E3=80=8C?= =?UTF-8?q?=E4=B8=8A=E4=B8=80=E6=AC=A1=E8=B5=B7=E8=8D=89=E3=80=8D=E9=83=BD?= =?UTF-8?q?=E5=86=99=E5=AE=8C=E4=BA=86=E3=80=81=E5=8F=AA=E6=98=AF=E6=9C=89?= =?UTF-8?q?=E7=95=99=E7=A9=BA=E5=88=97=E7=9A=84=E4=B9=9F=E6=8F=90=EF=BC=8C?= =?UTF-8?q?=E5=86=99=E6=98=8E=E5=87=A0=E5=88=97=E3=80=82=E6=B8=85=E5=8D=95?= =?UTF-8?q?=E4=B8=8A=E4=B8=8A=E6=AC=A1=E8=B5=B7=E8=8D=89=E6=9C=89=E7=95=99?= =?UTF-8?q?=E7=A9=BA=E5=88=97=E7=9A=84=E8=A1=A8=E4=B8=80=E8=A1=8C=E6=B7=A1?= =?UTF-8?q?=E8=89=B2=E8=BD=BB=E6=8F=90=E7=A4=BA=EF=BC=88=E4=B8=8D=E6=98=AF?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E6=A0=87=E8=AE=B0=EF=BC=8C=E5=92=8C=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E3=80=81=E9=83=A8=E5=88=86=E5=AE=8C=E6=88=90=E7=9A=84?= =?UTF-8?q?=E6=A0=87=E8=AE=B0=E5=88=86=E5=BC=80=EF=BC=9B=E8=BF=98=E5=9C=A8?= =?UTF-8?q?=E8=B5=B7=E8=8D=89=E7=9A=84=E9=82=A3=E4=B8=80=E6=AC=A1=E4=B8=8D?= =?UTF-8?q?=E7=AE=97=E3=80=8C=E4=B8=8A=E6=AC=A1=E3=80=8D=EF=BC=89=EF=BC=9B?= =?UTF-8?q?=E8=A1=A8=E8=AF=A6=E6=83=85=E9=A1=B6=E9=83=A8=E5=90=8C=E6=A0=B7?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=EF=BC=88=E5=8F=AA=E6=9C=89=E7=95=99=E7=A9=BA?= =?UTF-8?q?=E5=88=97=E6=97=B6=E6=98=AF=E6=8F=90=E7=A4=BA=E3=80=81=E4=B8=8D?= =?UTF-8?q?=E6=98=AF=E5=A4=B1=E8=B4=A5=EF=BC=9B=E9=83=A8=E5=88=86=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E5=8F=88=E6=9C=89=E7=95=99=E7=A9=BA=E5=88=97=E7=9A=84?= =?UTF-8?q?=E5=B9=B6=E5=9C=A8=E4=B8=80=E6=9D=A1=E9=87=8C=EF=BC=89=EF=BC=8C?= =?UTF-8?q?=E7=BB=99=E3=80=8C=E9=87=8D=E6=96=B0=E8=B5=B7=E8=8D=89=E8=BF=99?= =?UTF-8?q?=E5=BC=A0=E8=A1=A8=E3=80=8D=E5=92=8C=E3=80=8C=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E8=B5=B7=E8=8D=89=E8=A6=81=E6=B1=82=E3=80=8D=EF=BC=88=E6=89=93?= =?UTF-8?q?=E5=BC=80=E8=B5=B7=E8=8D=89=E8=AE=BE=E7=BD=AE=EF=BC=8C=E8=8C=83?= =?UTF-8?q?=E5=9B=B4=E5=8F=AA=E6=9C=89=E8=BF=99=E5=BC=A0=E8=A1=A8=EF=BC=89?= =?UTF-8?q?=E3=80=82=E7=B1=BB=E5=9E=8B=E5=8A=A0=20CatalogDraftTask.blank?= =?UTF-8?q?=5Fcolumns=E3=80=81blank=5Fsample=EF=BC=8CCatalogDraftJob.blank?= =?UTF-8?q?=5Fcolumns=EF=BC=8Clast=5Fdraft.blank=5Fcolumns=EF=BC=88?= =?UTF-8?q?=E8=80=81=E6=9C=8D=E5=8A=A1=E7=AB=AF=E6=B2=A1=E6=9C=89=EF=BC=8C?= =?UTF-8?q?=E6=8C=89=200=EF=BC=89=E3=80=82check-catalog=20=E6=96=B0?= =?UTF-8?q?=E5=8A=A0=E4=B8=80=E8=8A=82=E8=A6=86=E7=9B=96=E9=80=90=E8=A1=A8?= =?UTF-8?q?=E4=B8=8E=E6=B1=87=E6=80=BB=E3=80=81=E5=B1=95=E5=BC=80=E5=88=97?= =?UTF-8?q?=E5=90=8D=E3=80=81=E9=BB=98=E8=AE=A4=E5=88=97=E8=A1=A8=E3=80=81?= =?UTF-8?q?=E6=B8=85=E5=8D=95=E8=BD=BB=E6=8F=90=E7=A4=BA=EF=BC=88=E9=A2=9C?= =?UTF-8?q?=E8=89=B2=E5=90=8C=E6=AC=A1=E8=A6=81=E4=BF=A1=E6=81=AF=E3=80=81?= =?UTF-8?q?=E4=B8=8D=E6=98=AF=E8=AD=A6=E5=91=8A=E8=89=B2=EF=BC=89=E3=80=81?= =?UTF-8?q?=E8=BF=9B=E8=A1=8C=E4=B8=AD=E4=B8=8D=E7=AE=97=E3=80=8C=E4=B8=8A?= =?UTF-8?q?=E6=AC=A1=E3=80=8D=E3=80=81=E5=9B=9E=E5=88=B0=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E9=87=8D=E6=96=B0=E8=B5=B7=E8=8D=89=EF=BC=88=E5=85=88=E5=AD=98?= =?UTF-8?q?=E8=B5=B7=E8=8D=89=E8=A6=81=E6=B1=82=E5=86=8D=E5=BB=BA=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=EF=BC=89=E3=80=81=E7=BB=93=E6=9D=9F=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=E3=80=81=E8=A1=A8=E8=AF=A6=E6=83=85=E7=9A=84=E4=B8=A4=E4=B8=AA?= =?UTF-8?q?=E6=8C=89=E9=92=AE=EF=BC=8C=E7=AA=84=E5=B1=8F=E5=B1=95=E5=BC=80?= =?UTF-8?q?=E5=88=97=E5=90=8D=E4=B8=80=E5=B9=B6=E9=87=8F=EF=BC=9Bcheck-all?= =?UTF-8?q?=20=E9=87=8C=E6=95=B0=E6=8D=AE=E7=9B=AE=E5=BD=95=E7=9A=84?= =?UTF-8?q?=E8=80=97=E6=97=B6=E6=8C=89=E5=AE=9E=E6=B5=8B=E6=94=B9=E6=88=90?= =?UTF-8?q?=20130=20=E7=A7=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/lib/terms.ts | 26 ++- frontend/src/pages/catalog/CatalogPage.tsx | 21 ++- frontend/src/pages/catalog/DraftDialog.tsx | 141 ++++++++++++++--- frontend/src/pages/catalog/TableDetail.tsx | 55 +++++-- frontend/src/pages/catalog/TableIndex.tsx | 17 +- frontend/src/pages/catalog/draftJob.ts | 23 +++ frontend/src/types.ts | 11 ++ scripts/check-all.mjs | 2 +- scripts/check-catalog.mjs | 175 ++++++++++++++++++++- 9 files changed, 410 insertions(+), 61 deletions(-) diff --git a/frontend/src/lib/terms.ts b/frontend/src/lib/terms.ts index dfb0aa2f..b409f578 100644 --- a/frontend/src/lib/terms.ts +++ b/frontend/src/lib/terms.ts @@ -2395,6 +2395,8 @@ export const CATALOG_TEXT = { // - 「只重试失败的表」:重新起草失败、部分完成和已停止的表;已经写进目录的内容不回滚、不受影响。 // - 「没写出来」:一次调用没有写出内容(额度用完、只给空壳、超时、出错)。接连没写出来时服务端给一句「提醒」, // 好转后收起;提醒是提醒的样式,不是错误。超时的调用不计用量。 +// - 「留空」:模型列出了一列(或这一段请它写了),中文名和含义却都没给,写「N 列模型拿不准、留空了」。表的状态不因此 +// 变,不算没写全;办法是在起草要求里补充缩写和术语的含义,再重新起草,已有的内容不会丢。 // =========================================================================== /** 起草要求、本次补充说明的字数上限(服务端同一个数:超长 422) */ @@ -2424,9 +2426,14 @@ export const CATALOG_DRAFT_TEXT = { tooMany: (max: number) => `一次最多起草 ${formatNumber(max)} 张表,请缩小范围`, saveGuidanceFailed: (reason: string) => `起草要求未能保存,未开始起草:${reason}`, startFailed: (reason: string) => `未能开始起草:${reason}`, - // ---- 上一次起草(设置阶段,没写全的才提) + // ---- 上一次起草(设置阶段,没写全、有留空列的才提) last: (state: string, at: string) => `上一次起草(${at}):${state}`, + /** 上一次的结果:状态,有留空的列时接着写几列 */ + lastState: (title: string, blank: number) => (blank ? `${title},${formatNumber(blank)} 列模型拿不准、留空了` : title), lastOpen: '查看', + // ---- 从结果、表详情回到设置:范围就是那几张表 + scopeBlank: (n: number) => `有留空列的 ${formatNumber(n)} 张表`, + scopeTable: (name: string) => `这张表:${name}`, // ---- 进行中 queued: '排队中,等待开始…', running: '正在起草…', @@ -2457,6 +2464,15 @@ export const CATALOG_DRAFT_TEXT = { timeoutNote: '超时的调用不计用量,实际用量可能更多', /** 额度用完、超时多了,服务端把之后的段缩小了 */ partColumns: (n: number) => `模型多次用完额度或超时,之后的段每段最多 ${formatNumber(n)} 列`, + // ---- 留空的列:模型列出了、中文名和含义却都没给。表的状态不因此变 + blank: (n: number) => `${formatNumber(n)} 列模型拿不准、留空了`, + /** 留空的列名(服务端最多给 5 个);没列全时写一共几列 */ + blankSample: (sample: string[], n: number) => + `留空的列:${sample.join('、')}${n > sample.length ? ` 等 ${formatNumber(n)} 列` : ''}`, + blankTotal: (columns: number, tables: number) => `共 ${formatNumber(columns)} 列模型拿不准、留空了(${formatNumber(tables)} 张表)`, + blankAdvice: '可以在起草要求里补充缩写和术语的含义,再重新起草这几张表,已有的内容不会丢', + blankRedraft: (n: number) => `重新起草这 ${formatNumber(n)} 张表`, + blankRedraftHint: '回到起草设置,范围是这几张表:先在起草要求里补充缩写和术语的含义,再开始起草', // ---- 结束 title: { queued: '排队中,等待开始…', @@ -2486,6 +2502,7 @@ export const CATALOG_DRAFT_TEXT = { retryHint: '重新起草失败、部分完成和已停止的表;已写进目录的内容不受影响', showAll: (n: number) => `显示全部 ${formatNumber(n)} 张表`, showProblems: '只看未写全的表', + showProblemsBlank: '只看未写全、有留空列的表', tasks: '逐表进度', taskStatus: { pending: '等待', running: '起草中', ok: '已完成', partial: '部分完成', failed: '失败', skipped: '已跳过', cancelled: '已停止', @@ -2501,6 +2518,8 @@ export const CATALOG_DRAFT_TEXT = { toast: { succeeded: (summary: string) => `数据目录起草完成:${summary}`, partial: (n: number) => `数据目录起草完成,${formatNumber(n)} 张表未写全`, + /** 完成、部分完成的提示后面接着写留空的列(没有就不写) */ + withBlank: (text: string, blank: number) => (blank ? `${text};${formatNumber(blank)} 列模型拿不准、留空了` : text), failed: (reason: string) => (reason ? `数据目录起草失败:${reason}` : '数据目录起草失败'), cancelled: '数据目录起草已停止', interrupted: '服务重启,数据目录起草中断', @@ -2510,6 +2529,11 @@ export const CATALOG_DRAFT_TEXT = { lastFailed: '上次起草失败', lastPartial: '上次起草部分完成', lastWith: (label: string, reason: string) => `${label}:${reason}`, + /** 上次起草有留空的列:清单上是轻提示(不是失败标记),表详情顶部同一句 */ + lastBlank: (n: number) => `上次起草有 ${formatNumber(n)} 列模型拿不准、留空了`, + lastBlankAdvice: '可以在起草要求里补充缩写和术语的含义,再重新起草这张表,已有的内容不会丢', + draftGuidance: '补充起草要求', + draftGuidanceHint: '打开起草设置,范围是这张表:先在起草要求里补充缩写和术语的含义,再开始起草', redraft: '重新起草这张表', redraftHint: '用模型重新起草这张表(不带补充说明,带上数据源的起草要求);人工确认、驳回过的项不会被改动', redraftBusy: '已有起草任务在进行,结束后再重新起草这张表', diff --git a/frontend/src/pages/catalog/CatalogPage.tsx b/frontend/src/pages/catalog/CatalogPage.tsx index 51097aea..3257ec32 100644 --- a/frontend/src/pages/catalog/CatalogPage.tsx +++ b/frontend/src/pages/catalog/CatalogPage.tsx @@ -12,8 +12,8 @@ import { } from '../../lib/terms' import { StatusLegend, SystemNotesNotice } from './parts' import { DraftDialog } from './DraftDialog' -import { isJobActive, retryableCount, useDraftJob } from './draftJob' -import type { DraftStartBody } from './draftJob' +import { isJobActive, jobBlank, retryableCount, useDraftJob } from './draftJob' +import type { DraftPreset, DraftStartBody } from './draftJob' import { ProfileDialog } from './ProfileDialog' import type { ProfileJob } from './ProfileDialog' import { ProfileSettingsDialog } from './ProfileSettings' @@ -105,9 +105,9 @@ export function CatalogPage() { const [selected, setSelected] = useState>(() => new Set()) /** * 起草对话框开着没有、打开时显示什么:setup 选范围开始;job 显示页面跟着的任务(进行中的进度、刚结束的结果)。 - * busy:是撞上已有任务(409)转过去显示的 + * busy:是撞上已有任务(409)转过去显示的;preset:设置阶段预先选好的范围(表详情里「补充起草要求」) */ - const [draftOpen, setDraftOpen] = useState<{ view: 'setup' | 'job'; busy?: boolean } | null>(null) + const [draftOpen, setDraftOpen] = useState<{ view: 'setup' | 'job'; busy?: boolean; preset?: DraftPreset } | null>(null) /** 起草写进了这些表:详情据此重新载入(编辑中的不动,保存时服务端会用版本号拦下) */ const [drafted, setDrafted] = useState<{ seq: number; tables: Set }>({ seq: 0, tables: new Set() }) const [profiling, setProfiling] = useState(false) @@ -169,8 +169,10 @@ export function CatalogPage() { key: `catalog-draft:${src}`, duration: 10000, action: { label: DT.viewResult, onClick: () => { if (sourceRef.current === src) setDraftOpen({ view: 'job' }) } }, } - if (job.status === 'succeeded') toast.ok(DT.toast.succeeded(CT.draftSummary(job.added, job.updated, job.removed)), opts) - else if (job.status === 'partial') toast.warn(DT.toast.partial(retryableCount(job)), opts) + // 留空的列不算没写全,提示里接着写一句,点「查看结果」看是哪几张表、怎么办 + const blank = jobBlank(job) + if (job.status === 'succeeded') toast.ok(DT.toast.withBlank(DT.toast.succeeded(CT.draftSummary(job.added, job.updated, job.removed)), blank), opts) + else if (job.status === 'partial') toast.warn(DT.toast.withBlank(DT.toast.partial(retryableCount(job)), blank), opts) else if (job.status === 'failed') toast.error(DT.toast.failed(job.error ?? ''), opts) else if (job.status === 'interrupted') toast.warn(DT.toast.interrupted, opts) else toast.warn(DT.toast.cancelled, opts) @@ -195,6 +197,10 @@ export function CatalogPage() { toast.error(e) } }, [draft.start]) + /** 详情里「补充起草要求」(上次起草有留空的列):打开起草设置,范围只有这张表,先补起草要求再开始 */ + const draftWithGuidance = useCallback((t: string) => { + setDraftOpen({ view: 'setup', preset: { tables: [t], label: DT.scopeTable(t) } }) + }, []) const onGuidanceSaved = useCallback((g: string | null) => { setData((cur) => cur && ({ ...cur, guidance: g })) }, []) @@ -402,7 +408,7 @@ export function CatalogPage() { ? ( + draftBusy={!!stripJob} onRedraft={redraftTable} onDraftGuidance={draftWithGuidance} /> ) : } @@ -446,6 +452,7 @@ export function CatalogPage() { job={draftJob} initial={draftOpen.view} initialBusy={!!draftOpen.busy} + preset={draftOpen.preset ?? null} stopping={draft.stopping} onStart={startDraft} onCancel={draft.cancel} diff --git a/frontend/src/pages/catalog/DraftDialog.tsx b/frontend/src/pages/catalog/DraftDialog.tsx index c68c46d4..4dafc323 100644 --- a/frontend/src/pages/catalog/DraftDialog.tsx +++ b/frontend/src/pages/catalog/DraftDialog.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef, useState } from 'react' -import { CircleCheck, CircleDashed, CircleMinus, CircleSlash, CircleX, RotateCcw, Sparkles, TriangleAlert } from 'lucide-react' +import { CircleCheck, CircleDashed, CircleMinus, CircleSlash, CircleX, Info, RotateCcw, Sparkles, TriangleAlert } from 'lucide-react' import type { LucideIcon } from 'lucide-react' import clsx from 'clsx' import { ApiError, api } from '../../api/client' @@ -9,26 +9,33 @@ import { errorMessage } from '../../lib/errors' import { formatTime } from '../../lib/format' import { CATALOG_DRAFT_TEXT as DT, CATALOG_TEXT as CT, DRAFT_GUIDANCE_MAX, DRAFT_NOTE_MAX } from '../../lib/terms' import { - DRAFT_MAX_TABLES, DRAFT_MODEL_UNAVAILABLE, DRAFT_PART_COLUMNS, DRAFT_TOP_N, RETRYABLE, isJobActive, nothingWritten, retryableCount, - taskReason, + DRAFT_MAX_TABLES, DRAFT_MODEL_UNAVAILABLE, DRAFT_PART_COLUMNS, DRAFT_TOP_N, RETRYABLE, blankOf, isJobActive, jobBlank, needsLook, + nothingWritten, retryableCount, taskReason, } from './draftJob' -import type { DraftStartBody } from './draftJob' +import type { DraftPreset, DraftStartBody } from './draftJob' // =========================================================================== // 起草数据目录。起草是服务端的后台任务:这里选范围、写起草要求和本次补充说明,建任务;之后显示页面跟着的那个任务 // (进行中的进度、结束后的结果),任务本身的状态在页面上(draftJob.ts),关掉对话框照常进行。 // // 三段:设置(范围、用不用模型;用模型时写起草要求、补充说明,写明用哪个起草模型)→ 进行中(总进度、合计、调用接连 -// 没写出来时的提醒、逐表的状态和宽表的段进度、没写全的原因;可以停止、在后台继续)→ 结束(汇总、没写全的表和原因、 -// 只重试失败的表)。 +// 没写出来时的提醒、逐表的状态和宽表的段进度、没写全的原因、留空的列;可以停止、在后台继续)→ 结束(汇总、没写全的表 +// 和原因、留空的列合计、只重试失败的表)。 // 已经写进目录的内容不回滚——起草只增改推断项,人工确认、驳回过的项服务端不会动。 +// +// 留空的列(模型列出了、中文名和含义都没给)不算没写全,表照样是已完成;结果里写合计和办法——在起草要求里补充缩写和 +// 术语的含义,再重新起草这几张表。「重新起草这几张表」回到设置:范围就是那几张、勾着用模型、焦点在起草要求, +// 补完再开始。表详情里的「补充起草要求」也是这样打开(范围是那一张,见 preset)。 // =========================================================================== -type Scope = 'selected' | 'filtered' | 'top' | 'all' +type Scope = 'preset' | 'selected' | 'filtered' | 'top' | 'all' /** 字数按服务端的算法:去掉首尾空白后的字符数(按码位,不按 UTF-16 的长度) */ const charCount = (s: string) => [...s.trim()].length +/** 起草要求输入框的 id:预先选好范围回到设置时焦点放在这里 */ +const GUIDANCE_ID = 'catalog-draft-guidance' + /** * 建任务(开始、只重试失败的表)没建成怎么说。起草模型用不了(409,建任务和重试都会回):服务端那句话已经写了原因和 * 去处,照原样,长句也不拆成「标题:原因」;别的错误前面写明没有开始 @@ -37,8 +44,8 @@ const startErrorText = (e: unknown) => (e instanceof ApiError && e.code === DRAFT_MODEL_UNAVAILABLE ? e.message : DT.startFailed(errorMessage(e))) export function DraftDialog({ - sourceId, sourceDescription, guidance, rows, visible, selected, filtered, job, initial, initialBusy = false, stopping, - onStart, onCancel, onRetry, onTrack, onGuidanceSaved, onClose, + sourceId, sourceDescription, guidance, rows, visible, selected, filtered, job, initial, initialBusy = false, preset: initialPreset = null, + stopping, onStart, onCancel, onRetry, onTrack, onGuidanceSaved, onClose, }: { sourceId: string /** 数据源说明:用模型起草时连同起草要求一起交给模型 */ @@ -58,6 +65,11 @@ export function DraftDialog({ initial: 'setup' | 'job' /** 打开时显示的任务是撞上已有任务(409)转过来的:写明 */ initialBusy?: boolean + /** + * 打开设置时预先选好的范围(表详情里「补充起草要求」:只有那张表)。给了就勾上用模型、焦点放在起草要求上: + * 打开它就是为了先补起草要求再起草 + */ + preset?: DraftPreset | null /** 点了停止、服务端还没停下来 */ stopping: boolean /** 建任务;409 已有任务时页面改取进行中的那一个(busy 为真) */ @@ -73,8 +85,12 @@ export function DraftDialog({ // 表结构里已经没有的表起草不了(服务端会记「已跳过」),不算进范围 const live = useMemo(() => rows.filter((r) => r.in_schema).map((r) => r.table_name), [rows]) const liveSet = useMemo(() => new Set(live), [live]) + /** 预先选好的范围:打开时给的,或者结果里点了「重新起草这几张表」 */ + const [preset, setPreset] = useState(initialPreset) const scopes = useMemo(() => { const out: { key: Scope; label: string; tables: string[] }[] = [] + const pre = preset?.tables.filter((t) => liveSet.has(t)) ?? [] + if (preset && pre.length) out.push({ key: 'preset', label: preset.label, tables: pre }) const sel = selected.filter((t) => liveSet.has(t)) if (sel.length) out.push({ key: 'selected', label: CT.scopeSelected(sel.length), tables: sel }) const vis = visible.filter((r) => r.in_schema).map((r) => r.table_name) @@ -82,11 +98,13 @@ export function DraftDialog({ if (live.length > DRAFT_TOP_N) out.push({ key: 'top', label: CT.scopeTop(DRAFT_TOP_N), tables: live.slice(0, DRAFT_TOP_N) }) out.push({ key: 'all', label: CT.scopeAll(live.length), tables: live }) return out - }, [selected, visible, filtered, live, liveSet]) + }, [preset, selected, visible, filtered, live, liveSet]) const [scope, setScope] = useState(() => scopes[0].key) const tables = scopes.find((s) => s.key === scope)?.tables ?? scopes[0].tables const tooMany = tables.length > DRAFT_MAX_TABLES - const [useModel, setUseModel] = useState(false) + const [useModel, setUseModel] = useState(!!initialPreset) + /** 回到设置以后把焦点放进起草要求(预先选好范围的两种入口都是为了先补起草要求) */ + const [focusGuidance, setFocusGuidance] = useState(!!initialPreset) const [guidanceText, setGuidanceText] = useState(guidance) const [note, setNote] = useState('') const guidanceLen = charCount(guidanceText) @@ -102,7 +120,7 @@ export function DraftDialog({ const [busy, setBusy] = useState(initialBusy) const [acting, setActing] = useState(false) const [actionError, setActionError] = useState(null) - /** 上一次起草(没写全的才提),设置阶段写一行、可以点开 */ + /** 上一次起草(没写全、有留空列的才提),设置阶段写一行、可以点开 */ const [last, setLast] = useState(null) /** 起草模型的说法:设置里的起草模型,没配就是跟随助手的模型 */ const [modelLine, setModelLine] = useState<{ text: string; warn: string | null } | null>(null) @@ -120,13 +138,22 @@ export function DraftDialog({ onTrackRef.current(latest) setBusy(true) setMode('job') - } else if (latest.status !== 'succeeded') { + } else if (latest.status !== 'succeeded' || jobBlank(latest) > 0) { setLast(latest) } }, () => {}) return () => { alive = false } }, [inSetup, sourceId]) + // 预先选好范围回到设置(打开时给的、结果里点的):焦点放进起草要求 + useEffect(() => { + if (!focusGuidance || mode !== 'setup' || !useModel) return + const el = document.getElementById(GUIDANCE_ID) + if (!el) return + el.focus() + setFocusGuidance(false) + }, [focusGuidance, mode, useModel]) + // 起草模型:设置里的 catalog_draft 填了就是它;没填跟随助手的模型(写出当前是哪个,用不了时写原因) useEffect(() => { let alive = true @@ -194,6 +221,26 @@ export function DraftDialog({ setBusy(false) setMode('job') } + /** + * 结果里「重新起草这几张表」(有留空列的表):回到设置,范围就是这几张,勾上用模型,焦点放进起草要求——先补上 + * 缩写和术语的含义再开始。任务本身不动(页面照旧跟着它,开始新的起草才换) + */ + const redraftBlank = (names: string[]) => { + setPreset({ tables: names, label: DT.scopeBlank(names.length) }) + setScope('preset') + setUseModel(true) + setFocusGuidance(true) + setStartError(null) + setActionError(null) + setBusy(false) + setLast(null) + setMode('setup') + } + /** 结果里有留空列、还在表结构里的表(「重新起草这几张表」的范围) */ + const blankTables = useMemo( + () => (job ? job.tasks.filter((t) => blankOf(t) > 0 && liveSet.has(t.table_name)).map((t) => t.table_name) : []), + [job, liveSet], + ) const phase: 'setup' | 'running' | 'done' = inSetup ? 'setup' : isJobActive(job) ? 'running' : 'done' const canStart = !!tables.length && !tooMany && !(useModel && (guidanceOver || noteOver)) && !starting @@ -263,7 +310,7 @@ export function DraftDialog({ {modelLine?.warn && {modelLine.warn}}
- {(p) => (