diff --git a/src/components/generator/GeneratorApp.tsx b/src/components/generator/GeneratorApp.tsx index c8f4157..5f753e6 100644 --- a/src/components/generator/GeneratorApp.tsx +++ b/src/components/generator/GeneratorApp.tsx @@ -46,6 +46,15 @@ const QUALITY_LABEL_KEYS: Record = { + goal: 'qualityGoalHint', + features: 'qualityFeaturesHint', + io: 'qualityIoHint', + acceptance: 'qualityAcceptanceHint', + who: 'qualityWhoHint', + metric: 'qualityMetricHint', +}; + export function GeneratorApp({ locale, dict }: Props) { const [state, setState] = useState(DEFAULT_FORM); const [lang, setLang] = useState(locale); @@ -355,6 +364,7 @@ export function GeneratorApp({ locale, dict }: Props) { {promptHealth.checks.map((check) => ( ))} + {!promptHealth.ready && ( +
    + {promptHealth.checks + .filter((check) => !check.ok) + .slice(0, 2) + .map((check) => ( +
  • + + + + {dict.generator[QUALITY_LABEL_KEYS[check.id]]} + + {' · '} + {dict.generator[QUALITY_HINT_KEYS[check.id]]} + +
  • + ))} +
+ )}
               {prompt}
diff --git a/src/data/cases/_more.ts b/src/data/cases/_more.ts
index 10ed28d..68fc31f 100644
--- a/src/data/cases/_more.ts
+++ b/src/data/cases/_more.ts
@@ -2249,7 +2249,7 @@ export const dataSkuProfitRadar: CaseBundle = {
       communication: COMMUNICATION_ZH,
     }, 'zh'),
     en: composeCasePrompt({
-      role: caseRole('an e-commerce data analyst (tool must be usable by non-technical teammates)', 'en'),
+      role: caseRole('an e-commerce data analyst whose teammates across the business will use this tool — make it one-click and friendly', 'en'),
       goal: 'Combine revenue, margin, ads, refunds, and stock turnover to find SKUs worth more resources.',
       platform: '- Windows + macOS; Electron + React + TypeScript; local SQLite; SheetJS; ECharts',
       features: `1. Import sales, cost, ad spend, refund, and stock sheets; merge by SKU.
@@ -2324,7 +2324,7 @@ export const productListingQualityChecker: CaseBundle = {
       communication: COMMUNICATION_ZH,
     }, 'zh'),
     en: composeCasePrompt({
-      role: caseRole('merchandising/product teammates (must be usable by non-technical staff)', 'en'),
+      role: caseRole('merchandising/product teammates — the tool should feel natural in their hands, one-click with no setup', 'en'),
       goal: 'Batch-check listing materials for completeness and consistency to reduce last-minute rework before launch.',
       platform: '- Windows + macOS; Electron + React + TypeScript; SheetJS; local JSON rules',
       features: `1. Import product sheet, image list, certificate file list.
diff --git a/src/data/cases/finance.ts b/src/data/cases/finance.ts
index 12e2619..a9ff4fe 100644
--- a/src/data/cases/finance.ts
+++ b/src/data/cases/finance.ts
@@ -57,15 +57,13 @@ export const financeReconciliation: CaseBundle = {
       platform: `- 平台:Windows 10 / 11 桌面应用
 - 框架:Electron + React + TypeScript
 - 表格处理:SheetJS
-- 默认本地处理;联网传输须加密并告知用户
 - 交付时打包成 Windows .exe 安装包`,
       features: `1. 首页两个醒目按钮:「导入订单 Excel」「导入银行流水 Excel」,支持拖拽。
 2. 导入后自动显示表头和前 5 行预览,让用户从下拉框里选「订单号」「金额」「交易时间」「备注」字段。记住选择下次默认回填。
 3. 示例字段兼容:订单表常见列「订单号 / 实付金额 / 下单时间 / 店铺」;银行流水常见列「交易单号 / 收入金额 / 交易时间 / 摘要」。列名可有细微差异。
 4. 「开始对账」按钮显示进度条。匹配逻辑:订单号去空格后作为主键;金额差 <= 0.01 元视为匹配;重复订单号要合并提示;退款 / 冲正流水单独标记,不直接当异常。
 5. 结果页两栏:左边展示匹配数、差异数、匹配率、总金额;右边表格列出每一条差异,字段包括订单号、订单金额、银行金额、差额、可能原因(金额不一致 / 银行流水缺失 / 订单缺失 / 重复订单 / 疑似退款)。
-6. 「导出差异明细到 Excel」按钮,默认文件名 "差异明细-YYYY-MM.xlsx"。
-7. 数据默认本地处理,联网功能须告知用户并加密传输。`,
+6. 「导出差异明细到 Excel」按钮,默认文件名 "差异明细-YYYY-MM.xlsx"。`,
       sampleData: `sample-data/ 中放两个文件:
 orders.xlsx — 列:订单号, 实付金额, 下单时间, 店铺(示例:DD202401001, 299.00, 2024-01-05, 旗舰店)
 bank.xlsx — 列:交易单号, 收入金额, 交易时间, 摘要(示例:DD202401001, 299.00, 2024-01-06, 支付宝转入)
@@ -98,15 +96,13 @@ bank.xlsx — 列:交易单号, 收入金额, 交易时间, 摘要(示例:
       platform: `- Platform: Windows 10 / 11 desktop app
 - Framework: Electron + React + TypeScript
 - Spreadsheet parsing: SheetJS
-- Process locally by default; network calls require encryption and user consent
 - Deliver a Windows .exe installer`,
       features: `1. Home screen with two prominent buttons: "Import Orders Excel" and "Import Bank Excel". Drag-and-drop works.
 2. After import, show headers and first 5 rows. Let the user pick Order ID / Amount / Transaction Time / Notes columns from dropdowns. Remember and pre-fill next time.
 3. Example headers to support: orders may use Order ID / Paid Amount / Order Time / Store; bank statements may use Transaction ID / Income Amount / Transaction Time / Memo. Tolerate minor header wording differences.
 4. "Reconcile" button triggers a progress bar. Rule: trim order IDs and use them as keys; difference <= 0.01 = match; duplicate order IDs are grouped and flagged; refunds / reversals are tagged separately, not treated as ordinary mismatches.
 5. Results page has two panes. Left: matched count, mismatched count, match rate, total amount. Right: a mismatch table with order ID, order amount, bank amount, diff, reason (amounts differ / missing in bank / missing in orders / duplicate order / likely refund).
-6. "Export mismatches to Excel" with default filename "diff-YYYY-MM.xlsx".
-7. Process locally by default; network calls require encryption and user consent.`,
+6. "Export mismatches to Excel" with default filename "diff-YYYY-MM.xlsx".`,
       style: `- Minimal desktop-tool style: light background, clear sections, radius 8, moderate information density.
 - Primary button: muted dark. Secondary: light gray.
 - Follows Windows light/dark setting.
diff --git a/src/i18n/dictionaries/en.ts b/src/i18n/dictionaries/en.ts
index 19fb342..21b72a9 100644
--- a/src/i18n/dictionaries/en.ts
+++ b/src/i18n/dictionaries/en.ts
@@ -190,6 +190,12 @@ export const en = {
     qualityMetric: 'Measurable gain',
     qualityPass: 'OK',
     qualityImprove: 'Add',
+    qualityGoalHint: 'In one or two sentences, say who it is for, what it solves, and the outcome.',
+    qualityFeaturesHint: 'One line each — be concrete: which file to drag in, which button to click, what comes out.',
+    qualityIoHint: 'Name the input and output, e.g. "Import Excel → export diff sheet".',
+    qualityAcceptanceHint: 'Spell out what counts as done in one line, e.g. "main flow runs, empty data does not crash, double-click to open".',
+    qualityWhoHint: 'Say who will use it in one phrase, e.g. "finance teammate / teacher / just me".',
+    qualityMetricHint: 'Give one measurable metric, e.g. "from 2 days to 1 hour" or "one click replaces 30 manual steps".',
 
     quickTemplatesTitle: 'Start from a template',
     quickTemplatesHint: 'One click fills the form. Tweak and go.',
diff --git a/src/i18n/dictionaries/zh.ts b/src/i18n/dictionaries/zh.ts
index a8a5212..8a78661 100644
--- a/src/i18n/dictionaries/zh.ts
+++ b/src/i18n/dictionaries/zh.ts
@@ -191,6 +191,12 @@ export const zh = {
     qualityMetric: '量化收益',
     qualityPass: '已填',
     qualityImprove: '待补充',
+    qualityGoalHint: '一两句话说清楚:给谁用、解决什么、产出是什么。',
+    qualityFeaturesHint: '一行一条,具体到拖入哪个文件、点哪个按钮、产出什么。',
+    qualityIoHint: '写清楚输入和输出,例如"导入 Excel → 导出差异表"。',
+    qualityAcceptanceHint: '补一句怎么算做完,例如"主流程跑通、空数据不闪退、可双击打开"。',
+    qualityWhoHint: '写一下谁来用,例如"财务同事 / 老师 / 自己用都行"。',
+    qualityMetricHint: '给一个量化指标,例如"从 2 天压到 1 小时"或"一键替代手工 30 步"。',
 
     quickTemplatesTitle: '不想从零开始?挑个模板',
     quickTemplatesHint: '一键填好,改几个字就能用。',
diff --git a/src/lib/generatorPromptHealth.ts b/src/lib/generatorPromptHealth.ts
index 718e30f..3116f52 100644
--- a/src/lib/generatorPromptHealth.ts
+++ b/src/lib/generatorPromptHealth.ts
@@ -67,6 +67,10 @@ const WHO_WORDS = [
   '部门',
   '门店',
   '小伙伴',
+  '客户',
+  '用户',
+  '学生',
+  '老师',
   'team',
   'finance',
   'ops',
@@ -80,6 +84,11 @@ const WHO_WORDS = [
   'colleague',
   'department',
   'store',
+  'teammate',
+  'customer',
+  'user',
+  'student',
+  'teacher',
 ] as const;
 
 const METRIC_WORDS = [
@@ -96,6 +105,9 @@ const METRIC_WORDS = [
   '节省',
   '省下',
   '快',
+  '批量',
+  '一键',
+  '成倍',
   'hour',
   'minute',
   'second',
@@ -105,8 +117,17 @@ const METRIC_WORDS = [
   'faster',
   'times',
   'save time',
+  'batch',
+  'one-click',
+  'instant',
+  'in one go',
 ] as const;
 
+// Number + unit pattern: "2 天", "1 小时", "10 万行", "100k rows", "30%".
+// A measurable goal usually has a quantified change; this catches more cases
+// than the keyword list alone.
+const METRIC_NUMBER_UNIT = /\d+\s*(?:%|天|时|分|秒|周|月|年|倍|万|千|百|个|条|行|次|页|kb|mb|gb|k|m|day|hour|min|sec|week|month|year|row|item|page|file)/i;
+
 function includesAny(text: string, words: readonly string[]) {
   const normalized = text.toLowerCase();
   return words.some((word) => normalized.includes(word.toLowerCase()));
@@ -136,7 +157,7 @@ export function getPromptHealth(
     },
     {
       id: 'metric',
-      ok: includesAny(requestText, METRIC_WORDS),
+      ok: includesAny(requestText, METRIC_WORDS) || METRIC_NUMBER_UNIT.test(requestText),
     },
   ];
   const passed = checks.filter((check) => check.ok).length;
diff --git a/src/lib/promptBuilder.ts b/src/lib/promptBuilder.ts
index dfe369e..8bde7d7 100644
--- a/src/lib/promptBuilder.ts
+++ b/src/lib/promptBuilder.ts
@@ -352,7 +352,7 @@ export function buildRecoveryPrompt(state: FormState, lang: PromptLang): string
 2. 最小 diff 修改;不删功能、不换技术栈来绕过
 3. 重新运行:安装 / lint / 类型检查 / 构建 / 启动
 4. 同一问题 3 次失败 → 降级边缘功能,先恢复主流程
-5. 修完后用示例数据走完主流程,看到产物再说修好
+5. 修完后用示例数据走完主流程,看到产物才算修好
 
 遵守原提示词的所有安全底线和执行纪律。
 
diff --git a/src/lib/promptModules.test.ts b/src/lib/promptModules.test.ts
index c51faab..d58c05b 100644
--- a/src/lib/promptModules.test.ts
+++ b/src/lib/promptModules.test.ts
@@ -41,6 +41,7 @@ describe('prompt module constants', () => {
     expect(CONSTRAINTS_ZH).toContain('脱敏 sample-data');
     expect(CONSTRAINTS_ZH).toContain('npm view');
     expect(CONSTRAINTS_ZH).toContain('立即运行验证');
+    expect(CONSTRAINTS_ZH).toContain('另存为');
   });
 
   it('CONSTRAINTS_EN contains key rules', () => {
@@ -48,6 +49,7 @@ describe('prompt module constants', () => {
     expect(CONSTRAINTS_EN).toContain('anonymized sample-data');
     expect(CONSTRAINTS_EN).toContain('npm view');
     expect(CONSTRAINTS_EN).toContain('Verify each feature');
+    expect(CONSTRAINTS_EN).toContain('Save as');
   });
 
   it('DOD_ZH has checklist format', () => {
@@ -114,7 +116,7 @@ describe('prompt module constants', () => {
 
   it('OPENING_BRIEF_EN primes Codex to greet the user in 3-8 numbered lines', () => {
     expect(OPENING_BRIEF_EN).toContain('[Opening Brief]');
-    expect(OPENING_BRIEF_EN).toContain('3-8 lines');
+    expect(OPENING_BRIEF_EN).toContain('3-8 numbered lines');
     expect(OPENING_BRIEF_EN).toContain('numbered');
     expect(OPENING_BRIEF_EN).toContain("Don't wait");
     expect(OPENING_BRIEF_EN).toContain("Don't promise a timeline");
@@ -125,7 +127,6 @@ describe('prompt module constants', () => {
     expect(WARM_UX_ZH).toContain('Demo 模式');
     expect(WARM_UX_ZH).toContain('用示例数据试一试');
     expect(WARM_UX_ZH).toContain('业务语言');
-    expect(WARM_UX_ZH).toContain('另存为');
     expect(WARM_UX_ZH).toContain('系统通知');
   });
 
@@ -186,7 +187,6 @@ describe('prompt module constants', () => {
     expect(WARM_UX_EN).toContain('demo mode');
     expect(WARM_UX_EN).toContain('Try with sample data');
     expect(WARM_UX_EN).toContain('business language');
-    expect(WARM_UX_EN).toContain('Save as');
     expect(WARM_UX_EN).toContain('system notification');
   });
 
diff --git a/src/lib/promptModules.ts b/src/lib/promptModules.ts
index c2a62f0..8b8453a 100644
--- a/src/lib/promptModules.ts
+++ b/src/lib/promptModules.ts
@@ -12,14 +12,14 @@ export type ModuleTech = 'electron' | 'tauri' | 'pyqt' | 'auto';
 export const SAFETY_RULES_ZH = `【安全底线】
 - 默认本地处理;需要联网时须加密传输并明确告知用户。
 - 不写死 API Key、绝对路径、个人邮箱或内网地址。
-- 输出不覆盖原文件,冲突加时间后缀。
+- 写盘默认走"另存为";不覆盖原文件,冲突自动加时间后缀。
 - 不引入不存在的 npm 包;不确定时先查 npm view。
 - 缺真实数据时先造脱敏 sample-data,不等用户提供文件才开工。`;
 
 export const SAFETY_RULES_EN = `[Safety Rules]
 - Process locally by default; network calls require encryption and explicit user consent.
 - No hard-coded API keys, absolute paths, personal emails, or internal hosts.
-- Never overwrite inputs; timestamp conflicts.
+- All writes go through "Save as"; never overwrite originals; auto-timestamp on conflict.
 - Do not invent npm packages; verify with npm view first.
 - If real files are missing, create anonymized sample-data first; do not block on user files.`;
 
@@ -38,36 +38,14 @@ export const CONSTRAINTS_EN = `${SAFETY_RULES_EN}\n\n${QUALITY_RULES_EN}`;
 // ─── Opening Brief (talk to the user before you build) ──────────
 
 export const OPENING_BRIEF_ZH = `【开工前的开场白】
-动手之前,先用 3-8 句告诉用户接下来会发生什么。3 句够就只用 3 句。
-
-每句一行,带数字序号。短句。干净。有节奏。不用专业词。用「你」对话。讲画面、讲体感、讲结果——不讲实现。
-
-下面是可选的素材清单,挑用得上的写,按这个顺序排——用不上的直接跳过,宁缺毋滥:
-- 一句话点题:要做的是什么。
-- 它替你解决什么。
-- 关键的几步,一步一句。
-- 用到了什么,一行带过。
-- 打开后,你会看到什么。
-- 你的文件存在哪,谁能看到。
-- 下一步看什么。
-
-不要承诺时间("X 分钟内可用"),因为不准。说完不等回话,按下面的流程立刻动手。`;
+动手之前,先用 3-8 句告诉用户接下来会发生什么。3 句够就 3 句,每句一行、带数字序号。
+讲画面、讲体感、讲结果——不讲实现。短句、不用专业词、用「你」对话。
+不要承诺时间("X 分钟内可用"),因为不准。说完不等回话,按下面的【快速启动协议】立刻动手。`;
 
 export const OPENING_BRIEF_EN = `[Opening Brief]
-Before you build, use 3-8 lines to tell the user what's coming. If 3 lines do it, use 3.
-
-One sentence per line, numbered. Short. Clean. Rhythmic. No jargon. Speak to "you". Picture, feel, result — not implementation.
-
-Pick from this checklist — keep them in order, skip what doesn't fit. Better to leave one out than fill in fluff:
-- What you'll build, in one line.
-- The friction it removes.
-- The key steps — one per line.
-- The stack, in a line.
-- What you'll see when it opens.
-- Where your files live. Who sees them.
-- What to look at next.
-
-Don't promise a timeline ("ready in X minutes") — you can't know. Don't wait for a reply. Follow the flow below immediately.`;
+Before you build, use 3-8 numbered lines to tell the user what's coming. If 3 lines do it, use 3 — one short sentence per line.
+Picture, feel, result — not implementation. Plain words. Speak to "you".
+Don't promise a timeline ("ready in X minutes") — you can't know. Don't wait for a reply. Follow the [Quick Start Protocol] below immediately.`;
 
 // ─── Warm UX Contract (treat the user with care) ────────────────
 
@@ -76,9 +54,8 @@ export const WARM_UX_ZH = `【温暖体验契约】
 - 首次启动 = Demo 模式:自动加载 sample-data/ 跑完主流程一次,让用户立刻看到结果界面,而不是空状态。
 - 工作台顶部永远有「用示例数据试一试」按钮,任何时候都能一键演示。
 - 按钮、提示、错误一律用业务语言。例:「找不到订单号这一列」,不是「Column "order_id" not found」。
-- 任何写盘操作默认走「另存为」;从不覆盖原文件,冲突自动加时间后缀。
 - 步骤 ≥3 的操作给"撤销"或"取消"出口;≥5 步的关键操作要二次确认。
-- 大批量任务显示进度条 + 预估剩余时间,最长每 1 秒刷新一次。
+- 大批量任务显示进度条 + 预估剩余时间,每秒最多刷新一次。
 - 主流程一完成就在应用内给反馈;若窗口在后台,再发一次系统通知(Toast),点击直达结果。
 - 失败时永远给出"下一步可以做什么"(重试 / 换文件 / 查看日志 / 复制错误),不要只留一行红色字。`;
 
@@ -87,7 +64,6 @@ What happens around the code matters more than the code itself. The finish shoul
 - First launch = demo mode: auto-load sample-data/ and run the main flow once so the user sees a real result page, not an empty state.
 - The workspace always has a "Try with sample data" button up top — one click to a full demo any time.
 - Buttons, hints, and errors in business language. Example: "Can't find the Order ID column", not "Column 'order_id' not found".
-- Any write goes through "Save as"; never overwrite originals; timestamp on conflict.
 - Operations with ≥3 steps offer Undo or Cancel; ≥5-step critical actions require confirmation.
 - Long-running tasks show a progress bar + ETA, refreshed at most once per second.
 - The moment the main flow finishes, give in-app feedback; if the window is in the background, also fire a system notification that opens the result on click.
@@ -96,7 +72,7 @@ What happens around the code matters more than the code itself. The finish shoul
 // ─── Success Picture (the moment of "wow") ──────────────────────
 
 export const SUCCESS_PICTURE_ZH = `【完成态画面】
-主流程结束的那一屏,是用户对这个工具的第一印象。把它当礼物来做。
+主流程结束的那一屏,是用户对这个工具最持久的印象。把它当礼物来做。
 - 大号数字 + 业务语言小结,30 字以内。例:「对账 482 单,差异 5 单。已存到 桌面/差异-2026-05.xlsx」。
 - 关键发现用一行带颜色的 chip 摘要:「⚠ 3 单金额不一致 · ✦ 2 单疑似退款」。
 - 三个动作按钮固定位置:「打开输出文件夹」「再做一次」「换一个文件」。
diff --git a/src/lib/promptQuality.test.ts b/src/lib/promptQuality.test.ts
index 4a12cad..fa8be57 100644
--- a/src/lib/promptQuality.test.ts
+++ b/src/lib/promptQuality.test.ts
@@ -15,16 +15,6 @@ describe('withDesktopQualityBar', () => {
     expect(second).toBe(first);
   });
 
-  it('compacts old repeated Chinese prompt endings', () => {
-    const first = withDesktopQualityBar(
-      '请做一个本地桌面工具。\n先给简短方案摘要,然后直接实现、运行和验证。中文。',
-      'zh'
-    );
-
-    expect(first).not.toContain('先给简短方案摘要');
-    expect(first).toContain('≤8 行摘要');
-  });
-
   it('adds the English quality bar once', () => {
     const first = withDesktopQualityBar('Build a local desktop tool.', 'en');
     const second = withDesktopQualityBar(first, 'en');
@@ -38,14 +28,9 @@ describe('withDesktopQualityBar', () => {
     expect(second).toBe(first);
   });
 
-  it('compacts old repeated English prompt endings', () => {
-    const first = withDesktopQualityBar(
-      'Build a local desktop tool.\nStart with a brief plan summary, then implement, run, and verify. English throughout.',
-      'en'
-    );
-
-    expect(first).not.toContain('brief plan summary');
-    expect(first).toContain('summarize in ≤8 lines');
+  it('collapses runs of 3+ blank lines into a single blank line', () => {
+    const wrapped = withDesktopQualityBar('请做一个本地桌面工具。\n\n\n\n', 'zh');
+    expect(wrapped).not.toMatch(/\n{3,}/);
   });
 
   it('places the final report schema at the very end of the tail (zh)', () => {
diff --git a/src/lib/promptQuality.ts b/src/lib/promptQuality.ts
index 3c14638..108e743 100644
--- a/src/lib/promptQuality.ts
+++ b/src/lib/promptQuality.ts
@@ -41,26 +41,17 @@ ${DOD_EN}
 
 ${FINAL_REPORT_EN}`;
 
-// ─── Legacy Cleanup ──────────────────────────────────────────────
-
-function compactLegacyBoilerplate(prompt: string): string {
-  return prompt
-    .replace(/\n?请先给\s*10\s*行以内方案摘要,然后直接实现、运行和验证。/g, '')
-    .replace(/\n?先给\s*(?:10\s*行以内|简短)方案摘要,然后直接实现、运行和验证。(?:全程)?中文(?:沟通)?。/g, '')
-    .replace(/\n?Start with a brief plan summary, then implement, run, and verify\.(?: English throughout\.)?/g, '')
-    .replace(/不确定的地方直接问我/g, '只有真实阻塞问题再问我')
-    .replace(/Ask me if unsure\./g, 'Ask only for true blockers.')
-    .replace(/请先确认方案再动手/g, '')
-    .replace(/Please confirm the plan before starting/g, '')
-    .replace(/\n{3,}/g, '\n\n')
-    .trim();
+// ─── Blank-line folding ──────────────────────────────────────────
+
+function dedupeBlankLines(prompt: string): string {
+  return prompt.replace(/\n{3,}/g, '\n\n').trim();
 }
 
 // ─── Public API ──────────────────────────────────────────────────
 
 export function withDesktopQualityBar(prompt: string, lang: PromptQualityLang): string {
   const marker = lang === 'zh' ? QUALITY_MARKER_ZH : QUALITY_MARKER_EN;
-  const compacted = compactLegacyBoilerplate(prompt);
+  const compacted = dedupeBlankLines(prompt);
   if (compacted.includes(marker)) return compacted;
 
   const tail = lang === 'zh' ? QUALITY_TAIL_ZH : QUALITY_TAIL_EN;