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:
@@ -4,6 +4,7 @@ import { GlassCard, GlassButton, GlassInput, GlassTextarea } from '../components
|
||||
import Modal from '../components/Modal';
|
||||
import useStore from '../store/useStore';
|
||||
import { analyzeLifeEvent } from '../services/ai';
|
||||
import useTypewriterStream from '../hooks/useTypewriterStream';
|
||||
|
||||
/**
|
||||
* 格式化 AI 反馈内容的组件
|
||||
@@ -93,6 +94,7 @@ const TimelineView = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [streamFeedback, setStreamFeedback] = useState('');
|
||||
const feedbackWriter = useTypewriterStream({ interval: 18, step: 1 });
|
||||
|
||||
// 编辑模式状态:null 表示新增模式,有值表示编辑模式(存储事件 ID)
|
||||
const [editingEventId, setEditingEventId] = useState(null);
|
||||
@@ -114,6 +116,7 @@ const TimelineView = () => {
|
||||
setEditingEventId(null);
|
||||
setEventForm({ title: '', time: '', content: '' });
|
||||
setStreamFeedback('');
|
||||
feedbackWriter.reset();
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
@@ -129,6 +132,7 @@ const TimelineView = () => {
|
||||
content: event.content || ''
|
||||
});
|
||||
setStreamFeedback(event.aiFeedback || '');
|
||||
feedbackWriter.reset();
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
@@ -140,6 +144,7 @@ const TimelineView = () => {
|
||||
setEditingEventId(null);
|
||||
setEventForm({ title: '', time: '', content: '' });
|
||||
setStreamFeedback('');
|
||||
feedbackWriter.reset();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -152,12 +157,18 @@ const TimelineView = () => {
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
feedbackWriter.reset();
|
||||
|
||||
try {
|
||||
setStreamFeedback('');
|
||||
const aiFeedback = await analyzeLifeEvent(eventForm, {
|
||||
onDelta: (_delta, output) => setStreamFeedback(output)
|
||||
onDelta: (_delta, output) => {
|
||||
setStreamFeedback(output);
|
||||
feedbackWriter.push(output);
|
||||
}
|
||||
});
|
||||
feedbackWriter.finish(aiFeedback);
|
||||
await feedbackWriter.waitForDone();
|
||||
|
||||
if (editingEventId) {
|
||||
// 编辑模式:调用更新接口
|
||||
@@ -177,6 +188,7 @@ const TimelineView = () => {
|
||||
// 重置表单并关闭模态框
|
||||
closeModal();
|
||||
} catch (error) {
|
||||
feedbackWriter.fail('AI 疗愈反馈生成失败,请稍后重试');
|
||||
console.error('Failed to save event:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -323,13 +335,17 @@ const TimelineView = () => {
|
||||
onChange={(v) => setEventForm(prev => ({ ...prev, content: v }))}
|
||||
rows={5}
|
||||
/>
|
||||
{streamFeedback && (
|
||||
{(streamFeedback || isLoading) && (
|
||||
<div className="ai-glow-card p-4 rounded-2xl bg-orange-200/[0.02] border border-orange-200/5">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Sparkles className="w-3 h-3 text-orange-200" />
|
||||
<span className="text-[9px] uppercase tracking-[0.2em] text-orange-200/60 font-bold">实时疗愈反馈</span>
|
||||
<span className="text-[9px] tracking-[0.2em] text-orange-200/60 font-bold">
|
||||
{feedbackWriter.isWaiting ? '正在理解这段经历' : feedbackWriter.isDraining ? '正在收束反馈' : '实时疗愈反馈'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs italic text-white/50 leading-loose whitespace-pre-wrap">{streamFeedback}</p>
|
||||
{feedbackWriter.visibleText
|
||||
? <FeedbackContent content={feedbackWriter.visibleText} />
|
||||
: <p className="text-xs italic text-white/40 leading-loose">AI 正在梳理你的生命轨迹,请稍候...</p>}
|
||||
</div>
|
||||
)}
|
||||
<GlassButton
|
||||
|
||||
Reference in New Issue
Block a user