From b45380049445a1b7058d8032d1dc6b31a56386ae Mon Sep 17 00:00:00 2001 From: czj <13261895355@163.com> Date: Thu, 25 Jun 2026 13:51:41 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=BE=85=E7=A1=AE=E8=AE=A4?= =?UTF-8?q?=E4=BA=8B=E9=A1=B9=E9=80=90=E6=9D=A1=E9=87=87=E7=BA=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/globals.css | 34 ++++++++ .../__tests__/fact-card-editor.test.ts | 56 +++++++++++++ src/components/fact-card-editor.tsx | 82 ++++++++++++++++++- tests/e2e/mvp.spec.ts | 23 ++++-- 4 files changed, 183 insertions(+), 12 deletions(-) create mode 100644 src/components/__tests__/fact-card-editor.test.ts diff --git a/src/app/globals.css b/src/app/globals.css index 07a96d4..963a217 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -47,6 +47,12 @@ button:disabled { opacity: 0.45; } +.secondary-button { + background: #ffffff; + border-color: #cbd3df; + color: #172033; +} + input, select, textarea { @@ -69,6 +75,7 @@ label { } label span, +.field-label, h3 { color: #586174; font-size: 0.82rem; @@ -76,6 +83,11 @@ h3 { text-transform: uppercase; } +.field-group { + display: grid; + gap: 0.5rem; +} + .app-shell { display: grid; gap: 1rem; @@ -161,6 +173,28 @@ h3 { font-weight: 700; } +.uncertain-list { + display: grid; + gap: 0.65rem; +} + +.uncertain-row { + border-top: 1px solid #e5e9f0; + display: grid; + gap: 0.5rem; + padding-top: 0.65rem; +} + +.uncertain-row p { + margin: 0; +} + +.uncertain-actions { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; +} + .status-pill { border-radius: 999px; display: inline-flex; diff --git a/src/components/__tests__/fact-card-editor.test.ts b/src/components/__tests__/fact-card-editor.test.ts new file mode 100644 index 0000000..18f5d27 --- /dev/null +++ b/src/components/__tests__/fact-card-editor.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; + +import type { CandidateFactCard } from "../../lib/domain/types"; +import { resolveUncertainItem } from "../fact-card-editor"; + +const baseFactCard: CandidateFactCard = { + company_full_name: "示例科技有限公司", + company_short_names: ["示例科技"], + brand_names: ["示例品牌"], + product_names: ["GEO内容优化平台"], + target_industry: "GEO内容优化", + target_audience: "市场团队", + experience_years: 8, + core_claims: ["提供GEO内容优化服务"], + forbidden_claims: [], + image_topics: ["产品后台截图"], + uncertain_items: ["客户案例缺少来源", "出海能力需要确认"], + is_ready_for_optimization: false, +}; + +describe("FactCardEditor uncertain item resolution", () => { + it("adds a confirmed uncertain item to the selected fact field and removes it", () => { + const resolved = resolveUncertainItem(baseFactCard, 0, "core_claims"); + + expect(resolved.core_claims).toEqual([ + "提供GEO内容优化服务", + "客户案例缺少来源", + ]); + expect(resolved.uncertain_items).toEqual(["出海能力需要确认"]); + expect(resolved.is_ready_for_optimization).toBe(false); + }); + + it("marks the fact card ready after the final uncertain item is resolved", () => { + const resolved = resolveUncertainItem( + { + ...baseFactCard, + uncertain_items: ["图片主题需要确认"], + }, + 0, + "image_topics", + ); + + expect(resolved.image_topics).toEqual(["产品后台截图", "图片主题需要确认"]); + expect(resolved.uncertain_items).toEqual([]); + expect(resolved.is_ready_for_optimization).toBe(true); + }); + + it("can ignore an uncertain item without adding it to fact fields", () => { + const resolved = resolveUncertainItem(baseFactCard, 0, "ignore"); + + expect(resolved.core_claims).toEqual(["提供GEO内容优化服务"]); + expect(resolved.forbidden_claims).toEqual([]); + expect(resolved.image_topics).toEqual(["产品后台截图"]); + expect(resolved.uncertain_items).toEqual(["出海能力需要确认"]); + }); +}); diff --git a/src/components/fact-card-editor.tsx b/src/components/fact-card-editor.tsx index 4a5b54c..077ab67 100644 --- a/src/components/fact-card-editor.tsx +++ b/src/components/fact-card-editor.tsx @@ -16,7 +16,6 @@ const listFields = [ "core_claims", "forbidden_claims", "image_topics", - "uncertain_items", ] as const; const fieldLabels: Record<(typeof listFields)[number], string> = { @@ -26,9 +25,24 @@ const fieldLabels: Record<(typeof listFields)[number], string> = { core_claims: "核心事实/主张", forbidden_claims: "禁止使用的主张", image_topics: "图片主题", - uncertain_items: "待确认事项", }; +type UncertainItemResolutionTarget = + | "core_claims" + | "forbidden_claims" + | "image_topics" + | "ignore"; + +const uncertainItemActions: { + label: string; + target: UncertainItemResolutionTarget; +}[] = [ + { label: "采纳为核心事实", target: "core_claims" }, + { label: "标记为禁止主张", target: "forbidden_claims" }, + { label: "采纳为图片主题", target: "image_topics" }, + { label: "忽略", target: "ignore" }, +]; + export function FactCardEditor({ factCard, isSaving, @@ -60,11 +74,18 @@ export function FactCardEditor({ }); } + function resolveItem( + itemIndex: number, + target: UncertainItemResolutionTarget, + ) { + onChange(resolveUncertainItem(currentFactCard, itemIndex, target)); + } + return (
事实卡 -
@@ -122,6 +143,32 @@ export function FactCardEditor({ /> ))} +
+
待确认事项
+ {factCard.uncertain_items.length > 0 ? ( +
+ {factCard.uncertain_items.map((item, index) => ( +
+

{item}

+
+ {uncertainItemActions.map((action) => ( + + ))} +
+
+ ))} +
+ ) : ( +

待确认事项已处理。

+ )} +
{!canOptimize && (

请先处理待确认事项,再开始优化。 @@ -141,3 +188,32 @@ export function toConfirmedFactCard( confirmed_by_user: true, }; } + +export function resolveUncertainItem( + factCard: CandidateFactCard, + itemIndex: number, + target: UncertainItemResolutionTarget, +): CandidateFactCard { + const item = factCard.uncertain_items[itemIndex]?.trim(); + if (!item) return factCard; + + const uncertainItems = factCard.uncertain_items.filter( + (_, index) => index !== itemIndex, + ); + const nextFactCard = { + ...factCard, + uncertain_items: uncertainItems, + is_ready_for_optimization: uncertainItems.length === 0, + }; + + if (target === "ignore") return nextFactCard; + + return { + ...nextFactCard, + [target]: appendUniqueLine(nextFactCard[target], item), + }; +} + +function appendUniqueLine(values: string[], value: string) { + return values.includes(value) ? values : [...values, value]; +} diff --git a/tests/e2e/mvp.spec.ts b/tests/e2e/mvp.spec.ts index 57fc508..4927d4f 100644 --- a/tests/e2e/mvp.spec.ts +++ b/tests/e2e/mvp.spec.ts @@ -1,6 +1,8 @@ import { expect, test } from "@playwright/test"; test("中文界面可以生成优化文章和导出链接", async ({ page }) => { + test.setTimeout(180_000); + await page.goto("/"); await page.getByLabel("访问密钥").fill("local-dev-key"); @@ -14,21 +16,24 @@ test("中文界面可以生成优化文章和导出链接", async ({ page }) => await page.getByLabel("用户要求").fill("保持事实准确,语气自然。"); await page.getByRole("button", { name: "分析文章" }).click(); - await expect(page.getByText("候选事实卡已生成。")).toBeVisible(); + await expect(page.getByText("候选事实卡已生成")).toBeVisible({ + timeout: 70_000, + }); - await page - .locator("label") - .filter({ hasText: "待确认事项" }) - .locator("textarea") - .fill(""); + const confirmUncertainItemButtons = page.getByRole("button", { + name: "采纳为核心事实", + }); + while ((await confirmUncertainItemButtons.count()) > 0) { + await confirmUncertainItemButtons.first().click(); + } await page.getByRole("button", { name: "确认事实卡" }).click(); await expect(page.getByText("事实卡已确认。")).toBeVisible(); await page.getByRole("button", { name: "开始优化" }).click(); - await expect(page.getByText("优化完成。")).toBeVisible(); + await expect(page.getByRole("link", { name: "optimized.md" })).toBeVisible({ + timeout: 120_000, + }); await expect(page.getByText("质量报告")).toBeVisible(); - - await expect(page.getByRole("link", { name: "optimized.md" })).toBeVisible(); await expect(page.getByRole("link", { name: "optimized.docx" })).toBeVisible(); await expect(page.getByRole("link", { name: "qa_report.json" })).toBeVisible(); });