支持待确认事项逐条采纳
This commit is contained in:
@@ -47,6 +47,12 @@ button:disabled {
|
|||||||
opacity: 0.45;
|
opacity: 0.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.secondary-button {
|
||||||
|
background: #ffffff;
|
||||||
|
border-color: #cbd3df;
|
||||||
|
color: #172033;
|
||||||
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
select,
|
select,
|
||||||
textarea {
|
textarea {
|
||||||
@@ -69,6 +75,7 @@ label {
|
|||||||
}
|
}
|
||||||
|
|
||||||
label span,
|
label span,
|
||||||
|
.field-label,
|
||||||
h3 {
|
h3 {
|
||||||
color: #586174;
|
color: #586174;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
@@ -76,6 +83,11 @@ h3 {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field-group {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.app-shell {
|
.app-shell {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
@@ -161,6 +173,28 @@ h3 {
|
|||||||
font-weight: 700;
|
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 {
|
.status-pill {
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
@@ -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(["出海能力需要确认"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -16,7 +16,6 @@ const listFields = [
|
|||||||
"core_claims",
|
"core_claims",
|
||||||
"forbidden_claims",
|
"forbidden_claims",
|
||||||
"image_topics",
|
"image_topics",
|
||||||
"uncertain_items",
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const fieldLabels: Record<(typeof listFields)[number], string> = {
|
const fieldLabels: Record<(typeof listFields)[number], string> = {
|
||||||
@@ -26,9 +25,24 @@ const fieldLabels: Record<(typeof listFields)[number], string> = {
|
|||||||
core_claims: "核心事实/主张",
|
core_claims: "核心事实/主张",
|
||||||
forbidden_claims: "禁止使用的主张",
|
forbidden_claims: "禁止使用的主张",
|
||||||
image_topics: "图片主题",
|
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({
|
export function FactCardEditor({
|
||||||
factCard,
|
factCard,
|
||||||
isSaving,
|
isSaving,
|
||||||
@@ -60,11 +74,18 @@ export function FactCardEditor({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveItem(
|
||||||
|
itemIndex: number,
|
||||||
|
target: UncertainItemResolutionTarget,
|
||||||
|
) {
|
||||||
|
onChange(resolveUncertainItem(currentFactCard, itemIndex, target));
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel stack">
|
<section className="panel stack">
|
||||||
<div className="panel-heading">
|
<div className="panel-heading">
|
||||||
<span>事实卡</span>
|
<span>事实卡</span>
|
||||||
<button disabled={!canOptimize || isSaving} onClick={onConfirm}>
|
<button disabled={!canOptimize || isSaving} onClick={onConfirm} type="button">
|
||||||
{isSaving ? "保存中..." : "确认事实卡"}
|
{isSaving ? "保存中..." : "确认事实卡"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,6 +143,32 @@ export function FactCardEditor({
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
|
<div className="field-group">
|
||||||
|
<div className="field-label">待确认事项</div>
|
||||||
|
{factCard.uncertain_items.length > 0 ? (
|
||||||
|
<div className="uncertain-list">
|
||||||
|
{factCard.uncertain_items.map((item, index) => (
|
||||||
|
<div className="uncertain-row" key={`${item}-${index}`}>
|
||||||
|
<p>{item}</p>
|
||||||
|
<div className="uncertain-actions">
|
||||||
|
{uncertainItemActions.map((action) => (
|
||||||
|
<button
|
||||||
|
className="secondary-button"
|
||||||
|
key={action.target}
|
||||||
|
onClick={() => resolveItem(index, action.target)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{action.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="status-text pass">待确认事项已处理。</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{!canOptimize && (
|
{!canOptimize && (
|
||||||
<p className="status-text fail">
|
<p className="status-text fail">
|
||||||
请先处理待确认事项,再开始优化。
|
请先处理待确认事项,再开始优化。
|
||||||
@@ -141,3 +188,32 @@ export function toConfirmedFactCard(
|
|||||||
confirmed_by_user: true,
|
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];
|
||||||
|
}
|
||||||
|
|||||||
+14
-9
@@ -1,6 +1,8 @@
|
|||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
test("中文界面可以生成优化文章和导出链接", async ({ page }) => {
|
test("中文界面可以生成优化文章和导出链接", async ({ page }) => {
|
||||||
|
test.setTimeout(180_000);
|
||||||
|
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|
||||||
await page.getByLabel("访问密钥").fill("local-dev-key");
|
await page.getByLabel("访问密钥").fill("local-dev-key");
|
||||||
@@ -14,21 +16,24 @@ test("中文界面可以生成优化文章和导出链接", async ({ page }) =>
|
|||||||
await page.getByLabel("用户要求").fill("保持事实准确,语气自然。");
|
await page.getByLabel("用户要求").fill("保持事实准确,语气自然。");
|
||||||
|
|
||||||
await page.getByRole("button", { name: "分析文章" }).click();
|
await page.getByRole("button", { name: "分析文章" }).click();
|
||||||
await expect(page.getByText("候选事实卡已生成。")).toBeVisible();
|
await expect(page.getByText("候选事实卡已生成")).toBeVisible({
|
||||||
|
timeout: 70_000,
|
||||||
|
});
|
||||||
|
|
||||||
await page
|
const confirmUncertainItemButtons = page.getByRole("button", {
|
||||||
.locator("label")
|
name: "采纳为核心事实",
|
||||||
.filter({ hasText: "待确认事项" })
|
});
|
||||||
.locator("textarea")
|
while ((await confirmUncertainItemButtons.count()) > 0) {
|
||||||
.fill("");
|
await confirmUncertainItemButtons.first().click();
|
||||||
|
}
|
||||||
await page.getByRole("button", { name: "确认事实卡" }).click();
|
await page.getByRole("button", { name: "确认事实卡" }).click();
|
||||||
await expect(page.getByText("事实卡已确认。")).toBeVisible();
|
await expect(page.getByText("事实卡已确认。")).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole("button", { name: "开始优化" }).click();
|
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.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: "optimized.docx" })).toBeVisible();
|
||||||
await expect(page.getByRole("link", { name: "qa_report.json" })).toBeVisible();
|
await expect(page.getByRole("link", { name: "qa_report.json" })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user