Files
writer-work-flow/apps/web/components/generation/WorldGenerator.tsx

122 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useCallback, useState } from "react";
import { Copy, Sparkles } from "lucide-react";
import { useToast } from "@/components/Toast";
import { Button } from "@/components/ui/Button";
import { Field } from "@/components/ui/Field";
import { SectionHeader } from "@/components/ui/SectionHeader";
import { StatusNote } from "@/components/ui/StatusNote";
import { TextArea } from "@/components/ui/TextArea";
import { worldEntityRules } from "@/lib/generation/cards";
import type { WorldEntityCardView } from "@/lib/api/types";
import { useWorldGen } from "@/lib/generation/useWorldGen";
import { cardClass } from "@/lib/ui/variants";
interface WorldGeneratorProps {
projectId: string;
}
// 世界观设计器UX §6.5):一句话需求 → 预览实体卡type/name/rules
// 本期世界观入库端点未建(仅预览);预览结果供作者参考/复制。
// 复制文案:名称 + 硬规则正文(供作者粘贴到「规则」或「设定库」)。
function entityClipboardText(entity: WorldEntityCardView): string {
const rules = worldEntityRules(entity);
const body = rules.length > 0 ? rules.join("\n") : "(无显式硬规则)";
return `${entity.name}${entity.type}\n${body}`;
}
export function WorldGenerator({ projectId }: WorldGeneratorProps) {
const gen = useWorldGen();
const toast = useToast();
const [brief, setBrief] = useState("");
const onGenerate = useCallback(async () => {
await gen.generate(projectId, brief);
}, [gen, projectId, brief]);
const onCopy = useCallback(
async (entity: WorldEntityCardView): Promise<void> => {
try {
await navigator.clipboard.writeText(entityClipboardText(entity));
toast("已复制到剪贴板。", "success");
} catch {
toast("复制失败,请手动选择文本。", "error");
}
},
[toast],
);
const generating = gen.status === "generating";
return (
<section className="flex flex-col gap-4" aria-label="世界观设计器">
<div className="rounded border border-line bg-panel p-4">
<SectionHeader title="世界观设计器" className="mb-3" />
<Field label="设定方向" className="mb-3">
<TextArea
value={brief}
onChange={(e) => setBrief(e.target.value)}
placeholder="如:东方修真世界,灵气复苏,宗门林立,强者寿元有限"
rows={3}
/>
</Field>
<Button
onClick={() => void onGenerate()}
disabled={generating || brief.trim().length === 0}
variant="primary"
>
<Sparkles className="h-4 w-4" aria-hidden="true" />
{generating ? "生成中…" : "生成世界观"}
</Button>
</div>
{gen.status === "done" && gen.entities.length > 0 ? (
<div className="flex flex-col gap-3">
<StatusNote variant="info">
</StatusNote>
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
{gen.entities.map((entity, i) => (
<article
key={`${entity.name}-${i}`}
className={cardClass("p-3 text-sm")}
>
<header className="mb-2 flex items-center gap-2">
<span className="font-serif text-base text-ink">
{entity.name}
</span>
<span className="rounded bg-bg px-2 py-0.5 text-xs text-ink-soft">
{entity.type}
</span>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => void onCopy(entity)}
className="ml-auto"
aria-label={`复制 ${entity.name}`}
>
<Copy className="h-4 w-4" aria-hidden="true" />
</Button>
</header>
{worldEntityRules(entity).length > 0 ? (
<ul className="flex list-disc flex-col gap-1 pl-5 text-ink-soft">
{worldEntityRules(entity).map((rule, j) => (
<li key={j}>{rule}</li>
))}
</ul>
) : (
<p className="text-ink-soft"></p>
)}
</article>
))}
</div>
</div>
) : null}
</section>
);
}