fix(web): 链相位语义色/失败可重试 + 模板填入中文标签/删除可撤销/工具卡文案 + 模板库 PageHeader

This commit is contained in:
Yaojia Wang
2026-06-30 08:56:23 +02:00
parent 6bb3559e25
commit 164e98887e
10 changed files with 235 additions and 74 deletions

View File

@@ -8,8 +8,10 @@ import { ConflictAdjudication } from "@/components/generation/ConflictAdjudicati
import { Button } from "@/components/ui/Button";
import { Badge } from "@/components/ui/Badge";
import { Field } from "@/components/ui/Field";
import { StatusNote } from "@/components/ui/StatusNote";
import { TextArea } from "@/components/ui/TextArea";
import { TextInput } from "@/components/ui/TextInput";
import { ThinkingIndicator } from "@/components/ThinkingIndicator";
import type { ToolDescriptorView, ToolInputFieldView } from "@/lib/api/types";
import {
buildGenerateRequest,
@@ -50,11 +52,17 @@ export function GeneratorRunner({
setValues((prev) => ({ ...prev, [name]: value }));
}, []);
// 「从模板填入」的目标字段brief / text无则不渲染
// 「从模板填入」的目标字段brief / text无则不渲染匹配按 name展示用中文 label。
const fillTarget = useMemo(
() => templateFillTarget(tool.input_fields),
[tool.input_fields],
);
const fillLabel = useMemo(
() =>
(tool.input_fields ?? []).find((f) => f.name === fillTarget)?.label ??
fillTarget,
[tool.input_fields, fillTarget],
);
const onGenerate = useCallback(async (): Promise<void> => {
const missing = missingRequiredFields(tool.input_fields, values);
@@ -150,7 +158,11 @@ export function GeneratorRunner({
void onGenerate();
}}
>
<TemplateFiller targetField={fillTarget} onFill={setField} />
<TemplateFiller
targetField={fillTarget}
targetLabel={fillLabel}
onFill={setField}
/>
{(tool.input_fields ?? []).map((field) => (
<FormField
key={field.name}
@@ -163,6 +175,12 @@ export function GeneratorRunner({
<Sparkles className="h-4 w-4" aria-hidden="true" />
{generating ? "生成中…" : "生成"}
</Button>
{generating ? (
<ThinkingIndicator
label="生成中,长文可能需要十几秒…"
className="text-cinnabar"
/>
) : null}
</form>
{gen.genStatus === "preview" && gen.preview ? (
@@ -213,9 +231,9 @@ export function GeneratorRunner({
) : null}
{gen.ingestStatus === "done" ? (
<p className="text-sm text-ink-soft">
{gen.created.length}
</p>
<StatusNote variant="success" title={`已入库 ${gen.created.length}`}>
<p></p>
</StatusNote>
) : null}
</div>
) : null}

View File

@@ -9,15 +9,21 @@ import type { TemplateResponse } from "@/lib/api/types";
import { buttonClass } from "@/lib/ui/variants";
interface TemplateFillerProps {
// 可填入的目标输入字段名(如 brief / text空 → 不渲染(无可填字段)。
// 可填入的目标输入字段名(如 brief / text空 → 不渲染(无可填字段)。匹配/回写按此 name。
targetField: string | null;
// 目标字段的中文展示名(如「需求」「原文」);缺省回退到 name。仅用于展示不暴露英文 key。
targetLabel?: string | null;
// 选定模板 → 把其 body 填进目标字段(纯前端,不改生成器后端)。
onFill: (field: string, body: string) => void;
}
// 「从模板填入」F3纯前端进场懒加载模板库选一个把 body 填进生成器的 brief/原文输入。
// 与生成器后端解耦——只读 /templates 并回写本地表单值。无模板/无目标字段则不渲染。
export function TemplateFiller({ targetField, onFill }: TemplateFillerProps) {
export function TemplateFiller({
targetField,
targetLabel,
onFill,
}: TemplateFillerProps) {
const toast = useToast();
const [templates, setTemplates] = useState<TemplateResponse[] | null>(null);
const [open, setOpen] = useState(false);
@@ -43,6 +49,7 @@ export function TemplateFiller({ targetField, onFill }: TemplateFillerProps) {
if (!targetField) return null;
const field = targetField;
const fieldLabel = targetLabel ?? targetField;
if (!open) {
return (
@@ -63,7 +70,7 @@ export function TemplateFiller({ targetField, onFill }: TemplateFillerProps) {
aria-label="从模板填入"
>
<div className="flex items-center justify-between">
<span className="text-sm text-ink-soft">{field}</span>
<span className="text-sm text-ink-soft">{fieldLabel}</span>
<Button
variant="ghost"
size="sm"

View File

@@ -4,6 +4,7 @@ import { ArrowRight, Database, Sparkles } from "lucide-react";
import { Badge } from "@/components/ui/Badge";
import type { ToolDescriptorView } from "@/lib/api/types";
import { isLegacyTool } from "@/lib/toolbox/toolbox";
import { cardClass } from "@/lib/ui/variants";
interface ToolCardProps {
@@ -15,12 +16,17 @@ interface ToolCardProps {
// 工具箱卡片(对齐 ProjectCard 纸感视觉):标题 + 副标题 + 题材徽标 + NEW/可入库标。
// 纯展示;点击交由 ToolboxPage 分派legacy 跳转 / 新工具开 runner
export function ToolCard({ tool, onOpen }: ToolCardProps) {
const legacy = isLegacyTool(tool);
// legacy 工具跳现有页面;新工具进结构化预览流,提示语据此分支(不暴露内部 key
const hint = legacy
? "前往现有页面"
: `${tool.input_fields?.[0]?.label ?? "需求"} → 结构化预览`;
return (
<button
type="button"
onClick={() => onOpen(tool)}
className={cardClass(
"group flex h-full min-h-[164px] flex-col p-5 text-left transition-colors hover:border-cinnabar focus-visible:border-cinnabar focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cinnabar/30",
"group flex h-full min-h-[164px] flex-col p-5 text-left motion-safe:transition-colors hover:border-cinnabar focus-visible:border-cinnabar focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cinnabar/30",
)}
>
<div className="mb-2 flex flex-wrap items-center gap-2">
@@ -37,11 +43,9 @@ export function ToolCard({ tool, onOpen }: ToolCardProps) {
<p className="line-clamp-2 text-sm text-ink-soft">{tool.subtitle}</p>
<div className="mt-auto flex items-center gap-2 pt-4 text-xs text-ink-soft">
<Sparkles className="h-3.5 w-3.5 text-cinnabar" aria-hidden="true" />
<span className="truncate">
{tool.input_fields?.[0]?.label ?? "现有页面"}
</span>
<span className="truncate">{hint}</span>
<ArrowRight
className="ml-auto h-3.5 w-3.5 text-cinnabar opacity-0 transition-opacity group-hover:opacity-100"
className="ml-auto h-3.5 w-3.5 text-cinnabar opacity-0 motion-safe:transition-opacity group-hover:opacity-100"
aria-hidden="true"
/>
</div>