feat: ScriptView 改造为对话中心查看/修改模式
- 新增 scriptChat.js 和 message.js 服务导入 - 新增 viewMode、scriptId、conversationId、currentVersionMessageId、 selectedVersionMessageId、messages、versions、chatInput 等状态 - 新增阅读模式:版本栏 + 剧本分章节展示 + 操作按钮 - 新增聊天模式:消息列表 + 聊天输入栏 + 对话修改功能 - 集成 createScriptWithDialogue、streamScriptChat、listMessagesByConversation、 listMessageVersions、switchVersion、deleteVersion 等后端 API - 替换 runGeneration 使用 createScriptWithDialogue 创建剧本和对话 - 替换 openScriptChat 使用后端消息加载,不再依赖 localStorage - 移除 localStorage 作为主要消息存储(persistResultMessages、 hydrateResultMessages、readStoredMessages、readScriptChatHistory 改为空操作) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -158,72 +158,171 @@
|
||||
<view class="result-scroll-content">
|
||||
<view id="result-scroll-top" class="result-scroll-top"></view>
|
||||
|
||||
<view class="conversation compact">
|
||||
<view class="chat-bubble user">
|
||||
<text>{{ wishText }}</text>
|
||||
<text class="bubble-time">{{ currentMessageTime }}</text>
|
||||
</view>
|
||||
<view class="chat-bubble system done">
|
||||
<text>心愿已实现,故事已为你展开</text>
|
||||
<text class="bubble-time">{{ currentResultTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<MessageCard
|
||||
:content="displayedResultContent"
|
||||
:title="currentResult?.title"
|
||||
:tags="resultTags"
|
||||
:collapsed="storyCollapsed"
|
||||
:content-length="displayedResultContent.length"
|
||||
:is-short-message="false"
|
||||
:tts-icon="ttsActionIcon"
|
||||
:tts-text="ttsActionText"
|
||||
@toggle-collapse="toggleStoryCollapse"
|
||||
@copy="copyResultContent"
|
||||
@change-direction="changeDirection"
|
||||
@not-like-me="notLikeMe"
|
||||
@continue="continueInChat"
|
||||
@play-tts="trackTtsClick"
|
||||
/>
|
||||
|
||||
<view v-if="resultMessages.length" class="result-chat-list">
|
||||
<view
|
||||
v-for="message in resultMessages"
|
||||
:key="message.id"
|
||||
>
|
||||
<MessageCard
|
||||
v-if="isAssistantMessage(message)"
|
||||
:content="message.content"
|
||||
:collapsed="isMessageCollapsed(message)"
|
||||
:content-length="message.content.length"
|
||||
:is-short-message="message.content.length < 100"
|
||||
:tts-icon="ttsPlayer.playing.value ? 'Ⅱ' : '▶'"
|
||||
:tts-text="ttsPlayer.playing.value ? '暂停' : '播放'"
|
||||
@toggle-collapse="toggleMessageCollapse(message)"
|
||||
@copy="copyMessageContent(message)"
|
||||
@change-direction="changeDirection"
|
||||
@not-like-me="notLikeMe"
|
||||
@continue="continueInChat"
|
||||
@play-tts="playMessageTts(message)"
|
||||
/>
|
||||
<view v-else class="chat-bubble user">
|
||||
<text>{{ message.content }}</text>
|
||||
<text class="bubble-time">{{ message.time }}</text>
|
||||
<!-- 阅读模式 -->
|
||||
<view v-if="viewMode === 'read'">
|
||||
<view class="version-bar">
|
||||
<view
|
||||
v-for="v in versions"
|
||||
:key="v.id"
|
||||
class="version-tag"
|
||||
:class="{ active: selectedVersionMessageId === v.id, current: currentVersionMessageId === v.id }"
|
||||
@click="onSelectVersion(v.id)"
|
||||
>
|
||||
<text>V{{ v.versionNumber }}</text>
|
||||
<text v-if="currentVersionMessageId === v.id" class="version-current-label">(当前)</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="resultChatting" class="generation-loading">
|
||||
<view class="loading-orbit streaming">
|
||||
<view class="orbit-ring outer"></view>
|
||||
<view class="orbit-ring inner"></view>
|
||||
<view class="orbit-core"></view>
|
||||
<view class="script-read-content">
|
||||
<view class="script-title">{{ currentScriptContent.title }}</view>
|
||||
<view v-if="currentScriptContent.plotIntro" class="script-section">
|
||||
<view class="script-section-title">序幕:低谷回响</view>
|
||||
<view class="script-section-body">{{ currentScriptContent.plotIntro }}</view>
|
||||
</view>
|
||||
<view v-if="currentScriptContent.plotTurning" class="script-section">
|
||||
<view class="script-section-title">转折:契机出现</view>
|
||||
<view class="script-section-body">{{ currentScriptContent.plotTurning }}</view>
|
||||
</view>
|
||||
<view v-if="currentScriptContent.plotClimax" class="script-section">
|
||||
<view class="script-section-title">高潮:命运抉择</view>
|
||||
<view class="script-section-body">{{ currentScriptContent.plotClimax }}</view>
|
||||
</view>
|
||||
<view v-if="currentScriptContent.plotEnding" class="script-section">
|
||||
<view class="script-section-title">结局:新的开始</view>
|
||||
<view class="script-section-body">{{ currentScriptContent.plotEnding }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="read-action-bar">
|
||||
<view class="read-action-btn" @click="enterChatMode">进入对话修改</view>
|
||||
<view class="read-action-btn secondary" @click="rewriteCurrentVersion">改写当前版本</view>
|
||||
<view class="read-action-btn secondary" @click="continueCurrentVersion">续写当前版本</view>
|
||||
<view v-if="selectedVersionMessageId && selectedVersionMessageId !== currentVersionMessageId" class="read-action-btn primary" @click="setAsCurrentVersion">设为当前版本</view>
|
||||
</view>
|
||||
<text class="loading-copy">正在为你重写人生</text>
|
||||
<text class="loading-subcopy">AI 正在思考下一步方向</text>
|
||||
</view>
|
||||
|
||||
<view :id="resultScrollAnchor" class="result-scroll-anchor"></view>
|
||||
<!-- 聊天模式 -->
|
||||
<view v-else-if="viewMode === 'chat'">
|
||||
<view class="conversation compact">
|
||||
<view class="chat-bubble user">
|
||||
<text>{{ wishText }}</text>
|
||||
<text class="bubble-time">{{ currentMessageTime }}</text>
|
||||
</view>
|
||||
<view class="chat-bubble system done">
|
||||
<text>心愿已实现,故事已为你展开</text>
|
||||
<text class="bubble-time">{{ currentResultTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="displayMessages.length" class="result-chat-list">
|
||||
<view
|
||||
v-for="message in displayMessages"
|
||||
:key="message.id"
|
||||
>
|
||||
<MessageCard
|
||||
v-if="message.sender === 'assistant' || message.type === 'script'"
|
||||
:content="message.content"
|
||||
:collapsed="isMessageCollapsed(message)"
|
||||
:content-length="message.content.length"
|
||||
:is-short-message="message.content.length < 100"
|
||||
:tts-icon="ttsPlayer.playing.value ? 'Ⅱ' : '▶'"
|
||||
:tts-text="ttsPlayer.playing.value ? '暂停' : '播放'"
|
||||
@toggle-collapse="toggleMessageCollapse(message)"
|
||||
@copy="copyMessageContent(message)"
|
||||
@change-direction="changeDirection"
|
||||
@not-like-me="notLikeMe"
|
||||
@continue="continueMessage(message)"
|
||||
@play-tts="playMessageTts(message)"
|
||||
/>
|
||||
<view v-else class="chat-bubble user">
|
||||
<text>{{ message.content }}</text>
|
||||
<text class="bubble-time">{{ message.time || formatMessageTime() }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="generating" class="generation-loading">
|
||||
<view class="loading-orbit streaming">
|
||||
<view class="orbit-ring outer"></view>
|
||||
<view class="orbit-ring inner"></view>
|
||||
<view class="orbit-core"></view>
|
||||
</view>
|
||||
<text class="loading-copy">正在为你重写人生</text>
|
||||
<text class="loading-subcopy">AI 正在思考下一步方向</text>
|
||||
</view>
|
||||
|
||||
<view :id="resultScrollAnchor" class="result-scroll-anchor"></view>
|
||||
</view>
|
||||
|
||||
<!-- 兼容旧模式 -->
|
||||
<view v-else>
|
||||
<view class="conversation compact">
|
||||
<view class="chat-bubble user">
|
||||
<text>{{ wishText }}</text>
|
||||
<text class="bubble-time">{{ currentMessageTime }}</text>
|
||||
</view>
|
||||
<view class="chat-bubble system done">
|
||||
<text>心愿已实现,故事已为你展开</text>
|
||||
<text class="bubble-time">{{ currentResultTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<MessageCard
|
||||
:content="displayedResultContent"
|
||||
:title="currentResult?.title"
|
||||
:tags="resultTags"
|
||||
:collapsed="storyCollapsed"
|
||||
:content-length="displayedResultContent.length"
|
||||
:is-short-message="false"
|
||||
:tts-icon="ttsActionIcon"
|
||||
:tts-text="ttsActionText"
|
||||
@toggle-collapse="toggleStoryCollapse"
|
||||
@copy="copyResultContent"
|
||||
@change-direction="changeDirection"
|
||||
@not-like-me="notLikeMe"
|
||||
@continue="continueInChat"
|
||||
@play-tts="trackTtsClick"
|
||||
/>
|
||||
|
||||
<view v-if="resultMessages.length" class="result-chat-list">
|
||||
<view
|
||||
v-for="message in resultMessages"
|
||||
:key="message.id"
|
||||
>
|
||||
<MessageCard
|
||||
v-if="isAssistantMessage(message)"
|
||||
:content="message.content"
|
||||
:collapsed="isMessageCollapsed(message)"
|
||||
:content-length="message.content.length"
|
||||
:is-short-message="message.content.length < 100"
|
||||
:tts-icon="ttsPlayer.playing.value ? 'Ⅱ' : '▶'"
|
||||
:tts-text="ttsPlayer.playing.value ? '暂停' : '播放'"
|
||||
@toggle-collapse="toggleMessageCollapse(message)"
|
||||
@copy="copyMessageContent(message)"
|
||||
@change-direction="changeDirection"
|
||||
@not-like-me="notLikeMe"
|
||||
@continue="continueInChat"
|
||||
@play-tts="playMessageTts(message)"
|
||||
/>
|
||||
<view v-else class="chat-bubble user">
|
||||
<text>{{ message.content }}</text>
|
||||
<text class="bubble-time">{{ message.time }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="resultChatting" class="generation-loading">
|
||||
<view class="loading-orbit streaming">
|
||||
<view class="orbit-ring outer"></view>
|
||||
<view class="orbit-ring inner"></view>
|
||||
<view class="orbit-core"></view>
|
||||
</view>
|
||||
<text class="loading-copy">正在为你重写人生</text>
|
||||
<text class="loading-subcopy">AI 正在思考下一步方向</text>
|
||||
</view>
|
||||
|
||||
<view :id="resultScrollAnchor" class="result-scroll-anchor"></view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
@@ -232,7 +331,36 @@
|
||||
<view v-if="showScrollBottomBtn" class="scroll-jump-btn" @click="scrollResultToBottom">底部</view>
|
||||
</view>
|
||||
|
||||
<view class="result-chat-bar">
|
||||
<!-- 聊天模式输入栏 -->
|
||||
<view v-if="viewMode === 'chat'" class="result-chat-bar">
|
||||
<view
|
||||
class="chat-voice-btn"
|
||||
:class="{ pressing: voiceState === 'pressing', recognizing: voiceState === 'recognizing' }"
|
||||
@touchstart.prevent="startVoicePress"
|
||||
@touchend.prevent="endVoicePress"
|
||||
@touchcancel.prevent="cancelVoicePress"
|
||||
>
|
||||
<text class="voice-icon">🎤</text>
|
||||
</view>
|
||||
<view class="result-input-shell" :class="{ twoLine: resultInputLevel === 'two', expanded: resultInputLevel === 'multi' }">
|
||||
<text v-if="!chatInput" class="custom-input-placeholder result-input-placeholder">输入修改建议,或让 AI 继续生成</text>
|
||||
<textarea
|
||||
class="result-chat-input"
|
||||
v-model="chatInput"
|
||||
:auto-height="resultInputLevel !== 'single'"
|
||||
:show-confirm-bar="false"
|
||||
maxlength="500"
|
||||
confirm-type="send"
|
||||
@focus="resultInputFocused = true"
|
||||
@blur="resultInputFocused = false"
|
||||
@confirm="sendChat"
|
||||
/>
|
||||
</view>
|
||||
<view class="chat-send-btn" :class="{ disabled: !chatInput.trim() || generating }" @click="sendChat">发送</view>
|
||||
</view>
|
||||
|
||||
<!-- 非聊天模式输入栏(兼容旧模式) -->
|
||||
<view v-else class="result-chat-bar">
|
||||
<view
|
||||
class="chat-voice-btn"
|
||||
:class="{ pressing: voiceState === 'pressing', recognizing: voiceState === 'recognizing' }"
|
||||
@@ -273,6 +401,15 @@ import { useTypewriterStream } from '../../composables/useTypewriterStream.js'
|
||||
import { transcribeAudio } from '../../services/asr.js'
|
||||
import { streamAiScene } from '../../services/aiRuntime.js'
|
||||
import MessageCard from '../../components/MessageCard.vue'
|
||||
import {
|
||||
createScriptWithDialogue,
|
||||
streamScriptChat,
|
||||
listMessagesByConversation,
|
||||
listMessageVersions,
|
||||
switchVersion,
|
||||
deleteVersion
|
||||
} from '../../services/scriptChat.js'
|
||||
import { createMessage } from '../../services/message.js'
|
||||
|
||||
const store = useAppStore()
|
||||
const pagePath = '/pages/main/ScriptView'
|
||||
@@ -319,6 +456,14 @@ const remainingCount = ref(3)
|
||||
const style = ref('career')
|
||||
const randomRecommendations = ref([])
|
||||
const useSocialInsights = ref(true)
|
||||
const viewMode = ref('read')
|
||||
const scriptId = ref('')
|
||||
const conversationId = ref('')
|
||||
const currentVersionMessageId = ref('')
|
||||
const selectedVersionMessageId = ref('')
|
||||
const messages = ref([])
|
||||
const versions = ref([])
|
||||
const chatInput = ref('')
|
||||
const ttsPlayer = useTtsPlayer({ pagePath })
|
||||
let recorderManager = null
|
||||
let recordStartedAt = 0
|
||||
@@ -461,6 +606,235 @@ const resultHasScriptReplies = computed(() => {
|
||||
return resultMessages.value.some(item => item.role === 'assistant' && item.kind === 'script')
|
||||
})
|
||||
|
||||
const currentScriptContent = computed(() => {
|
||||
const targetId = selectedVersionMessageId.value || currentVersionMessageId.value
|
||||
const targetMessage = messages.value.find(m => m.id === targetId && m.type === 'script')
|
||||
if (!targetMessage || !targetMessage.metadata) {
|
||||
return {
|
||||
title: currentResult.value?.title || '',
|
||||
plotIntro: currentResult.value?.plotIntro || '',
|
||||
plotTurning: currentResult.value?.plotTurning || '',
|
||||
plotClimax: currentResult.value?.plotClimax || '',
|
||||
plotEnding: currentResult.value?.plotEnding || ''
|
||||
}
|
||||
}
|
||||
try {
|
||||
return JSON.parse(targetMessage.metadata)
|
||||
} catch (e) {
|
||||
return { title: '', plotIntro: targetMessage.metadata || '' }
|
||||
}
|
||||
})
|
||||
|
||||
const displayMessages = computed(() => {
|
||||
return messages.value.filter(m => m.type !== 'script' || !m.parentMessageId)
|
||||
})
|
||||
|
||||
const onSelectVersion = (messageId) => {
|
||||
selectedVersionMessageId.value = messageId
|
||||
}
|
||||
|
||||
const setAsCurrentVersion = async () => {
|
||||
try {
|
||||
await switchVersion({ scriptId: scriptId.value, messageId: selectedVersionMessageId.value })
|
||||
currentVersionMessageId.value = selectedVersionMessageId.value
|
||||
uni.showToast({ title: '已设为当前版本', icon: 'success' })
|
||||
} catch (error) {
|
||||
uni.showToast({ title: error.message || '切换失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const enterChatMode = () => {
|
||||
viewMode.value = 'chat'
|
||||
loadMessages()
|
||||
}
|
||||
|
||||
const rewriteCurrentVersion = () => {
|
||||
selectedVersionMessageId.value = currentVersionMessageId.value
|
||||
viewMode.value = 'chat'
|
||||
loadMessages()
|
||||
}
|
||||
|
||||
const continueCurrentVersion = () => {
|
||||
selectedVersionMessageId.value = currentVersionMessageId.value
|
||||
viewMode.value = 'chat'
|
||||
loadMessages()
|
||||
}
|
||||
|
||||
const loadMessages = async () => {
|
||||
if (!conversationId.value) return
|
||||
try {
|
||||
const res = await listMessagesByConversation({ conversationId: conversationId.value, includeVersions: false })
|
||||
messages.value = res.data || []
|
||||
const scriptMessages = messages.value.filter(m => m.type === 'script')
|
||||
if (scriptMessages.length > 0) {
|
||||
const rootMessage = scriptMessages[0]
|
||||
const versionRes = await listMessageVersions(rootMessage.id)
|
||||
versions.value = versionRes.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({ title: error.message || '加载消息失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const sendChat = async () => {
|
||||
if (!chatInput.value.trim() || generating.value) return
|
||||
const content = chatInput.value.trim()
|
||||
chatInput.value = ''
|
||||
const messageId = currentVersionMessageId.value
|
||||
|
||||
const userMessageRes = await createMessage({
|
||||
conversationId: conversationId.value,
|
||||
content,
|
||||
sender: 'user',
|
||||
type: 'chat'
|
||||
})
|
||||
const userMessageId = userMessageRes.data?.id
|
||||
|
||||
messages.value.push({
|
||||
id: userMessageId,
|
||||
conversationId: conversationId.value,
|
||||
content,
|
||||
sender: 'user',
|
||||
type: 'chat'
|
||||
})
|
||||
|
||||
generating.value = true
|
||||
await streamScriptChat({
|
||||
operationType: 'chat',
|
||||
conversationId: conversationId.value,
|
||||
messageId,
|
||||
userMessageId,
|
||||
content,
|
||||
scriptId: scriptId.value,
|
||||
onDelta: () => {},
|
||||
onDone: () => {
|
||||
generating.value = false
|
||||
loadMessages()
|
||||
},
|
||||
onError: (error) => {
|
||||
generating.value = false
|
||||
uni.showToast({ title: error || '生成失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const rewriteMessage = (msg) => {
|
||||
chatInput.value = ''
|
||||
uni.showModal({
|
||||
title: '改写意图',
|
||||
editable: true,
|
||||
placeholderText: '例如:把结局改得更积极',
|
||||
success: async (res) => {
|
||||
if (res.confirm && res.content) {
|
||||
await sendRewrite(msg.id, res.content)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const sendRewrite = async (targetMessageId, content) => {
|
||||
const userMessageRes = await createMessage({
|
||||
conversationId: conversationId.value,
|
||||
content,
|
||||
sender: 'user',
|
||||
type: 'chat'
|
||||
})
|
||||
const userMessageId = userMessageRes.data?.id
|
||||
|
||||
messages.value.push({
|
||||
id: userMessageId,
|
||||
conversationId: conversationId.value,
|
||||
content,
|
||||
sender: 'user',
|
||||
type: 'chat'
|
||||
})
|
||||
|
||||
generating.value = true
|
||||
await streamScriptChat({
|
||||
operationType: 'rewrite',
|
||||
conversationId: conversationId.value,
|
||||
messageId: targetMessageId,
|
||||
userMessageId,
|
||||
content,
|
||||
scriptId: scriptId.value,
|
||||
onDelta: () => {},
|
||||
onDone: () => {
|
||||
generating.value = false
|
||||
loadMessages()
|
||||
},
|
||||
onError: (error) => {
|
||||
generating.value = false
|
||||
uni.showToast({ title: error || '改写失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const continueMessage = (msg) => {
|
||||
chatInput.value = '继续生成后续内容'
|
||||
sendChatAsContinue(msg.id)
|
||||
}
|
||||
|
||||
const sendChatAsContinue = async (targetMessageId) => {
|
||||
const content = '继续生成后续内容'
|
||||
const userMessageRes = await createMessage({
|
||||
conversationId: conversationId.value,
|
||||
content,
|
||||
sender: 'user',
|
||||
type: 'chat'
|
||||
})
|
||||
const userMessageId = userMessageRes.data?.id
|
||||
|
||||
generating.value = true
|
||||
await streamScriptChat({
|
||||
operationType: 'continue',
|
||||
conversationId: conversationId.value,
|
||||
messageId: targetMessageId,
|
||||
userMessageId,
|
||||
content,
|
||||
scriptId: scriptId.value,
|
||||
onDelta: () => {},
|
||||
onDone: () => {
|
||||
generating.value = false
|
||||
loadMessages()
|
||||
},
|
||||
onError: (error) => {
|
||||
generating.value = false
|
||||
uni.showToast({ title: error || '续写失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const showVersions = async (msg) => {
|
||||
const res = await listMessageVersions(msg.id)
|
||||
const list = res.data || []
|
||||
const items = list.map(v => `V${v.versionNumber} ${v.id === currentVersionMessageId.value ? '(当前)' : ''}`)
|
||||
uni.showActionSheet({
|
||||
itemList: items,
|
||||
success: (res) => {
|
||||
const selected = list[res.tapIndex]
|
||||
selectedVersionMessageId.value = selected.id
|
||||
viewMode.value = 'read'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const removeVersion = async (msg) => {
|
||||
if (msg.id === currentVersionMessageId.value) {
|
||||
uni.showToast({ title: '不能删除当前生效版本', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: `确定删除 V${msg.versionNumber} 吗?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
await deleteVersion(msg.id)
|
||||
await loadMessages()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const measureResultViewport = () => {
|
||||
nextTick(() => {
|
||||
const query = uni.createSelectorQuery()
|
||||
@@ -851,45 +1225,22 @@ const getScriptChatHistoryKey = (conversationId = getCurrentConversationId()) =>
|
||||
}
|
||||
|
||||
const readStoredMessages = (key) => {
|
||||
if (!key) return []
|
||||
const value = uni.getStorageSync(key)
|
||||
return Array.isArray(value) ? value.filter(item => {
|
||||
if (!item?.role) return false
|
||||
if (item.role === 'script') return false
|
||||
return typeof item.content === 'string'
|
||||
}) : []
|
||||
// localStorage 不再作为主要消息存储
|
||||
return []
|
||||
}
|
||||
|
||||
const readScriptChatHistory = ({ conversationId, scriptId } = {}) => {
|
||||
const messages = readStoredMessages(getScriptChatHistoryKey(conversationId))
|
||||
if (messages.length || !scriptId) return messages
|
||||
return readStoredMessages(`${LEGACY_SCRIPT_CHAT_HISTORY_PREFIX}${scriptId}`)
|
||||
// localStorage 不再作为主要消息存储
|
||||
return []
|
||||
}
|
||||
|
||||
const persistResultMessages = () => {
|
||||
const key = getScriptChatHistoryKey()
|
||||
if (!key) return
|
||||
const messages = resultMessages.value
|
||||
.filter(item => item?.role && item.role !== 'script' && !item.pending && item.content)
|
||||
.map(item => ({
|
||||
id: item.id,
|
||||
role: item.role,
|
||||
kind: item.kind || '',
|
||||
content: item.content,
|
||||
time: item.time
|
||||
}))
|
||||
uni.setStorageSync(key, messages)
|
||||
// localStorage 不再作为主要消息存储,消息通过后端 API 管理
|
||||
}
|
||||
|
||||
const hydrateResultMessages = ({ conversationId, scriptId } = {}) => {
|
||||
resultMessages.value = readScriptChatHistory({ conversationId, scriptId }).map(item => ({
|
||||
id: item.id || createMessageId(),
|
||||
role: item.role,
|
||||
kind: item.kind || '',
|
||||
content: item.content || '',
|
||||
pending: false,
|
||||
time: item.time || formatMessageTime()
|
||||
}))
|
||||
// localStorage 不再作为主要消息存储,消息通过后端 API 管理
|
||||
resultMessages.value = []
|
||||
collapsedMessageIds.value = {}
|
||||
}
|
||||
|
||||
@@ -1076,13 +1427,13 @@ const normalizeGeneratedContent = (text) => {
|
||||
|
||||
const openScriptChat = async (payload = {}) => {
|
||||
uni.removeStorageSync(PENDING_SCRIPT_CHAT_KEY)
|
||||
const scriptId = String(payload?.id || payload?.scriptId || '')
|
||||
const scriptIdParam = String(payload?.id || payload?.scriptId || '')
|
||||
let script = payload?.script || (payload?.id ? payload : null)
|
||||
if (!script?.id && scriptId) {
|
||||
script = store.getScriptById(scriptId)
|
||||
if (!script?.id && scriptIdParam) {
|
||||
script = store.getScriptById(scriptIdParam)
|
||||
if (!script) {
|
||||
await store.fetchScripts()
|
||||
script = store.getScriptById(scriptId)
|
||||
script = store.getScriptById(scriptIdParam)
|
||||
}
|
||||
}
|
||||
if (!script?.id) return
|
||||
@@ -1102,15 +1453,21 @@ const openScriptChat = async (payload = {}) => {
|
||||
ttsPlayer.reset()
|
||||
|
||||
currentResult.value = normalizeGeneratedScript({ script })
|
||||
currentConversationId.value = ensureConversationId(currentResult.value)
|
||||
scriptId.value = script.id
|
||||
conversationId.value = script.conversationId || ''
|
||||
currentVersionMessageId.value = script.currentVersionMessageId || ''
|
||||
selectedVersionMessageId.value = script.currentVersionMessageId || ''
|
||||
wishText.value = script.theme || script.prompt || script.title || '继续修改这个人生剧本'
|
||||
currentMessageTime.value = formatMessageTime()
|
||||
currentResultTime.value = script.updateTime || script.updatedAt || script.createTime || script.createdAt || formatMessageTime()
|
||||
hydrateResultMessages({ conversationId: currentConversationId.value, scriptId: script.id })
|
||||
viewMode.value = 'read'
|
||||
viewState.value = 'result'
|
||||
if (conversationId.value) {
|
||||
await loadMessages()
|
||||
}
|
||||
analytics.track('script_chat_open_from_library', {
|
||||
script_id: script.id,
|
||||
has_history: resultMessages.value.length > 0
|
||||
has_history: messages.value.length > 0
|
||||
}, { eventType: 'script', pagePath })
|
||||
scrollResultToLatest()
|
||||
}
|
||||
@@ -1233,21 +1590,10 @@ const generateScriptByStream = async (text, options = {}) => {
|
||||
})
|
||||
}
|
||||
|
||||
const runGeneration = async ({ prompt, displayText, source = 'text', saveTheme, revision = false }) => {
|
||||
const runGeneration = async ({ prompt, displayText, source = 'text', saveTheme }) => {
|
||||
const text = String(prompt || '').trim()
|
||||
const display = String(displayText || text).trim()
|
||||
if (!text || generating.value) return
|
||||
const isRevision = Boolean(revision)
|
||||
const conversationId = isRevision ? ensureConversationId() : createConversationId()
|
||||
const parentScriptId = isRevision ? getCurrentScriptId() : ''
|
||||
const revisionIndex = isRevision
|
||||
? getScriptVersions().length + 1
|
||||
: 1
|
||||
currentConversationId.value = conversationId
|
||||
lastGenerationRevision.value = isRevision
|
||||
lastSubmitSource.value = source
|
||||
generationDisplayText.value = display
|
||||
lastGenerationPrompt.value = text
|
||||
|
||||
analytics.track('script_wish_submit', {
|
||||
source,
|
||||
@@ -1267,75 +1613,58 @@ const runGeneration = async ({ prompt, displayText, source = 'text', saveTheme,
|
||||
ttsPlayer.reset()
|
||||
viewState.value = 'generating'
|
||||
keepGenerationAtBottom()
|
||||
analytics.track('script_generation_progress_view', {
|
||||
source,
|
||||
prompt_length: display.length
|
||||
}, { eventType: 'script', pagePath })
|
||||
|
||||
let res
|
||||
try {
|
||||
res = await generateScriptByStream(text, {
|
||||
saveTheme: saveTheme || display,
|
||||
conversationId,
|
||||
parentScriptId,
|
||||
revisionIndex,
|
||||
revisionOf: parentScriptId,
|
||||
updateScriptId: isRevision ? getCurrentScriptId() : ''
|
||||
})
|
||||
} catch (streamError) {
|
||||
markGenerationFailed(streamError?.message || 'AI 流式生成失败')
|
||||
analytics.track('script_generate_stream_fail', {
|
||||
source,
|
||||
error: streamError?.message || 'stream_failed'
|
||||
}, { eventType: 'script', pagePath })
|
||||
res = {
|
||||
success: false,
|
||||
error: streamError?.message || 'AI 流式生成失败'
|
||||
const payload = {
|
||||
theme: text,
|
||||
style: style.value || 'career',
|
||||
length: 'medium',
|
||||
characterInfo: '',
|
||||
lifeEventsSummary: '',
|
||||
useSocialInsights: useSocialInsights.value
|
||||
}
|
||||
}
|
||||
|
||||
generating.value = false
|
||||
if (res.success) {
|
||||
clearGenerationFeedbackTimers()
|
||||
if (streamContent.value) streamWriter.finish(normalizeGeneratedContent(streamContent.value))
|
||||
keepGenerationAtBottom()
|
||||
}
|
||||
const res = await createScriptWithDialogue(payload)
|
||||
const data = res.data
|
||||
|
||||
if (!res.success) {
|
||||
analytics.track('script_generate_fail', {
|
||||
scriptId.value = data.scriptId
|
||||
conversationId.value = data.conversationId
|
||||
currentVersionMessageId.value = data.currentVersionMessageId
|
||||
selectedVersionMessageId.value = data.currentVersionMessageId
|
||||
|
||||
currentResult.value = {
|
||||
id: data.scriptId,
|
||||
title: data.title,
|
||||
theme: data.theme,
|
||||
style: data.style,
|
||||
length: data.length,
|
||||
plotIntro: data.plotIntro,
|
||||
plotTurning: data.plotTurning,
|
||||
plotClimax: data.plotClimax,
|
||||
plotEnding: data.plotEnding
|
||||
}
|
||||
|
||||
await loadMessages()
|
||||
viewMode.value = 'read'
|
||||
viewState.value = 'result'
|
||||
|
||||
analytics.track('script_generate_success', {
|
||||
source,
|
||||
error: res.error || 'unknown',
|
||||
style: currentResult.value.style || '',
|
||||
length: currentResult.value.length || '',
|
||||
use_social_insights: useSocialInsights.value,
|
||||
duration_ms: Date.now() - generationStartedAt.value
|
||||
}, { eventType: 'script', pagePath })
|
||||
markGenerationFailed(res.error || '生成失败')
|
||||
return
|
||||
} catch (error) {
|
||||
markGenerationFailed(error.message || '生成失败')
|
||||
analytics.track('script_generate_fail', {
|
||||
source,
|
||||
error: error.message || 'unknown'
|
||||
}, { eventType: 'script', pagePath })
|
||||
} finally {
|
||||
generating.value = false
|
||||
generationStatus.value = 'idle'
|
||||
}
|
||||
|
||||
currentResult.value = normalizeGeneratedScript(res.data)
|
||||
currentConversationId.value = ensureConversationId(currentResult.value)
|
||||
currentResultTime.value = formatMessageTime()
|
||||
if (!isRevision) {
|
||||
resultMessages.value = []
|
||||
collapsedMessageIds.value = {}
|
||||
}
|
||||
if (typeof res.data?.remainingCount === 'number') remainingCount.value = res.data.remainingCount
|
||||
|
||||
analytics.track('script_generate_success', {
|
||||
source,
|
||||
style: currentResult.value.style || '',
|
||||
length: currentResult.value.length || '',
|
||||
use_social_insights: useSocialInsights.value,
|
||||
duration_ms: Date.now() - generationStartedAt.value
|
||||
}, { eventType: 'script', pagePath })
|
||||
analytics.track('script_result_view', {
|
||||
script_id: currentResult.value.id || '',
|
||||
style: currentResult.value.style || '',
|
||||
length: currentResult.value.length || ''
|
||||
}, { eventType: 'script', pagePath })
|
||||
|
||||
generationStatus.value = 'idle'
|
||||
viewState.value = 'result'
|
||||
keepResultAtBottom()
|
||||
}
|
||||
|
||||
const submitWish = async (source = 'text') => {
|
||||
@@ -2845,4 +3174,113 @@ onUnmounted(() => {
|
||||
.chat-send-btn.disabled {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.version-bar {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.version-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 10rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(43, 19, 83, 0.46);
|
||||
border: 1rpx solid rgba(168, 85, 247, 0.24);
|
||||
color: rgba(232, 204, 255, 0.72);
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.version-tag.active {
|
||||
background: rgba(140, 68, 242, 0.52);
|
||||
border-color: rgba(209, 138, 255, 0.52);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.version-tag.current {
|
||||
border-color: rgba(255, 216, 107, 0.42);
|
||||
}
|
||||
|
||||
.version-current-label {
|
||||
font-size: 22rpx;
|
||||
color: #ffd86b;
|
||||
}
|
||||
|
||||
.script-read-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 28rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.script-title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.script-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 28rpx;
|
||||
background: rgba(16, 8, 34, 0.28);
|
||||
border: 1rpx solid rgba(192, 132, 252, 0.2);
|
||||
}
|
||||
|
||||
.script-section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #d18aff;
|
||||
}
|
||||
|
||||
.script-section-body {
|
||||
font-size: 30rpx;
|
||||
line-height: 48rpx;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.read-action-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.read-action-btn {
|
||||
flex: 1;
|
||||
min-width: 176rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999rpx;
|
||||
font-size: 27rpx;
|
||||
font-weight: 700;
|
||||
color: #1b0b31;
|
||||
background: linear-gradient(145deg, #ffd86b, #d18aff);
|
||||
box-shadow: 0 16rpx 36rpx rgba(209, 138, 255, 0.24);
|
||||
}
|
||||
|
||||
.read-action-btn.secondary {
|
||||
color: #e8ccff;
|
||||
background: rgba(43, 19, 83, 0.68);
|
||||
border: 1rpx solid rgba(168, 85, 247, 0.32);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.read-action-btn.primary {
|
||||
color: #1b0b31;
|
||||
background: linear-gradient(145deg, #934dff, #4d1ccb);
|
||||
box-shadow: 0 16rpx 36rpx rgba(168, 85, 247, 0.24);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user