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,6 +158,104 @@
|
|||||||
<view class="result-scroll-content">
|
<view class="result-scroll-content">
|
||||||
<view id="result-scroll-top" class="result-scroll-top"></view>
|
<view id="result-scroll-top" class="result-scroll-top"></view>
|
||||||
|
|
||||||
|
<!-- 阅读模式 -->
|
||||||
|
<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 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>
|
||||||
|
</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="conversation compact">
|
||||||
<view class="chat-bubble user">
|
<view class="chat-bubble user">
|
||||||
<text>{{ wishText }}</text>
|
<text>{{ wishText }}</text>
|
||||||
@@ -225,6 +323,7 @@
|
|||||||
|
|
||||||
<view :id="resultScrollAnchor" class="result-scroll-anchor"></view>
|
<view :id="resultScrollAnchor" class="result-scroll-anchor"></view>
|
||||||
</view>
|
</view>
|
||||||
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
<view v-if="showScrollTopBtn || showScrollBottomBtn" class="scroll-jump-group">
|
<view v-if="showScrollTopBtn || showScrollBottomBtn" class="scroll-jump-group">
|
||||||
@@ -232,7 +331,36 @@
|
|||||||
<view v-if="showScrollBottomBtn" class="scroll-jump-btn" @click="scrollResultToBottom">底部</view>
|
<view v-if="showScrollBottomBtn" class="scroll-jump-btn" @click="scrollResultToBottom">底部</view>
|
||||||
</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
|
<view
|
||||||
class="chat-voice-btn"
|
class="chat-voice-btn"
|
||||||
:class="{ pressing: voiceState === 'pressing', recognizing: voiceState === 'recognizing' }"
|
:class="{ pressing: voiceState === 'pressing', recognizing: voiceState === 'recognizing' }"
|
||||||
@@ -273,6 +401,15 @@ import { useTypewriterStream } from '../../composables/useTypewriterStream.js'
|
|||||||
import { transcribeAudio } from '../../services/asr.js'
|
import { transcribeAudio } from '../../services/asr.js'
|
||||||
import { streamAiScene } from '../../services/aiRuntime.js'
|
import { streamAiScene } from '../../services/aiRuntime.js'
|
||||||
import MessageCard from '../../components/MessageCard.vue'
|
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 store = useAppStore()
|
||||||
const pagePath = '/pages/main/ScriptView'
|
const pagePath = '/pages/main/ScriptView'
|
||||||
@@ -319,6 +456,14 @@ const remainingCount = ref(3)
|
|||||||
const style = ref('career')
|
const style = ref('career')
|
||||||
const randomRecommendations = ref([])
|
const randomRecommendations = ref([])
|
||||||
const useSocialInsights = ref(true)
|
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 })
|
const ttsPlayer = useTtsPlayer({ pagePath })
|
||||||
let recorderManager = null
|
let recorderManager = null
|
||||||
let recordStartedAt = 0
|
let recordStartedAt = 0
|
||||||
@@ -461,6 +606,235 @@ const resultHasScriptReplies = computed(() => {
|
|||||||
return resultMessages.value.some(item => item.role === 'assistant' && item.kind === 'script')
|
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 = () => {
|
const measureResultViewport = () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const query = uni.createSelectorQuery()
|
const query = uni.createSelectorQuery()
|
||||||
@@ -851,45 +1225,22 @@ const getScriptChatHistoryKey = (conversationId = getCurrentConversationId()) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const readStoredMessages = (key) => {
|
const readStoredMessages = (key) => {
|
||||||
if (!key) return []
|
// localStorage 不再作为主要消息存储
|
||||||
const value = uni.getStorageSync(key)
|
return []
|
||||||
return Array.isArray(value) ? value.filter(item => {
|
|
||||||
if (!item?.role) return false
|
|
||||||
if (item.role === 'script') return false
|
|
||||||
return typeof item.content === 'string'
|
|
||||||
}) : []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const readScriptChatHistory = ({ conversationId, scriptId } = {}) => {
|
const readScriptChatHistory = ({ conversationId, scriptId } = {}) => {
|
||||||
const messages = readStoredMessages(getScriptChatHistoryKey(conversationId))
|
// localStorage 不再作为主要消息存储
|
||||||
if (messages.length || !scriptId) return messages
|
return []
|
||||||
return readStoredMessages(`${LEGACY_SCRIPT_CHAT_HISTORY_PREFIX}${scriptId}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const persistResultMessages = () => {
|
const persistResultMessages = () => {
|
||||||
const key = getScriptChatHistoryKey()
|
// localStorage 不再作为主要消息存储,消息通过后端 API 管理
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hydrateResultMessages = ({ conversationId, scriptId } = {}) => {
|
const hydrateResultMessages = ({ conversationId, scriptId } = {}) => {
|
||||||
resultMessages.value = readScriptChatHistory({ conversationId, scriptId }).map(item => ({
|
// localStorage 不再作为主要消息存储,消息通过后端 API 管理
|
||||||
id: item.id || createMessageId(),
|
resultMessages.value = []
|
||||||
role: item.role,
|
|
||||||
kind: item.kind || '',
|
|
||||||
content: item.content || '',
|
|
||||||
pending: false,
|
|
||||||
time: item.time || formatMessageTime()
|
|
||||||
}))
|
|
||||||
collapsedMessageIds.value = {}
|
collapsedMessageIds.value = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1076,13 +1427,13 @@ const normalizeGeneratedContent = (text) => {
|
|||||||
|
|
||||||
const openScriptChat = async (payload = {}) => {
|
const openScriptChat = async (payload = {}) => {
|
||||||
uni.removeStorageSync(PENDING_SCRIPT_CHAT_KEY)
|
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)
|
let script = payload?.script || (payload?.id ? payload : null)
|
||||||
if (!script?.id && scriptId) {
|
if (!script?.id && scriptIdParam) {
|
||||||
script = store.getScriptById(scriptId)
|
script = store.getScriptById(scriptIdParam)
|
||||||
if (!script) {
|
if (!script) {
|
||||||
await store.fetchScripts()
|
await store.fetchScripts()
|
||||||
script = store.getScriptById(scriptId)
|
script = store.getScriptById(scriptIdParam)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!script?.id) return
|
if (!script?.id) return
|
||||||
@@ -1102,15 +1453,21 @@ const openScriptChat = async (payload = {}) => {
|
|||||||
ttsPlayer.reset()
|
ttsPlayer.reset()
|
||||||
|
|
||||||
currentResult.value = normalizeGeneratedScript({ script })
|
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 || '继续修改这个人生剧本'
|
wishText.value = script.theme || script.prompt || script.title || '继续修改这个人生剧本'
|
||||||
currentMessageTime.value = formatMessageTime()
|
currentMessageTime.value = formatMessageTime()
|
||||||
currentResultTime.value = script.updateTime || script.updatedAt || script.createTime || script.createdAt || 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'
|
viewState.value = 'result'
|
||||||
|
if (conversationId.value) {
|
||||||
|
await loadMessages()
|
||||||
|
}
|
||||||
analytics.track('script_chat_open_from_library', {
|
analytics.track('script_chat_open_from_library', {
|
||||||
script_id: script.id,
|
script_id: script.id,
|
||||||
has_history: resultMessages.value.length > 0
|
has_history: messages.value.length > 0
|
||||||
}, { eventType: 'script', pagePath })
|
}, { eventType: 'script', pagePath })
|
||||||
scrollResultToLatest()
|
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 text = String(prompt || '').trim()
|
||||||
const display = String(displayText || text).trim()
|
const display = String(displayText || text).trim()
|
||||||
if (!text || generating.value) return
|
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', {
|
analytics.track('script_wish_submit', {
|
||||||
source,
|
source,
|
||||||
@@ -1267,58 +1613,40 @@ const runGeneration = async ({ prompt, displayText, source = 'text', saveTheme,
|
|||||||
ttsPlayer.reset()
|
ttsPlayer.reset()
|
||||||
viewState.value = 'generating'
|
viewState.value = 'generating'
|
||||||
keepGenerationAtBottom()
|
keepGenerationAtBottom()
|
||||||
analytics.track('script_generation_progress_view', {
|
|
||||||
source,
|
|
||||||
prompt_length: display.length
|
|
||||||
}, { eventType: 'script', pagePath })
|
|
||||||
|
|
||||||
let res
|
|
||||||
try {
|
try {
|
||||||
res = await generateScriptByStream(text, {
|
const payload = {
|
||||||
saveTheme: saveTheme || display,
|
theme: text,
|
||||||
conversationId,
|
style: style.value || 'career',
|
||||||
parentScriptId,
|
length: 'medium',
|
||||||
revisionIndex,
|
characterInfo: '',
|
||||||
revisionOf: parentScriptId,
|
lifeEventsSummary: '',
|
||||||
updateScriptId: isRevision ? getCurrentScriptId() : ''
|
useSocialInsights: useSocialInsights.value
|
||||||
})
|
|
||||||
} 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 流式生成失败'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
generating.value = false
|
const res = await createScriptWithDialogue(payload)
|
||||||
if (res.success) {
|
const data = res.data
|
||||||
clearGenerationFeedbackTimers()
|
|
||||||
if (streamContent.value) streamWriter.finish(normalizeGeneratedContent(streamContent.value))
|
scriptId.value = data.scriptId
|
||||||
keepGenerationAtBottom()
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res.success) {
|
await loadMessages()
|
||||||
analytics.track('script_generate_fail', {
|
viewMode.value = 'read'
|
||||||
source,
|
viewState.value = 'result'
|
||||||
error: res.error || 'unknown',
|
|
||||||
duration_ms: Date.now() - generationStartedAt.value
|
|
||||||
}, { eventType: 'script', pagePath })
|
|
||||||
markGenerationFailed(res.error || '生成失败')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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', {
|
analytics.track('script_generate_success', {
|
||||||
source,
|
source,
|
||||||
@@ -1327,15 +1655,16 @@ const runGeneration = async ({ prompt, displayText, source = 'text', saveTheme,
|
|||||||
use_social_insights: useSocialInsights.value,
|
use_social_insights: useSocialInsights.value,
|
||||||
duration_ms: Date.now() - generationStartedAt.value
|
duration_ms: Date.now() - generationStartedAt.value
|
||||||
}, { eventType: 'script', pagePath })
|
}, { eventType: 'script', pagePath })
|
||||||
analytics.track('script_result_view', {
|
} catch (error) {
|
||||||
script_id: currentResult.value.id || '',
|
markGenerationFailed(error.message || '生成失败')
|
||||||
style: currentResult.value.style || '',
|
analytics.track('script_generate_fail', {
|
||||||
length: currentResult.value.length || ''
|
source,
|
||||||
|
error: error.message || 'unknown'
|
||||||
}, { eventType: 'script', pagePath })
|
}, { eventType: 'script', pagePath })
|
||||||
|
} finally {
|
||||||
|
generating.value = false
|
||||||
generationStatus.value = 'idle'
|
generationStatus.value = 'idle'
|
||||||
viewState.value = 'result'
|
}
|
||||||
keepResultAtBottom()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitWish = async (source = 'text') => {
|
const submitWish = async (source = 'text') => {
|
||||||
@@ -2845,4 +3174,113 @@ onUnmounted(() => {
|
|||||||
.chat-send-btn.disabled {
|
.chat-send-btn.disabled {
|
||||||
opacity: 0.45;
|
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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user