feat: localize optimizer interface

This commit is contained in:
Codex
2026-06-21 23:43:02 +08:00
parent 1298456683
commit 1596de4527
6 changed files with 72 additions and 56 deletions
+12 -12
View File
@@ -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">
+11 -11
View File
@@ -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)}
+20 -10
View File
@@ -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,9 +107,9 @@ 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) =>
update( update(
field, field,
@@ -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>
+5 -5
View File
@@ -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>
+10 -4
View File
@@ -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
View File
@@ -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();