feat: localize optimizer interface
This commit is contained in:
+12
-12
@@ -55,12 +55,12 @@ export default function Home() {
|
|||||||
body: JSON.stringify(input),
|
body: JSON.stringify(input),
|
||||||
});
|
});
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
if (!response.ok) throw new Error(body.error ?? "Analyze failed");
|
if (!response.ok) throw new Error(body.error ?? "分析失败");
|
||||||
setJobId(body.job.id);
|
setJobId(body.job.id);
|
||||||
setFactCard(body.candidateFactCard);
|
setFactCard(body.candidateFactCard);
|
||||||
setMessage("Candidate fact card created.");
|
setMessage("候选事实卡已生成。");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setMessage(error instanceof Error ? error.message : "Analyze failed");
|
setMessage(error instanceof Error ? error.message : "分析失败");
|
||||||
} finally {
|
} finally {
|
||||||
setBusyAction(null);
|
setBusyAction(null);
|
||||||
}
|
}
|
||||||
@@ -77,11 +77,11 @@ export default function Home() {
|
|||||||
body: JSON.stringify(toConfirmedFactCard(factCard)),
|
body: JSON.stringify(toConfirmedFactCard(factCard)),
|
||||||
});
|
});
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
if (!response.ok) throw new Error(body.error ?? "Confirm failed");
|
if (!response.ok) throw new Error(body.error ?? "确认失败");
|
||||||
setFactCard(body.factCard);
|
setFactCard(body.factCard);
|
||||||
setMessage("Fact card confirmed.");
|
setMessage("事实卡已确认。");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setMessage(error instanceof Error ? error.message : "Confirm failed");
|
setMessage(error instanceof Error ? error.message : "确认失败");
|
||||||
} finally {
|
} finally {
|
||||||
setBusyAction(null);
|
setBusyAction(null);
|
||||||
}
|
}
|
||||||
@@ -96,16 +96,16 @@ export default function Home() {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
if (!response.ok) throw new Error(body.error ?? "Optimize failed");
|
if (!response.ok) throw new Error(body.error ?? "优化失败");
|
||||||
setOptimizedArticle(body.optimizedArticle);
|
setOptimizedArticle(body.optimizedArticle);
|
||||||
setQaReport(body.qaReport);
|
setQaReport(body.qaReport);
|
||||||
setMessage(
|
setMessage(
|
||||||
body.qaReport.overall_status === "fail"
|
body.qaReport.overall_status === "fail"
|
||||||
? "QA found hard failures; export is blocked."
|
? "质检发现硬性失败,已阻止导出。"
|
||||||
: "Optimization complete.",
|
: "优化完成。",
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setMessage(error instanceof Error ? error.message : "Optimize failed");
|
setMessage(error instanceof Error ? error.message : "优化失败");
|
||||||
} finally {
|
} finally {
|
||||||
setBusyAction(null);
|
setBusyAction(null);
|
||||||
}
|
}
|
||||||
@@ -115,7 +115,7 @@ export default function Home() {
|
|||||||
<main className="app-shell">
|
<main className="app-shell">
|
||||||
<header className="topbar">
|
<header className="topbar">
|
||||||
<div>
|
<div>
|
||||||
<h1>GEO Agent Article Optimizer</h1>
|
<h1>GEO 智能文章优化器</h1>
|
||||||
{message && <p>{message}</p>}
|
{message && <p>{message}</p>}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -123,7 +123,7 @@ export default function Home() {
|
|||||||
onClick={optimize}
|
onClick={optimize}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{busyAction === "optimize" ? "Optimizing..." : "Optimize"}
|
{busyAction === "optimize" ? "优化中..." : "开始优化"}
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<div className="workflow-grid">
|
<div className="workflow-grid">
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ interface ArticleInputFormProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const platforms: { value: PublishPlatform; label: string }[] = [
|
const platforms: { value: PublishPlatform; label: string }[] = [
|
||||||
{ value: "official_site", label: "Official site" },
|
{ value: "official_site", label: "官网文章" },
|
||||||
{ value: "media_article", label: "Media article" },
|
{ value: "media_article", label: "媒体稿" },
|
||||||
{ value: "comparison_review", label: "Comparison review" },
|
{ value: "comparison_review", label: "对比评测" },
|
||||||
{ value: "recommendation_list", label: "Recommendation list" },
|
{ value: "recommendation_list", label: "推荐榜单" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function ArticleInputForm({
|
export function ArticleInputForm({
|
||||||
@@ -47,13 +47,13 @@ export function ArticleInputForm({
|
|||||||
return (
|
return (
|
||||||
<form className="panel stack" onSubmit={handleSubmit}>
|
<form className="panel stack" onSubmit={handleSubmit}>
|
||||||
<div className="panel-heading">
|
<div className="panel-heading">
|
||||||
<span>Article Input</span>
|
<span>文章输入</span>
|
||||||
<button disabled={isSubmitting} type="submit">
|
<button disabled={isSubmitting} type="submit">
|
||||||
{isSubmitting ? "Analyzing..." : "Analyze"}
|
{isSubmitting ? "分析中..." : "分析文章"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<label>
|
<label>
|
||||||
<span>Title</span>
|
<span>标题</span>
|
||||||
<input
|
<input
|
||||||
required
|
required
|
||||||
value={value.title}
|
value={value.title}
|
||||||
@@ -61,7 +61,7 @@ export function ArticleInputForm({
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>Body</span>
|
<span>正文</span>
|
||||||
<textarea
|
<textarea
|
||||||
required
|
required
|
||||||
className="body-input"
|
className="body-input"
|
||||||
@@ -70,14 +70,14 @@ export function ArticleInputForm({
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>Images</span>
|
<span>图片描述或图片链接</span>
|
||||||
<textarea
|
<textarea
|
||||||
value={value.image_lines}
|
value={value.image_lines}
|
||||||
onChange={(event) => update("image_lines", event.target.value)}
|
onChange={(event) => update("image_lines", event.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>Target platform</span>
|
<span>目标平台</span>
|
||||||
<select
|
<select
|
||||||
value={value.platform}
|
value={value.platform}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
@@ -92,7 +92,7 @@ export function ArticleInputForm({
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>User instructions</span>
|
<span>用户要求</span>
|
||||||
<textarea
|
<textarea
|
||||||
value={value.user_instructions}
|
value={value.user_instructions}
|
||||||
onChange={(event) => update("user_instructions", event.target.value)}
|
onChange={(event) => update("user_instructions", event.target.value)}
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ const listFields = [
|
|||||||
"uncertain_items",
|
"uncertain_items",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
const fieldLabels: Record<(typeof listFields)[number], string> = {
|
||||||
|
company_short_names: "公司简称",
|
||||||
|
brand_names: "品牌名称",
|
||||||
|
product_names: "产品名称",
|
||||||
|
core_claims: "核心事实/主张",
|
||||||
|
forbidden_claims: "禁止使用的主张",
|
||||||
|
image_topics: "图片主题",
|
||||||
|
uncertain_items: "待确认事项",
|
||||||
|
};
|
||||||
|
|
||||||
export function FactCardEditor({
|
export function FactCardEditor({
|
||||||
factCard,
|
factCard,
|
||||||
isSaving,
|
isSaving,
|
||||||
@@ -28,7 +38,7 @@ export function FactCardEditor({
|
|||||||
if (!factCard) {
|
if (!factCard) {
|
||||||
return (
|
return (
|
||||||
<section className="panel empty-panel">
|
<section className="panel empty-panel">
|
||||||
<div className="panel-heading">Fact Card</div>
|
<div className="panel-heading">事实卡</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -53,13 +63,13 @@ export function FactCardEditor({
|
|||||||
return (
|
return (
|
||||||
<section className="panel stack">
|
<section className="panel stack">
|
||||||
<div className="panel-heading">
|
<div className="panel-heading">
|
||||||
<span>Fact Card</span>
|
<span>事实卡</span>
|
||||||
<button disabled={!canOptimize || isSaving} onClick={onConfirm}>
|
<button disabled={!canOptimize || isSaving} onClick={onConfirm}>
|
||||||
{isSaving ? "Saving..." : "Confirm"}
|
{isSaving ? "保存中..." : "确认事实卡"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<label>
|
<label>
|
||||||
<span>Company full name</span>
|
<span>公司全称</span>
|
||||||
<input
|
<input
|
||||||
value={factCard.company_full_name}
|
value={factCard.company_full_name}
|
||||||
onChange={(event) => update("company_full_name", event.target.value)}
|
onChange={(event) => update("company_full_name", event.target.value)}
|
||||||
@@ -67,14 +77,14 @@ export function FactCardEditor({
|
|||||||
</label>
|
</label>
|
||||||
<div className="two-col">
|
<div className="two-col">
|
||||||
<label>
|
<label>
|
||||||
<span>Target industry</span>
|
<span>目标行业</span>
|
||||||
<input
|
<input
|
||||||
value={factCard.target_industry}
|
value={factCard.target_industry}
|
||||||
onChange={(event) => update("target_industry", event.target.value)}
|
onChange={(event) => update("target_industry", event.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<span>Target audience</span>
|
<span>目标受众</span>
|
||||||
<input
|
<input
|
||||||
value={factCard.target_audience}
|
value={factCard.target_audience}
|
||||||
onChange={(event) => update("target_audience", event.target.value)}
|
onChange={(event) => update("target_audience", event.target.value)}
|
||||||
@@ -82,7 +92,7 @@ export function FactCardEditor({
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<label>
|
<label>
|
||||||
<span>Experience years</span>
|
<span>经验年限</span>
|
||||||
<input
|
<input
|
||||||
min="0"
|
min="0"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -97,7 +107,7 @@ export function FactCardEditor({
|
|||||||
</label>
|
</label>
|
||||||
{listFields.map((field) => (
|
{listFields.map((field) => (
|
||||||
<label key={field}>
|
<label key={field}>
|
||||||
<span>{field.replace(/_/g, " ")}</span>
|
<span>{fieldLabels[field]}</span>
|
||||||
<textarea
|
<textarea
|
||||||
value={factCard[field].join("\n")}
|
value={factCard[field].join("\n")}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
@@ -114,7 +124,7 @@ export function FactCardEditor({
|
|||||||
))}
|
))}
|
||||||
{!canOptimize && (
|
{!canOptimize && (
|
||||||
<p className="status-text fail">
|
<p className="status-text fail">
|
||||||
Resolve uncertain items before optimization.
|
请先处理待确认事项,再开始优化。
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -18,21 +18,21 @@ export function OptimizedPreview({
|
|||||||
if (!article) {
|
if (!article) {
|
||||||
return (
|
return (
|
||||||
<section className="panel empty-panel">
|
<section className="panel empty-panel">
|
||||||
<div className="panel-heading">Optimized Result</div>
|
<div className="panel-heading">优化结果</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel stack">
|
<section className="panel stack">
|
||||||
<div className="panel-heading">Optimized Result</div>
|
<div className="panel-heading">优化结果</div>
|
||||||
<article className="preview">
|
<article className="preview">
|
||||||
<h2>{article.title}</h2>
|
<h2>{article.title}</h2>
|
||||||
<p>{article.summary}</p>
|
<p>{article.summary}</p>
|
||||||
<pre>{article.body_markdown}</pre>
|
<pre>{article.body_markdown}</pre>
|
||||||
</article>
|
</article>
|
||||||
<div>
|
<div>
|
||||||
<h3>Image suggestions</h3>
|
<h3>图片建议</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{article.image_suggestions.map((item) => (
|
{article.image_suggestions.map((item) => (
|
||||||
<li key={item.source}>
|
<li key={item.source}>
|
||||||
@@ -42,7 +42,7 @@ export function OptimizedPreview({
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3>Changed sections</h3>
|
<h3>修改位置</h3>
|
||||||
<div className="tag-row">
|
<div className="tag-row">
|
||||||
{article.changed_sections.map((section) => (
|
{article.changed_sections.map((section) => (
|
||||||
<span className="tag" key={section}>
|
<span className="tag" key={section}>
|
||||||
@@ -53,7 +53,7 @@ export function OptimizedPreview({
|
|||||||
</div>
|
</div>
|
||||||
{article.requires_user_confirmation.length > 0 && (
|
{article.requires_user_confirmation.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h3>Needs confirmation</h3>
|
<h3>需要人工确认</h3>
|
||||||
<ul>
|
<ul>
|
||||||
{article.requires_user_confirmation.map((item) => (
|
{article.requires_user_confirmation.map((item) => (
|
||||||
<li key={item}>{item}</li>
|
<li key={item}>{item}</li>
|
||||||
|
|||||||
@@ -6,11 +6,17 @@ interface QaReportPanelProps {
|
|||||||
report: QaReport | null;
|
report: QaReport | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const statusLabels = {
|
||||||
|
pass: "通过",
|
||||||
|
warn: "警告",
|
||||||
|
fail: "失败",
|
||||||
|
} as const;
|
||||||
|
|
||||||
export function QaReportPanel({ report }: QaReportPanelProps) {
|
export function QaReportPanel({ report }: QaReportPanelProps) {
|
||||||
if (!report) {
|
if (!report) {
|
||||||
return (
|
return (
|
||||||
<section className="panel empty-panel">
|
<section className="panel empty-panel">
|
||||||
<div className="panel-heading">Quality Report</div>
|
<div className="panel-heading">质量报告</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -18,9 +24,9 @@ export function QaReportPanel({ report }: QaReportPanelProps) {
|
|||||||
return (
|
return (
|
||||||
<section className="panel stack">
|
<section className="panel stack">
|
||||||
<div className="panel-heading">
|
<div className="panel-heading">
|
||||||
<span>Quality Report</span>
|
<span>质量报告</span>
|
||||||
<span className={`status-pill ${report.overall_status}`}>
|
<span className={`status-pill ${report.overall_status}`}>
|
||||||
{report.overall_status}
|
{statusLabels[report.overall_status]}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="qa-list">
|
<div className="qa-list">
|
||||||
@@ -29,7 +35,7 @@ export function QaReportPanel({ report }: QaReportPanelProps) {
|
|||||||
<div className="qa-title-row">
|
<div className="qa-title-row">
|
||||||
<strong>{check.rule_id.replace(/_/g, " ")}</strong>
|
<strong>{check.rule_id.replace(/_/g, " ")}</strong>
|
||||||
<span className={`status-pill ${check.status}`}>
|
<span className={`status-pill ${check.status}`}>
|
||||||
{check.status}
|
{statusLabels[check.status]}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p>{check.reason}</p>
|
<p>{check.reason}</p>
|
||||||
|
|||||||
+14
-14
@@ -1,31 +1,31 @@
|
|||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
test("happy path creates optimized article and export links", async ({ page }) => {
|
test("中文界面可以生成优化文章和导出链接", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|
||||||
await page.getByLabel("Title").fill("Example Technology Co., Ltd. GEO guide");
|
await page.getByLabel("标题").fill("Example Technology Co., Ltd. GEO 指南");
|
||||||
await page
|
await page
|
||||||
.getByLabel("Body")
|
.getByLabel("正文")
|
||||||
.fill(
|
.fill(
|
||||||
"Example Technology Co., Ltd. has 8 years of GEO optimization experience. Example GEO helps marketing teams improve content structure.",
|
"Example Technology Co., Ltd. has 8 years of GEO optimization experience. Example GEO 帮助市场团队优化内容结构。",
|
||||||
);
|
);
|
||||||
await page.getByLabel("Images").fill("Product dashboard");
|
await page.getByLabel("图片描述或图片链接").fill("产品仪表盘截图");
|
||||||
await page.getByLabel("User instructions").fill("Keep factual");
|
await page.getByLabel("用户要求").fill("保持事实准确,语气自然。");
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Analyze" }).click();
|
await page.getByRole("button", { name: "分析文章" }).click();
|
||||||
await expect(page.getByText("Candidate fact card created.")).toBeVisible();
|
await expect(page.getByText("候选事实卡已生成。")).toBeVisible();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.locator("label")
|
.locator("label")
|
||||||
.filter({ hasText: "uncertain items" })
|
.filter({ hasText: "待确认事项" })
|
||||||
.locator("textarea")
|
.locator("textarea")
|
||||||
.fill("");
|
.fill("");
|
||||||
await page.getByRole("button", { name: "Confirm" }).click();
|
await page.getByRole("button", { name: "确认事实卡" }).click();
|
||||||
await expect(page.getByText("Fact card confirmed.")).toBeVisible();
|
await expect(page.getByText("事实卡已确认。")).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Optimize" }).click();
|
await page.getByRole("button", { name: "开始优化" }).click();
|
||||||
await expect(page.getByText("Optimization complete.")).toBeVisible();
|
await expect(page.getByText("优化完成。")).toBeVisible();
|
||||||
await expect(page.getByText("Quality Report")).toBeVisible();
|
await expect(page.getByText("质量报告")).toBeVisible();
|
||||||
|
|
||||||
await expect(page.getByRole("link", { name: "optimized.md" })).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();
|
||||||
|
|||||||
Reference in New Issue
Block a user