feat(web): 生成器 ThinkingIndicator/空结果态/数量校验 + 文风页 PageHeader + Codex 分段控件 + RefineView role 修正
This commit is contained in:
@@ -11,6 +11,7 @@ import { Button } from "@/components/ui/Button";
|
|||||||
import { Card } from "@/components/ui/Card";
|
import { Card } from "@/components/ui/Card";
|
||||||
import { EmptyState } from "@/components/ui/EmptyState";
|
import { EmptyState } from "@/components/ui/EmptyState";
|
||||||
import { PageHeader } from "@/components/ui/PageHeader";
|
import { PageHeader } from "@/components/ui/PageHeader";
|
||||||
|
import { SegmentedControl } from "@/components/ui/SegmentedControl";
|
||||||
import type {
|
import type {
|
||||||
CharacterCardView,
|
CharacterCardView,
|
||||||
ProjectResponse,
|
ProjectResponse,
|
||||||
@@ -70,24 +71,13 @@ export function CodexPage({
|
|||||||
title="设定库"
|
title="设定库"
|
||||||
description="人物、世界观与时间线共同构成写作时的真相源。AI 生成内容也要先预览、再确认入库。"
|
description="人物、世界观与时间线共同构成写作时的真相源。AI 生成内容也要先预览、再确认入库。"
|
||||||
/>
|
/>
|
||||||
<div className="mb-4 flex items-center gap-2" role="tablist">
|
<SegmentedControl
|
||||||
{TABS.map((t) => (
|
className="mb-4 w-fit"
|
||||||
<button
|
ariaLabel="设定库分区"
|
||||||
key={t.key}
|
value={tab}
|
||||||
type="button"
|
onChange={setTab}
|
||||||
role="tab"
|
options={TABS.map((t) => ({ value: t.key, label: t.label }))}
|
||||||
aria-selected={tab === t.key}
|
/>
|
||||||
onClick={() => setTab(t.key)}
|
|
||||||
className={`rounded px-3 py-1.5 text-sm ${
|
|
||||||
tab === t.key
|
|
||||||
? "border-b-2 border-cinnabar text-cinnabar"
|
|
||||||
: "text-ink-soft hover:text-ink"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{t.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="min-h-0 flex-1 overflow-auto">
|
<div className="min-h-0 flex-1 overflow-auto">
|
||||||
{tab === "characters" ? (
|
{tab === "characters" ? (
|
||||||
@@ -119,7 +109,7 @@ export function CodexPage({
|
|||||||
{c.relations.map((r, j) => (
|
{c.relations.map((r, j) => (
|
||||||
<li
|
<li
|
||||||
key={`${r.name}-${r.kind}-${j}`}
|
key={`${r.name}-${r.kind}-${j}`}
|
||||||
className="rounded bg-panel px-1.5 py-0.5 text-[11px]"
|
className="rounded bg-panel px-1.5 py-0.5 text-2xs"
|
||||||
title={r.note ?? undefined}
|
title={r.note ?? undefined}
|
||||||
>
|
>
|
||||||
{r.kind} · {r.name}
|
{r.kind} · {r.name}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { Database, Sparkles } from "lucide-react";
|
import { Database, Sparkles } from "lucide-react";
|
||||||
|
|
||||||
|
import { ThinkingIndicator } from "@/components/ThinkingIndicator";
|
||||||
import { Button } from "@/components/ui/Button";
|
import { Button } from "@/components/ui/Button";
|
||||||
import { Field } from "@/components/ui/Field";
|
import { Field } from "@/components/ui/Field";
|
||||||
import { SectionHeader } from "@/components/ui/SectionHeader";
|
import { SectionHeader } from "@/components/ui/SectionHeader";
|
||||||
@@ -10,6 +11,7 @@ import { StatusNote } from "@/components/ui/StatusNote";
|
|||||||
import { TextInput } from "@/components/ui/TextInput";
|
import { TextInput } from "@/components/ui/TextInput";
|
||||||
import type { CharacterCardView } from "@/lib/api/types";
|
import type { CharacterCardView } from "@/lib/api/types";
|
||||||
import {
|
import {
|
||||||
|
clampCharacterCount,
|
||||||
MAX_CHARACTER_COUNT,
|
MAX_CHARACTER_COUNT,
|
||||||
MIN_CHARACTER_COUNT,
|
MIN_CHARACTER_COUNT,
|
||||||
updateCard,
|
updateCard,
|
||||||
@@ -95,13 +97,19 @@ export function CharacterGenerator({
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<div className="flex flex-wrap items-end gap-3">
|
<div className="flex flex-wrap items-end gap-3">
|
||||||
<Field label="数量" className="w-24">
|
<Field
|
||||||
|
label="数量"
|
||||||
|
className="w-28"
|
||||||
|
help={`${MIN_CHARACTER_COUNT}–${MAX_CHARACTER_COUNT} 张`}
|
||||||
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
type="number"
|
type="number"
|
||||||
min={MIN_CHARACTER_COUNT}
|
min={MIN_CHARACTER_COUNT}
|
||||||
max={MAX_CHARACTER_COUNT}
|
max={MAX_CHARACTER_COUNT}
|
||||||
value={count}
|
value={count}
|
||||||
onChange={(e) => setCount(Number(e.target.value))}
|
onChange={(e) =>
|
||||||
|
setCount(clampCharacterCount(Number(e.target.value)))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="定位(可选)" className="min-w-[12rem] flex-1">
|
<Field label="定位(可选)" className="min-w-[12rem] flex-1">
|
||||||
@@ -113,7 +121,12 @@ export function CharacterGenerator({
|
|||||||
</Field>
|
</Field>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => void onGenerate()}
|
onClick={() => void onGenerate()}
|
||||||
disabled={generating || brief.trim().length === 0}
|
disabled={
|
||||||
|
generating ||
|
||||||
|
brief.trim().length === 0 ||
|
||||||
|
count < MIN_CHARACTER_COUNT ||
|
||||||
|
count > MAX_CHARACTER_COUNT
|
||||||
|
}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
>
|
>
|
||||||
<Sparkles className="h-4 w-4" aria-hidden="true" />
|
<Sparkles className="h-4 w-4" aria-hidden="true" />
|
||||||
@@ -122,6 +135,12 @@ export function CharacterGenerator({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{generating ? (
|
||||||
|
<div className="rounded border border-line bg-panel/50 p-4 text-cinnabar">
|
||||||
|
<ThinkingIndicator label="生成中" />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{gen.genStatus === "preview" && drafts.length > 0 ? (
|
{gen.genStatus === "preview" && drafts.length > 0 ? (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<p className="text-xs text-ink-soft">
|
<p className="text-xs text-ink-soft">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { ThinkingIndicator } from "@/components/ThinkingIndicator";
|
||||||
import { Button } from "@/components/ui/Button";
|
import { Button } from "@/components/ui/Button";
|
||||||
import type { IngestConflicts } from "@/lib/generation/cards";
|
import type { IngestConflicts } from "@/lib/generation/cards";
|
||||||
|
|
||||||
@@ -74,6 +75,11 @@ export function ConflictAdjudication({
|
|||||||
取消,回去改卡
|
取消,回去改卡
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
{busy ? (
|
||||||
|
<div className="mt-3 text-cinnabar">
|
||||||
|
<ThinkingIndicator label="入库中" />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { Copy, Sparkles } from "lucide-react";
|
import { Copy, Globe2, RotateCcw, Sparkles } from "lucide-react";
|
||||||
|
|
||||||
import { useToast } from "@/components/Toast";
|
import { useToast } from "@/components/Toast";
|
||||||
|
import { ThinkingIndicator } from "@/components/ThinkingIndicator";
|
||||||
import { Button } from "@/components/ui/Button";
|
import { Button } from "@/components/ui/Button";
|
||||||
|
import { EmptyState } from "@/components/ui/EmptyState";
|
||||||
import { Field } from "@/components/ui/Field";
|
import { Field } from "@/components/ui/Field";
|
||||||
import { SectionHeader } from "@/components/ui/SectionHeader";
|
import { SectionHeader } from "@/components/ui/SectionHeader";
|
||||||
import { StatusNote } from "@/components/ui/StatusNote";
|
import { StatusNote } from "@/components/ui/StatusNote";
|
||||||
@@ -72,6 +74,49 @@ export function WorldGenerator({ projectId }: WorldGeneratorProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{generating ? (
|
||||||
|
<div className="rounded border border-line bg-panel/50 p-4 text-cinnabar">
|
||||||
|
<ThinkingIndicator label="生成中" />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{gen.status === "error" ? (
|
||||||
|
<StatusNote variant="danger">
|
||||||
|
<span className="flex flex-wrap items-center gap-3">
|
||||||
|
<span>生成失败,请稍后重试。</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => void onGenerate()}
|
||||||
|
>
|
||||||
|
<RotateCcw className="h-4 w-4" aria-hidden="true" />
|
||||||
|
重试
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</StatusNote>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{gen.status === "done" && gen.entities.length === 0 ? (
|
||||||
|
<EmptyState
|
||||||
|
icon={Globe2}
|
||||||
|
title="本次未生成可用实体"
|
||||||
|
description="可换一个更具体的设定方向,或补充世界的硬规则与边界后再试。"
|
||||||
|
action={
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => void onGenerate()}
|
||||||
|
disabled={brief.trim().length === 0}
|
||||||
|
>
|
||||||
|
<Sparkles className="h-4 w-4" aria-hidden="true" />
|
||||||
|
重新生成
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{gen.status === "done" && gen.entities.length > 0 ? (
|
{gen.status === "done" && gen.entities.length > 0 ? (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<StatusNote variant="info">
|
<StatusNote variant="info">
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export function RefineView({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="rounded border border-cinnabar bg-panel p-3 text-xs"
|
className="rounded border border-cinnabar bg-panel p-3 text-xs"
|
||||||
role="dialog"
|
role="region"
|
||||||
aria-label="回炉对比"
|
aria-label="回炉对比"
|
||||||
>
|
>
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
@@ -61,14 +61,22 @@ export function RefineView({
|
|||||||
<Badge variant="warning">
|
<Badge variant="warning">
|
||||||
该段在终稿中为空,请先在终稿中保留该段正文。
|
该段在终稿中为空,请先在终稿中保留该段正文。
|
||||||
</Badge>
|
</Badge>
|
||||||
) : refiner.status === "refining" ? (
|
) : (
|
||||||
<p className="text-info">
|
<div role="status" aria-live="polite">
|
||||||
<ThinkingIndicator label="回炉中" />
|
{refiner.status === "refining" ? (
|
||||||
</p>
|
<p className="text-info">
|
||||||
) : refiner.status === "error" ? (
|
<ThinkingIndicator label="回炉中" />
|
||||||
<p className="text-conflict">回炉失败,请稍后重试。</p>
|
</p>
|
||||||
) : refiner.result ? (
|
) : refiner.status === "error" ? (
|
||||||
<div className="space-y-2">
|
<p className="text-conflict">回炉失败,请稍后重试。</p>
|
||||||
|
) : refiner.result ? (
|
||||||
|
<span className="sr-only">回炉完成,可对比后采纳。</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{segment.trim().length > 0 && refiner.result ? (
|
||||||
|
<div className="mt-2 space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-1 text-ink-soft">原段</p>
|
<p className="mb-1 text-ink-soft">原段</p>
|
||||||
<p className="whitespace-pre-wrap rounded border border-line bg-bg p-2 text-ink-soft line-through">
|
<p className="whitespace-pre-wrap rounded border border-line bg-bg p-2 text-ink-soft line-through">
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { AppShell } from "@/components/AppShell";
|
import { AppShell } from "@/components/AppShell";
|
||||||
|
import { PageHeader } from "@/components/ui/PageHeader";
|
||||||
|
import { SectionHeader } from "@/components/ui/SectionHeader";
|
||||||
import type { ProjectResponse } from "@/lib/api/types";
|
import type { ProjectResponse } from "@/lib/api/types";
|
||||||
import { useStyleLearn } from "@/lib/style/useStyleLearn";
|
import { useStyleLearn } from "@/lib/style/useStyleLearn";
|
||||||
import type { Fingerprint, StyleLearnMode } from "@/lib/style/style";
|
import type { Fingerprint, StyleLearnMode } from "@/lib/style/style";
|
||||||
@@ -27,21 +29,31 @@ export function StylePage({ project, initialFingerprint }: StylePageProps) {
|
|||||||
projectId={project.id}
|
projectId={project.id}
|
||||||
activeNav="style"
|
activeNav="style"
|
||||||
>
|
>
|
||||||
<div className="flex min-h-[calc(100vh-var(--chrome,4rem))] flex-col lg:grid lg:h-[calc(100vh-var(--chrome,4rem))] lg:min-h-0 lg:grid-cols-[28rem_1fr]">
|
<div className="flex min-h-[calc(100vh-var(--chrome,4rem))] flex-col">
|
||||||
<section className="flex flex-col border-line bg-panel px-6 py-6 lg:overflow-auto lg:border-r">
|
<div className="px-6 pt-6">
|
||||||
<h1 className="mb-3 font-serif text-lg text-ink">学习文风</h1>
|
<PageHeader
|
||||||
<StyleUpload
|
title="文风指纹"
|
||||||
busy={learn.busy}
|
description="上传你的代表作样本,让 AI 学出十六维文风指纹,作为后续写作与第四审的对照标尺。"
|
||||||
pollStatus={learn.pollStatus === "idle" ? "idle" : learn.pollStatus}
|
|
||||||
progress={learn.progress}
|
|
||||||
hasFingerprint={learn.fingerprint !== null}
|
|
||||||
onLearn={onLearn}
|
|
||||||
/>
|
/>
|
||||||
</section>
|
</div>
|
||||||
|
<div className="flex flex-1 flex-col lg:grid lg:min-h-0 lg:grid-cols-[28rem_1fr]">
|
||||||
|
<section className="flex flex-col border-line bg-panel px-6 py-6 lg:overflow-auto lg:border-r">
|
||||||
|
<SectionHeader title="学习文风" className="mb-3" />
|
||||||
|
<StyleUpload
|
||||||
|
busy={learn.busy}
|
||||||
|
pollStatus={
|
||||||
|
learn.pollStatus === "idle" ? "idle" : learn.pollStatus
|
||||||
|
}
|
||||||
|
progress={learn.progress}
|
||||||
|
hasFingerprint={learn.fingerprint !== null}
|
||||||
|
onLearn={onLearn}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="bg-bg px-6 py-6 lg:overflow-auto">
|
<section className="bg-bg px-6 py-6 lg:overflow-auto">
|
||||||
<FingerprintView fingerprint={learn.fingerprint} />
|
<FingerprintView fingerprint={learn.fingerprint} />
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function StylePanel({ style, incomplete, onRefine }: StylePanelProps) {
|
|||||||
return (
|
return (
|
||||||
<section className="mb-4">
|
<section className="mb-4">
|
||||||
<h2 className="text-xs font-semibold uppercase tracking-wide text-ink-soft">
|
<h2 className="text-xs font-semibold uppercase tracking-wide text-ink-soft">
|
||||||
文风 (style-auditor)
|
文风
|
||||||
</h2>
|
</h2>
|
||||||
{incomplete ? (
|
{incomplete ? (
|
||||||
<Badge variant="warning" className="mt-1">
|
<Badge variant="warning" className="mt-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user