Files
writer-work-flow/apps/web/components/outline/OutlineEditor.tsx
Yaojia Wang 8779530806 feat(ux): T4-a 全局 AI 工具条 — 项目页顶部常驻写本章/审稿/大纲/设定库/工具箱
新 lib/nav/ai-tools.ts (aiToolItems) + components/AiToolbar.tsx 服务端组件;AppShell 顶栏下渲染(仅项目页),用 --chrome CSS 变量统一 chrome 高度,6 个固定高度页改用 calc(100vh-var(--chrome,4rem)) 适配。TDD: ai-tools 测。前端门禁绿: lint/tsc/vitest/build。
2026-06-20 18:19:46 +02:00

98 lines
3.4 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 { useMemo, useState } from "react";
import { AppShell } from "@/components/AppShell";
import type { OutlineChapterView, ProjectResponse } from "@/lib/api/types";
import { groupByVolume } from "@/lib/outline/outline";
import { useOutline } from "@/lib/outline/useOutline";
import { OutlineChapterRow } from "./OutlineChapterRow";
interface OutlineEditorProps {
project: ProjectResponse;
initialChapters: OutlineChapterView[];
}
// 大纲编辑器主体UX §6.7。RSC 种入已存大纲Client 承载生成POST .../outline
// 无凭据 → 503 LLM_UNAVAILABLE 经 error 检出、引导去设置。
export function OutlineEditor({
project,
initialChapters,
}: OutlineEditorProps) {
const { chapters, status, error, generate } = useOutline(initialChapters);
const [volume, setVolume] = useState(1);
const volumes = useMemo(() => groupByVolume(chapters), [chapters]);
const generating = status === "generating";
return (
<AppShell
title={`${project.title}`}
subtitle="大纲"
projectId={project.id}
activeNav="outline"
>
<div className="flex h-[calc(100vh-var(--chrome,4rem))] flex-col p-6">
<div className="mb-4 flex items-center gap-3">
<h1 className="font-serif text-lg text-ink"></h1>
<label htmlFor="vol" className="ml-auto text-xs text-ink-soft">
</label>
<input
id="vol"
inputMode="numeric"
value={volume}
onChange={(e) => setVolume(Math.max(1, Number(e.target.value) || 1))}
className="w-16 rounded border border-line bg-bg px-2 py-1 text-sm text-ink focus:border-cinnabar focus:outline-none"
/>
<button
type="button"
disabled={generating}
onClick={() => void generate(project.id, volume)}
className="rounded bg-cinnabar px-3 py-1.5 text-sm text-panel hover:opacity-90 disabled:opacity-40"
>
{generating ? "排大纲中…" : "✦ AI 排大纲"}
</button>
</div>
{error ? (
<p className="mb-4 text-sm text-conflict">
{error.code}{error.message}
{error.code === "LLM_UNAVAILABLE" ? (
<>
{" "}
<a
href="/settings/providers"
className="underline hover:text-cinnabar"
>
</a>
</>
) : null}
</p>
) : null}
<div className="min-h-0 flex-1 overflow-auto">
{volumes.length === 0 ? (
<p className="rounded border border-dashed border-line p-6 text-sm text-ink-soft">
AI
</p>
) : (
volumes.map((group) => (
<section key={group.volume} className="mb-6">
<h2 className="mb-2 font-serif text-base text-ink">
{group.volume}
</h2>
<ul className="space-y-1">
{group.chapters.map((ch) => (
<OutlineChapterRow key={ch.no} chapter={ch} />
))}
</ul>
</section>
))
)}
</div>
</div>
</AppShell>
);
}