fix(web): 链相位语义色/失败可重试 + 模板填入中文标签/删除可撤销/工具卡文案 + 模板库 PageHeader
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user