287 lines
8.3 KiB
TypeScript
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];
|
|
}
|