fix: 大纲确认/修改时显示 loading + novel 消息使用 typewriter 逐字流式输出

- confirmOutline / modifyOutline 中设置 pendingNextResponse.value = true
  解决大纲确认/修改后等待响应期间无 loading 的问题
- novel_start 时为 novel 消息创建独立 typewriter writer 实例(novelWriters Map)
- novel_delta 时通过 writer.push() 推送增量,逐字渲染 visibleText
- novel_done 时 finish writer 并同步最终文本,清理 writer 实例

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-21 20:33:52 +08:00
parent 7b53821cec
commit 6afec090e4
+30 -5
View File
@@ -290,6 +290,8 @@ const currentVersionMessageId = ref('')
const novelSessionId = ref('') const novelSessionId = ref('')
const novelFullText = ref('') const novelFullText = ref('')
const novelOutline = ref(null) const novelOutline = ref(null)
// novel 消息 id → typewriter writer 实例映射(用于真正的逐字流式输出)
const novelWriters = new Map()
const clarificationCard = ref(null) const clarificationCard = ref(null)
const currentStreamTask = ref(null) const currentStreamTask = ref(null)
const answeringClarification = ref(false) const answeringClarification = ref(false)
@@ -1705,16 +1707,27 @@ const handleShortNovelEvent = (event) => {
generationStatus.value = 'idle' generationStatus.value = 'idle'
pendingNextResponse.value = false pendingNextResponse.value = false
break break
case 'novel_start': case 'novel_start': {
addResultMessage({ role: 'assistant', kind: 'novel', content: '', pending: true }) 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' generationStatus.value = 'streaming'
pendingNextResponse.value = false pendingNextResponse.value = false
break break
}
case 'novel_delta': { case 'novel_delta': {
// 往最后一条 novel 消息追加 delta // 往最后一条 pending novel 消息推送 delta 到 typewriter writer
const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel' && m.pending) const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel' && m.pending)
if (lastNovel) { 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() keepResultAtBottom()
break break
@@ -1723,7 +1736,17 @@ const handleShortNovelEvent = (event) => {
// 标记最后一条 novel 消息完成 // 标记最后一条 novel 消息完成
const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel') const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel')
if (lastNovel) { 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 lastNovel.pending = false
} }
scriptId.value = payload.scriptId || '' scriptId.value = payload.scriptId || ''
@@ -1775,6 +1798,7 @@ const submitClarification = (answer) => {
const confirmOutline = (msg) => { const confirmOutline = (msg) => {
if (msg) msg.confirmed = true if (msg) msg.confirmed = true
pendingNextResponse.value = true
addResultMessage({ role: 'user', kind: 'text', content: '确认大纲' }) addResultMessage({ role: 'user', kind: 'text', content: '确认大纲' })
currentStreamTask.value = followupStream({ currentStreamTask.value = followupStream({
sessionId: novelSessionId.value, sessionId: novelSessionId.value,
@@ -1792,6 +1816,7 @@ const modifyOutline = (msg) => {
return return
} }
if (msg) msg.confirmed = true if (msg) msg.confirmed = true
pendingNextResponse.value = true
addResultMessage({ role: 'user', kind: 'text', content: feedback }) addResultMessage({ role: 'user', kind: 'text', content: feedback })
currentStreamTask.value = followupStream({ currentStreamTask.value = followupStream({
sessionId: novelSessionId.value, sessionId: novelSessionId.value,