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:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="path-view">
|
||||
<view class="path-view" :style="{ paddingTop: capsuleTopReservePx + 'px' }">
|
||||
<text class="page-title">实现路径</text>
|
||||
|
||||
<view v-if="!selectedScript" class="empty-state glass-card">
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<view v-else-if="!currentPath" class="empty-state glass-card">
|
||||
<text class="empty-icon">🎯</text>
|
||||
<text class="empty-text">等待开启人生导航...</text>
|
||||
<text class="empty-text">{{ pathEmptyText }}</text>
|
||||
</view>
|
||||
|
||||
<view v-else class="path-content">
|
||||
@@ -41,14 +41,19 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, onMounted, watch } from 'vue'
|
||||
import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useAppStore } from '../../stores/app.js'
|
||||
import { useTypewriterStream } from '../../composables/useTypewriterStream.js'
|
||||
import { useMenuButtonSafeArea } from '../../composables/useMenuButtonSafeArea.js'
|
||||
import * as lifePathService from '../../services/lifePath.js'
|
||||
import { streamAiScene } from '../../services/aiRuntime.js'
|
||||
|
||||
const store = useAppStore()
|
||||
const { capsuleTopReservePx } = useMenuButtonSafeArea({ extraTopPx: 10 })
|
||||
|
||||
const pathData = ref(null)
|
||||
const pathGenerating = ref(false)
|
||||
const pathWriter = useTypewriterStream({ interval: 24, step: 1 })
|
||||
|
||||
const selectedScript = computed(() => {
|
||||
return store.scripts.find(s => s.isSelected)
|
||||
@@ -60,6 +65,34 @@ const currentPath = computed(() => {
|
||||
return null
|
||||
})
|
||||
|
||||
const pathEmptyText = computed(() => {
|
||||
if (pathWriter.isWaiting.value) return '正在分析剧本,拆解可执行路径...'
|
||||
if (pathWriter.isStreaming.value || pathWriter.isDraining.value) return '正在生成路径节点...'
|
||||
return '等待开启人生导航...'
|
||||
})
|
||||
|
||||
const buildStreamPath = (scriptId, title, output) => ({
|
||||
id: `stream-${scriptId}`,
|
||||
scriptId,
|
||||
title,
|
||||
description: output,
|
||||
steps: output.split('\n').filter(Boolean).slice(0, 6).map((line, index) => ({
|
||||
phase: `阶段${index + 1}`,
|
||||
task: line.replace(/^\d+[.、]\s*/, '').slice(0, 28),
|
||||
desc: line,
|
||||
content: line,
|
||||
done: index === 0
|
||||
})),
|
||||
progress: 8,
|
||||
status: 'active'
|
||||
})
|
||||
|
||||
watch(() => pathWriter.visibleText.value, (text) => {
|
||||
if (!(pathGenerating.value || pathWriter.isStreaming.value || pathWriter.isDraining.value) || !selectedScript.value?.id || !text) return
|
||||
const title = selectedScript.value.title ? `${selectedScript.value.title} · 实现路径` : '我的实现路径'
|
||||
pathData.value = buildStreamPath(selectedScript.value.id, title, text)
|
||||
})
|
||||
|
||||
const loadPath = async (scriptId) => {
|
||||
if (!scriptId) return
|
||||
try {
|
||||
@@ -76,6 +109,8 @@ const createPlaceholderPath = async (scriptId) => {
|
||||
const script = selectedScript.value || {}
|
||||
const title = script.title ? `${script.title} · 实现路径` : '我的实现路径'
|
||||
let generatedText = ''
|
||||
pathGenerating.value = true
|
||||
pathWriter.reset()
|
||||
try {
|
||||
const streamRes = await streamAiScene({
|
||||
sceneCode: 'life_healing',
|
||||
@@ -86,26 +121,23 @@ const createPlaceholderPath = async (scriptId) => {
|
||||
},
|
||||
onDelta: (_delta, output) => {
|
||||
generatedText = output
|
||||
pathData.value = {
|
||||
id: `stream-${scriptId}`,
|
||||
scriptId,
|
||||
title,
|
||||
description: output,
|
||||
steps: output.split('\n').filter(Boolean).slice(0, 6).map((line, index) => ({
|
||||
phase: `阶段${index + 1}`,
|
||||
task: line.replace(/^\d+[.、]\s*/, '').slice(0, 28),
|
||||
desc: line,
|
||||
content: line,
|
||||
done: index === 0
|
||||
})),
|
||||
progress: 8,
|
||||
status: 'active'
|
||||
}
|
||||
pathWriter.push(output)
|
||||
},
|
||||
onDone: (_event, output) => {
|
||||
pathWriter.finish(output)
|
||||
},
|
||||
onError: (message) => {
|
||||
pathWriter.fail(message || '路径生成失败')
|
||||
}
|
||||
})
|
||||
generatedText = streamRes.output || generatedText
|
||||
pathWriter.finish(generatedText)
|
||||
await pathWriter.waitForDone()
|
||||
} catch (error) {
|
||||
pathWriter.fail(error?.message || '路径生成失败')
|
||||
generatedText = ''
|
||||
} finally {
|
||||
pathGenerating.value = false
|
||||
}
|
||||
const steps = [
|
||||
{ phase: '阶段1', task: '整理目标', desc: '把剧本中的关键目标拆成可以执行的小目标。', content: '把剧本中的关键目标拆成可以执行的小目标。', done: true },
|
||||
@@ -128,7 +160,7 @@ const createPlaceholderPath = async (scriptId) => {
|
||||
const res = await lifePathService.createPath({
|
||||
scriptId,
|
||||
title,
|
||||
description: script.summary || '根据选中的人生剧本生成的占位实现路径,后续可接入AI生成更细的行动计划。',
|
||||
description: generatedText || script.summary || '根据选中的人生剧本生成的占位实现路径,后续可接入AI生成更细的行动计划。',
|
||||
steps,
|
||||
progress: 8,
|
||||
status: 'active'
|
||||
@@ -164,6 +196,10 @@ onMounted(() => {
|
||||
loadPath(selectedScript.value.id)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
pathWriter.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user