- 动效基元:ai-dot 波动(三点竖跳)、ai-stream 不确定进度条、ai-breathe 呼吸微光(均带 reduced-motion 回退) - ThinkingIndicator 升级为明显波动;新增 Spinner / StreamingBar / GenerationSkeleton 原子件;Button 加 loading 态 - 写作台:首 token 前正文区『AI 正在构思本章…』占位(补最大缺口,不再空白像卡死)+ 流式顶部进度条 - 整章重写流式进度条;续写/润色忙碌骨架 + 触发键转圈;生成器/角色/世界观改用 Button loading
83 lines
2.8 KiB
TypeScript
83 lines
2.8 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useRef } from "react";
|
||
|
||
import { cn, focusRing, proseBody } from "@/lib/ui/variants";
|
||
import { ThinkingPlaceholder } from "./ThinkingPlaceholder";
|
||
|
||
export interface EditorSelection {
|
||
start: number;
|
||
end: number;
|
||
text: string;
|
||
}
|
||
|
||
interface EditorProps {
|
||
value: string;
|
||
onChange: (value: string) => void;
|
||
streaming: boolean;
|
||
// 构思中(流式已开始但首 token 未到):在正文区覆盖「AI 正在构思本章…」占位,
|
||
// 首个 token 到达即消失换成正文。纯展示层,不触碰流式逻辑。
|
||
isThinking?: boolean;
|
||
// 选区变化上报(供「润色选段」等选区级 AI 动作取材)。空选区也会上报(start===end)。
|
||
onSelectionChange?: (selection: EditorSelection) => void;
|
||
}
|
||
|
||
// 中栏正文编辑器:宋体、720px 居中、行高 1.9(UX §2.3 / §6.3)。
|
||
// 流式中显示朱砂光标提示(prefers-reduced-motion 下不闪烁,见 globals.css)。
|
||
export function Editor({
|
||
value,
|
||
onChange,
|
||
streaming,
|
||
isThinking = false,
|
||
onSelectionChange,
|
||
}: EditorProps) {
|
||
const ref = useRef<HTMLTextAreaElement>(null);
|
||
|
||
const reportSelection = (el: HTMLTextAreaElement): void => {
|
||
if (!onSelectionChange) return;
|
||
const start = el.selectionStart;
|
||
const end = el.selectionEnd;
|
||
onSelectionChange({ start, end, text: value.slice(start, end) });
|
||
};
|
||
|
||
// 自适应高度:textarea 高度=内容高度(overflow-hidden 不内部滚动),由外层写作区
|
||
// 单一滚动条统管。修复正文出现嵌套滚动条 + 定高 60vh 不铺满展示区的怪象。
|
||
// min-h-[60vh] 仍作空稿时的最小可写高度(内容更长时按内容增高、外层滚动)。
|
||
useEffect(() => {
|
||
const el = ref.current;
|
||
if (!el) return;
|
||
el.style.height = "auto";
|
||
el.style.height = `${el.scrollHeight}px`;
|
||
}, [value]);
|
||
|
||
return (
|
||
<div className="relative mx-auto max-w-prose">
|
||
<label htmlFor="chapter-editor" className="sr-only">
|
||
正文编辑器
|
||
</label>
|
||
<textarea
|
||
ref={ref}
|
||
id="chapter-editor"
|
||
value={value}
|
||
onChange={(e) => onChange(e.target.value)}
|
||
onSelect={(e) => reportSelection(e.currentTarget)}
|
||
readOnly={streaming}
|
||
aria-busy={streaming}
|
||
placeholder="夜色如墨……点击「写本章」让 AI 起草,或直接在此书写。"
|
||
className={cn(
|
||
proseBody,
|
||
"block min-h-[60vh] w-full resize-none overflow-hidden rounded bg-transparent text-ink placeholder:text-ink-soft/50",
|
||
focusRing,
|
||
)}
|
||
/>
|
||
{streaming && !isThinking ? (
|
||
<span
|
||
className="typewriter-cursor ml-0.5 inline-block h-[1.2em] w-[2px] translate-y-1 bg-cinnabar align-middle"
|
||
aria-hidden="true"
|
||
/>
|
||
) : null}
|
||
{isThinking ? <ThinkingPlaceholder /> : null}
|
||
</div>
|
||
);
|
||
}
|