feat: build local optimizer interface

This commit is contained in:
Codex
2026-06-21 23:42:37 +08:00
parent dd6480a8be
commit 67f19f8550
6 changed files with 745 additions and 22 deletions
+103
View File
@@ -0,0 +1,103 @@
"use client";
import type { FormEvent } from "react";
import type { PublishPlatform } from "../lib/domain/types";
export interface ArticleInputPayload {
title: string;
body: string;
image_lines: string;
platform: PublishPlatform;
user_instructions: string;
}
interface ArticleInputFormProps {
value: ArticleInputPayload;
isSubmitting: boolean;
onChange: (value: ArticleInputPayload) => void;
onSubmit: () => void;
}
const platforms: { value: PublishPlatform; label: string }[] = [
{ value: "official_site", label: "Official site" },
{ value: "media_article", label: "Media article" },
{ value: "comparison_review", label: "Comparison review" },
{ value: "recommendation_list", label: "Recommendation list" },
];
export function ArticleInputForm({
value,
isSubmitting,
onChange,
onSubmit,
}: ArticleInputFormProps) {
function update<K extends keyof ArticleInputPayload>(
key: K,
nextValue: ArticleInputPayload[K],
) {
onChange({ ...value, [key]: nextValue });
}
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
onSubmit();
}
return (
<form className="panel stack" onSubmit={handleSubmit}>
<div className="panel-heading">
<span>Article Input</span>
<button disabled={isSubmitting} type="submit">
{isSubmitting ? "Analyzing..." : "Analyze"}
</button>
</div>
<label>
<span>Title</span>
<input
required
value={value.title}
onChange={(event) => update("title", event.target.value)}
/>
</label>
<label>
<span>Body</span>
<textarea
required
className="body-input"
value={value.body}
onChange={(event) => update("body", event.target.value)}
/>
</label>
<label>
<span>Images</span>
<textarea
value={value.image_lines}
onChange={(event) => update("image_lines", event.target.value)}
/>
</label>
<label>
<span>Target platform</span>
<select
value={value.platform}
onChange={(event) =>
update("platform", event.target.value as PublishPlatform)
}
>
{platforms.map((platform) => (
<option key={platform.value} value={platform.value}>
{platform.label}
</option>
))}
</select>
</label>
<label>
<span>User instructions</span>
<textarea
value={value.user_instructions}
onChange={(event) => update("user_instructions", event.target.value)}
/>
</label>
</form>
);
}
+133
View File
@@ -0,0 +1,133 @@
"use client";
import type { CandidateFactCard, ConfirmedFactCard } from "../lib/domain/types";
interface FactCardEditorProps {
factCard: CandidateFactCard | null;
isSaving: boolean;
onChange: (factCard: CandidateFactCard) => void;
onConfirm: () => void;
}
const listFields = [
"company_short_names",
"brand_names",
"product_names",
"core_claims",
"forbidden_claims",
"image_topics",
"uncertain_items",
] as const;
export function FactCardEditor({
factCard,
isSaving,
onChange,
onConfirm,
}: FactCardEditorProps) {
if (!factCard) {
return (
<section className="panel empty-panel">
<div className="panel-heading">Fact Card</div>
</section>
);
}
const currentFactCard = factCard;
const canOptimize = currentFactCard.uncertain_items.length === 0;
function update<K extends keyof CandidateFactCard>(
key: K,
value: CandidateFactCard[K],
) {
onChange({
...currentFactCard,
[key]: value,
is_ready_for_optimization:
key === "uncertain_items"
? (value as string[]).length === 0
: currentFactCard.uncertain_items.length === 0,
});
}
return (
<section className="panel stack">
<div className="panel-heading">
<span>Fact Card</span>
<button disabled={!canOptimize || isSaving} onClick={onConfirm}>
{isSaving ? "Saving..." : "Confirm"}
</button>
</div>
<label>
<span>Company full name</span>
<input
value={factCard.company_full_name}
onChange={(event) => update("company_full_name", event.target.value)}
/>
</label>
<div className="two-col">
<label>
<span>Target industry</span>
<input
value={factCard.target_industry}
onChange={(event) => update("target_industry", event.target.value)}
/>
</label>
<label>
<span>Target audience</span>
<input
value={factCard.target_audience}
onChange={(event) => update("target_audience", event.target.value)}
/>
</label>
</div>
<label>
<span>Experience years</span>
<input
min="0"
type="number"
value={factCard.experience_years ?? ""}
onChange={(event) =>
update(
"experience_years",
event.target.value ? Number(event.target.value) : null,
)
}
/>
</label>
{listFields.map((field) => (
<label key={field}>
<span>{field.replace(/_/g, " ")}</span>
<textarea
value={factCard[field].join("\n")}
onChange={(event) =>
update(
field,
event.target.value
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean) as CandidateFactCard[typeof field],
)
}
/>
</label>
))}
{!canOptimize && (
<p className="status-text fail">
Resolve uncertain items before optimization.
</p>
)}
</section>
);
}
export function toConfirmedFactCard(
factCard: CandidateFactCard,
): ConfirmedFactCard {
return {
...factCard,
uncertain_items: [],
is_ready_for_optimization: true,
confirmed_by_user: true,
};
}
+83
View File
@@ -0,0 +1,83 @@
"use client";
import type { OptimizedArticle } from "../lib/domain/types";
interface OptimizedPreviewProps {
article: OptimizedArticle | null;
jobId: string | null;
exportBlocked: boolean;
}
const exportFiles = ["optimized.md", "optimized.docx", "qa_report.json"];
export function OptimizedPreview({
article,
jobId,
exportBlocked,
}: OptimizedPreviewProps) {
if (!article) {
return (
<section className="panel empty-panel">
<div className="panel-heading">Optimized Result</div>
</section>
);
}
return (
<section className="panel stack">
<div className="panel-heading">Optimized Result</div>
<article className="preview">
<h2>{article.title}</h2>
<p>{article.summary}</p>
<pre>{article.body_markdown}</pre>
</article>
<div>
<h3>Image suggestions</h3>
<ul>
{article.image_suggestions.map((item) => (
<li key={item.source}>
<strong>{item.source}</strong>: {item.suggestion}
</li>
))}
</ul>
</div>
<div>
<h3>Changed sections</h3>
<div className="tag-row">
{article.changed_sections.map((section) => (
<span className="tag" key={section}>
{section}
</span>
))}
</div>
</div>
{article.requires_user_confirmation.length > 0 && (
<div>
<h3>Needs confirmation</h3>
<ul>
{article.requires_user_confirmation.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
)}
<div className="export-row">
{exportFiles.map((fileName) =>
jobId && !exportBlocked ? (
<a
className="download-link"
href={`/api/jobs/${jobId}/exports/${fileName}`}
key={fileName}
>
{fileName}
</a>
) : (
<button disabled key={fileName} type="button">
{fileName}
</button>
),
)}
</div>
</section>
);
}
+44
View File
@@ -0,0 +1,44 @@
"use client";
import type { QaReport } from "../lib/domain/types";
interface QaReportPanelProps {
report: QaReport | null;
}
export function QaReportPanel({ report }: QaReportPanelProps) {
if (!report) {
return (
<section className="panel empty-panel">
<div className="panel-heading">Quality Report</div>
</section>
);
}
return (
<section className="panel stack">
<div className="panel-heading">
<span>Quality Report</span>
<span className={`status-pill ${report.overall_status}`}>
{report.overall_status}
</span>
</div>
<div className="qa-list">
{report.checks.map((check) => (
<div className="qa-item" key={check.rule_id}>
<div className="qa-title-row">
<strong>{check.rule_id.replace(/_/g, " ")}</strong>
<span className={`status-pill ${check.status}`}>
{check.status}
</span>
</div>
<p>{check.reason}</p>
<small>{check.evidence}</small>
{check.suggested_fix && <em>{check.suggested_fix}</em>}
{check.target_agent && <code>{check.target_agent}</code>}
</div>
))}
</div>
</section>
);
}