Files
GEOAgentArticleOptimizer/src/components/fact-card-editor.tsx
T
2026-07-01 13:25:07 +08:00

287 lines
8.3 KiB
TypeScript

"use client";
import { useState } from "react";
import type { CandidateFactCard, OptimizationFactCard } from "../lib/domain/types";
interface FactCardEditorProps {
factCard: OptimizationFactCard | null;
isSaving: boolean;
onChange: (factCard: OptimizationFactCard) => void;
}
const listFields = [
"company_short_names",
"brand_names",
"product_names",
"core_claims",
"forbidden_claims",
"image_topics",
] as const;
const fieldLabels: Record<(typeof listFields)[number], string> = {
company_short_names: "公司简称",
brand_names: "品牌名称",
product_names: "产品名称",
core_claims: "核心事实/主张",
forbidden_claims: "禁止使用的主张",
image_topics: "图片主题",
};
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,
onChange,
}: FactCardEditorProps) {
const [isEditing, setIsEditing] = useState(false);
if (!factCard) {
return (
<section className="panel compact-panel fact-card-panel empty-panel">
<div className="panel-heading">
<span>事实卡</span>
<span className="status-pill warn">待生成</span>
</div>
</section>
);
}
const hasUncertainItems = factCard.uncertain_items.length > 0;
function update<K extends keyof OptimizationFactCard>(
key: K,
value: OptimizationFactCard[K],
) {
if (!factCard) return;
onChange({
...factCard,
[key]: value,
confirmed_by_user: false,
is_ready_for_optimization:
key === "uncertain_items"
? (value as string[]).length === 0
: factCard.uncertain_items.length === 0,
});
}
function resolveItem(
itemIndex: number,
target: UncertainItemResolutionTarget,
) {
if (!factCard) return;
onChange(resolveUncertainItem(factCard, itemIndex, target));
}
return (
<section className="panel compact-panel fact-card-panel stack">
<div className="panel-heading">
<span>事实卡</span>
<div className="panel-actions">
<span className={`status-pill ${hasUncertainItems ? "warn" : "pass"}`}>
{hasUncertainItems ? "待复核" : "可用"}
</span>
<button
className="secondary-button"
disabled={isSaving}
onClick={() => setIsEditing((editing) => !editing)}
type="button"
>
{isEditing ? "收起编辑" : "编辑事实卡"}
</button>
</div>
</div>
<div className="fact-summary-grid">
<div>
<span>公司</span>
<strong>{factCard.company_full_name || "待补充"}</strong>
</div>
<div>
<span>行业</span>
<strong>{factCard.target_industry || "待补充"}</strong>
</div>
<div>
<span>受众</span>
<strong>{factCard.target_audience || "待补充"}</strong>
</div>
<div>
<span>经验</span>
<strong>
{factCard.experience_years === null
? "待补充"
: `${factCard.experience_years} 年`}
</strong>
</div>
</div>
{factCard.core_claims.length > 0 && (
<div className="fact-compact-list">
<span className="field-label">核心事实</span>
<div className="tag-row">
{factCard.core_claims.slice(0, 4).map((claim) => (
<span className="tag" key={claim}>
{claim}
</span>
))}
</div>
</div>
)}
{hasUncertainItems && (
<div className="fact-compact-list">
<span className="field-label">待确认事项</span>
<div className="tag-row">
{factCard.uncertain_items.slice(0, 4).map((item) => (
<span className="tag warn-tag" key={item}>
{item}
</span>
))}
</div>
</div>
)}
{isEditing && (
<div className="fact-edit-grid">
<label>
<span>公司全称</span>
<input
disabled={isSaving}
value={factCard.company_full_name}
onChange={(event) =>
update("company_full_name", event.target.value)
}
/>
</label>
<div className="two-col">
<label>
<span>目标行业</span>
<input
disabled={isSaving}
value={factCard.target_industry}
onChange={(event) =>
update("target_industry", event.target.value)
}
/>
</label>
<label>
<span>目标受众</span>
<input
disabled={isSaving}
value={factCard.target_audience}
onChange={(event) =>
update("target_audience", event.target.value)
}
/>
</label>
</div>
<label>
<span>经验年限</span>
<input
disabled={isSaving}
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>{fieldLabels[field]}</span>
<textarea
disabled={isSaving}
value={factCard[field].join("\n")}
onChange={(event) =>
update(
field,
event.target.value
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean) as OptimizationFactCard[typeof field],
)
}
/>
</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"
disabled={isSaving}
key={action.target}
onClick={() => resolveItem(index, action.target)}
type="button"
>
{action.label}
</button>
))}
</div>
</div>
))}
</div>
) : (
<p className="status-text pass">待确认事项已处理。</p>
)}
</div>
</div>
)}
</section>
);
}
export function resolveUncertainItem<T extends CandidateFactCard>(
factCard: T,
itemIndex: number,
target: UncertainItemResolutionTarget,
): T {
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];
}