Files
Yaojia Wang 1afeb2cdb5 feat(ui): 强化 AI 工作中动效——构思占位/流式进度条/波动三点/转圈
- 动效基元:ai-dot 波动(三点竖跳)、ai-stream 不确定进度条、ai-breathe 呼吸微光(均带 reduced-motion 回退)
- ThinkingIndicator 升级为明显波动;新增 Spinner / StreamingBar / GenerationSkeleton 原子件;Button 加 loading 态
- 写作台:首 token 前正文区『AI 正在构思本章…』占位(补最大缺口,不再空白像卡死)+ 流式顶部进度条
- 整章重写流式进度条;续写/润色忙碌骨架 + 触发键转圈;生成器/角色/世界观改用 Button loading
2026-07-12 19:32:18 +02:00

83 lines
2.8 KiB
TypeScript
Raw Permalink 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 { 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.9UX §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>
);
}