diff --git a/mini-program/src/pages/main/ScriptView.vue b/mini-program/src/pages/main/ScriptView.vue index f06e383..2101f84 100644 --- a/mini-program/src/pages/main/ScriptView.vue +++ b/mini-program/src/pages/main/ScriptView.vue @@ -290,6 +290,8 @@ const currentVersionMessageId = ref('') const novelSessionId = ref('') const novelFullText = ref('') const novelOutline = ref(null) +// novel 消息 id → typewriter writer 实例映射(用于真正的逐字流式输出) +const novelWriters = new Map() const clarificationCard = ref(null) const currentStreamTask = ref(null) const answeringClarification = ref(false) @@ -1705,16 +1707,27 @@ const handleShortNovelEvent = (event) => { generationStatus.value = 'idle' pendingNextResponse.value = false break - case 'novel_start': - addResultMessage({ role: 'assistant', kind: 'novel', content: '', pending: true }) + case 'novel_start': { + const novelMsg = addResultMessage({ role: 'assistant', kind: 'novel', content: '', pending: true }) + // 为这条 novel 消息创建独立的 typewriter writer,实现逐字流式输出 + novelWriters.set(novelMsg.id, useTypewriterStream({ interval: 32, step: 1 })) generationStatus.value = 'streaming' pendingNextResponse.value = false break + } case 'novel_delta': { - // 往最后一条 novel 消息追加 delta + // 往最后一条 pending novel 消息推送 delta 到 typewriter writer const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel' && m.pending) if (lastNovel) { - lastNovel.content += payload.delta || '' + const writer = novelWriters.get(lastNovel.id) + const delta = payload.delta || '' + if (writer) { + writer.push((writer.targetText?.value || '') + delta) + lastNovel.content = writer.visibleText.value + // writer 的 visibleText 是 reactive,需用 watch;但这里直接读取值即可 + } else { + lastNovel.content += delta + } } keepResultAtBottom() break @@ -1723,7 +1736,17 @@ const handleShortNovelEvent = (event) => { // 标记最后一条 novel 消息完成 const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel') if (lastNovel) { - if (payload.full_text) lastNovel.content = payload.full_text + const writer = novelWriters.get(lastNovel.id) + const finalText = payload.full_text || '' + if (writer) { + writer.finish(finalText) + // finish 后 typewriter 会逐字排空到 targetText,等排空完成再写回 content + // 但为了持久化和立即显示,这里直接同步设值 + lastNovel.content = finalText + novelWriters.delete(lastNovel.id) + } else if (finalText) { + lastNovel.content = finalText + } lastNovel.pending = false } scriptId.value = payload.scriptId || '' @@ -1775,6 +1798,7 @@ const submitClarification = (answer) => { const confirmOutline = (msg) => { if (msg) msg.confirmed = true + pendingNextResponse.value = true addResultMessage({ role: 'user', kind: 'text', content: '确认大纲' }) currentStreamTask.value = followupStream({ sessionId: novelSessionId.value, @@ -1792,6 +1816,7 @@ const modifyOutline = (msg) => { return } if (msg) msg.confirmed = true + pendingNextResponse.value = true addResultMessage({ role: 'user', kind: 'text', content: feedback }) currentStreamTask.value = followupStream({ sessionId: novelSessionId.value,