feat(frontend): 写作工作台补齐三项——内容感知书名 / 上下文速查抽屉 / genre采集
写作工作台重构剩余 3 项(多 agent 并行构建各自新文件,主线统一集成): - WFW-5 内容感知书名(#6):buildManuscriptExcerpt 取正文首尾摘录(硬上界控 token); GeneratorRunner 加 manuscriptText,有 brief 字段时出现「用当前正文」按钮把摘录填入 brief,让 book-title 等据正文反推。纯前端零后端;HITL——只填表单,生成仍需作者触发。 - WFW-6 上下文速查抽屉(#7):ContextDrawer 视口无关右侧 slide-over,设定库/大纲完整 只读速查 + 伏笔/规则/文风摘要跳链;底栏「速查」按钮触发。加法式、CONTEXT_DRAWER_ENABLED 一键回滚、旧侧栏/全宽页路由全保留。 - WFW-7 genre 采集 + 无大纲降级:项目无题材时首次写章前弹 GenrePicker 采集(临时并入 本次 directive 不落库);chapters 为空时编辑器显式提示「尚无大纲,将按设定自由生成」。 各项 TDD:manuscriptExcerpt/useGenreGate/contextDrawer/useContextDrawer 共 32 新单测。 门禁全绿:vitest 629 / tsc / lint / build / coverage 95.43%。
This commit is contained in:
62
apps/web/components/workbench/GenrePicker.tsx
Normal file
62
apps/web/components/workbench/GenrePicker.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/Button";
|
||||
|
||||
interface GenrePickerProps {
|
||||
// 备选题材(来自 lib/wizard/wizard.ts 的 GENRES)。
|
||||
genres: readonly string[];
|
||||
// 当前点选题材(未选 = null)。
|
||||
value: string | null;
|
||||
// 点选某题材(点已选项亦回传该值,交由上层决定是否切换)。
|
||||
onPick: (genre: string) => void;
|
||||
// 可选:关闭/跳过采集卡。
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
// 极轻 genre 采集卡(WFW-7):一组 chips 让作者点本次写作题材,
|
||||
// 杜绝无题材 slop。纯展示,选中态用 aria-pressed 标注。
|
||||
export function GenrePicker({
|
||||
genres,
|
||||
value,
|
||||
onPick,
|
||||
onDismiss,
|
||||
}: GenrePickerProps) {
|
||||
return (
|
||||
<div className="border-b border-line bg-panel px-4 py-3 sm:px-6">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="font-serif text-base text-ink">先选个题材</p>
|
||||
<p className="mt-0.5 text-sm text-ink-soft">
|
||||
本项目还没定题材,点一个题材再写,本次生成会更贴合、少套话。
|
||||
</p>
|
||||
</div>
|
||||
{onDismiss ? (
|
||||
<Button onClick={onDismiss} variant="secondary" size="sm">
|
||||
跳过
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
className="mt-3 flex flex-wrap gap-2"
|
||||
role="group"
|
||||
aria-label="本章题材"
|
||||
>
|
||||
{genres.map((genre) => {
|
||||
const active = genre === value;
|
||||
return (
|
||||
<Button
|
||||
key={genre}
|
||||
type="button"
|
||||
aria-pressed={active}
|
||||
onClick={() => onPick(genre)}
|
||||
variant={active ? "outline" : "secondary"}
|
||||
size="sm"
|
||||
>
|
||||
{genre}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user