feat: AI 打字机流式输出、小程序脚本主页布局及灵感卡片优化
- life-script: 新增 aiRuntime 打字机流式服务,PathView/ScriptView/TimelineView 接入打字机效果 - mini-program: ScriptView 重构为打字机输出 + 卡片式灵感列表,主页布局优化 - web: aiRuntime 服务新增流式输出支持 - chat store: 消息状态管理和打字机流式渲染支持 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import Modal from '../components/Modal';
|
||||
import useStore from '../store/useStore';
|
||||
import { scriptStyles, scriptLengths } from '../utils/constants';
|
||||
import { generateEpicScript } from '../services/ai';
|
||||
import useTypewriterStream from '../hooks/useTypewriterStream';
|
||||
|
||||
/**
|
||||
* ScriptView 组件
|
||||
@@ -39,6 +40,7 @@ const ScriptView = ({ onOpenProfile }) => {
|
||||
const [length, setLength] = useState(scriptLengths[0].value);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [streamContent, setStreamContent] = useState('');
|
||||
const scriptWriter = useTypewriterStream({ interval: 18, step: 1 });
|
||||
|
||||
// 编辑模态框状态
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
@@ -62,14 +64,22 @@ const ScriptView = ({ onOpenProfile }) => {
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
scriptWriter.reset();
|
||||
|
||||
try {
|
||||
setStreamContent('');
|
||||
const content = await generateEpicScript(
|
||||
{ theme, style, length, character: registrationData },
|
||||
lifeEvents,
|
||||
{ onDelta: (_delta, output) => setStreamContent(output) }
|
||||
{
|
||||
onDelta: (_delta, output) => {
|
||||
setStreamContent(output);
|
||||
scriptWriter.push(output);
|
||||
}
|
||||
}
|
||||
);
|
||||
scriptWriter.finish(content);
|
||||
await scriptWriter.waitForDone();
|
||||
await addScript({
|
||||
theme,
|
||||
style,
|
||||
@@ -82,6 +92,7 @@ const ScriptView = ({ onOpenProfile }) => {
|
||||
setTheme('');
|
||||
setStreamContent('');
|
||||
} catch (error) {
|
||||
scriptWriter.fail('生成失败,请稍后重试');
|
||||
console.error('Failed to generate script:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -121,14 +132,22 @@ const ScriptView = ({ onOpenProfile }) => {
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
scriptWriter.reset();
|
||||
|
||||
try {
|
||||
setStreamContent('');
|
||||
const content = await generateEpicScript(
|
||||
{ theme: editForm.theme, style: editForm.style, length: editForm.length, character: registrationData },
|
||||
lifeEvents,
|
||||
{ onDelta: (_delta, output) => setStreamContent(output) }
|
||||
{
|
||||
onDelta: (_delta, output) => {
|
||||
setStreamContent(output);
|
||||
scriptWriter.push(output);
|
||||
}
|
||||
}
|
||||
);
|
||||
scriptWriter.finish(content);
|
||||
await scriptWriter.waitForDone();
|
||||
await updateScript({
|
||||
id: editingScript.id,
|
||||
theme: editForm.theme,
|
||||
@@ -142,6 +161,7 @@ const ScriptView = ({ onOpenProfile }) => {
|
||||
closeEditModal();
|
||||
setStreamContent('');
|
||||
} catch (error) {
|
||||
scriptWriter.fail('生成失败,请稍后重试');
|
||||
console.error('Failed to update script:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -306,18 +326,21 @@ const ScriptView = ({ onOpenProfile }) => {
|
||||
{/* 右侧剧本展示区 */}
|
||||
<div className="lg:col-span-8">
|
||||
<div className="h-full">
|
||||
{isLoading && streamContent ? (
|
||||
{isLoading ? (
|
||||
<GlassCard className="h-full overflow-y-auto custom-scrollbar border-orange-200/20 shadow-2xl animate-fade-in" padding="lg">
|
||||
<div className="prose prose-invert max-w-none">
|
||||
<div className="flex justify-between items-center mb-8 pb-4 border-b border-white/5">
|
||||
<div>
|
||||
<h4 className="text-2xl font-serif text-orange-200">{theme}</h4>
|
||||
<p className="text-[10px] text-white/30 mt-1 uppercase tracking-widest">正在生成</p>
|
||||
<h4 className="text-2xl font-serif text-orange-200">{theme || editForm.theme}</h4>
|
||||
<p className="text-[10px] text-white/40 mt-1 tracking-widest">
|
||||
{scriptWriter.isWaiting ? '正在理解你的创作目标' : scriptWriter.isDraining ? '正在收束最后一句' : '正在逐字生成剧本'}
|
||||
</p>
|
||||
</div>
|
||||
<BookOpen className="text-white/20" />
|
||||
<Loader2 className="text-orange-200/60 animate-spin" />
|
||||
</div>
|
||||
<div className="text-white/70 leading-loose whitespace-pre-wrap space-y-6 text-sm">
|
||||
{streamContent}
|
||||
{scriptWriter.visibleText || '故事正在生成,请稍候...'}
|
||||
{(scriptWriter.isStreaming || scriptWriter.isDraining) && <span className="text-orange-200 animate-pulse">|</span>}
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
Reference in New Issue
Block a user