Files
happy-life-star/docs/superpowers/specs/2026-07-19-scriptview-unified-chat-design.md
T
peanut b2df7d4de0 docs:ScriptView 统一对话页改造设计
将澄清/大纲/小说生成从独立视图改为统一对话流消息。
viewState 简化为 home+chat,扩展 resultMessages 消息模型
支持 kind: text/card/outline/novel,novel_done 后切换旧接口
做后续对话修改。
2026-07-19 14:45:45 +08:00

250 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
author: AI Assistant
created_at: 2026-07-19
purpose: ScriptView 心愿实现页面统一对话流改造设计,将澄清/大纲/小说生成全部纳入单一对话页面
---
# ScriptView 统一对话页改造设计
## 概述
将小程序心愿实现页面(ScriptView.vue)的生成流程从"多独立视图切换"改为"单一对话页面流"。用户从提交心愿开始,始终停留在同一个对话页面,澄清问答、大纲确认/修改、小说流式生成、后续对话修改都作为对话消息依次出现在聊天流里。
## 背景与问题
### 当前架构(AS-IS
ScriptView.vue 用 `viewState` 在 5 个独立视图间切换:
| viewState | 内容 | 对话页? |
|---|---|---|
| `home` | 心愿输入首页(灵感推荐+输入框) | 否 |
| `generating` | 生成中加载页(用户气泡+loading orbit | 否 |
| `clarifying` | 澄清卡片(只有 ClarificationCard | 否 |
| `outlining` | 大纲确认(大纲+确认/修改按钮) | 否 |
| `result` | 结果对话页(MessageCard+用户气泡+输入栏) | 是 |
问题:澄清、大纲、小说生成阶段用户"不在对话页面",体验割裂。用户要求"心愿实现时必须一直处于对话的页面,必须支持对话和选择的澄清判断"。
### 可复用资产
`result` 视图已有完整对话机制:
- `resultMessages` 数组 + `addResultMessage({id, role, kind, content, pending, time})`
- `MessageCard` 组件渲染 AI 消息
- 用户气泡渲染用户消息
- 底部输入栏 `sendChat`(当前接旧接口 `streamScriptChat`
- 滚动控制(`keepResultAtBottom``resultScrollTop` 等)
## 目标架构(TO-BE
### viewState 简化
从 5 个视图简化为 **2 个**
```
home(首页:灵感推荐+输入框,保留不变)
↓ 用户提交心愿
chat(统一对话页:澄清→大纲→小说→后续修改 全在这里)
```
删除独立的 `generating`/`clarifying`/`outlining`/`novel-generating` 视图。
### 统一对话消息模型
复用 `resultMessages` + `addResultMessage`,扩展 `kind` 字段:
```javascript
{
id, // createMessageId()
role, // 'user' | 'assistant'
kind, // 'text' | 'card' | 'outline' | 'novel'
content, // 文本内容(text/novel
pending, // 是否加载中(novel 流式时 true)
time, // formatMessageTime()
card?, // kind:'card' 时携带澄清卡片数据
outline?, // kind:'outline' 时携带大纲数据
submitted?, // kind:'card' 时标记是否已提交(禁用交互)
confirmed?, // kind:'outline' 时标记是否已确认/修改(禁用交互)
failed? // 是否失败(error 事件)
}
```
### 消息类型与渲染
`chat` 视图对话区用 `v-for` 遍历 `resultMessages`,根据 `kind` 渲染不同气泡内容:
| kind | role | 渲染方式 |
|---|---|---|
| `text` | user | 用户气泡(`chat-bubble user` |
| `text` | assistant | AI 文本气泡(`chat-bubble system`),含状态提示/错误 |
| `card` | assistant | AI 气泡内嵌 `ClarificationCard``submitted` 后禁用交互并显示已选答案 |
| `outline` | assistant | AI 气泡内嵌大纲卡片+确认/修改按钮,`confirmed` 后禁用按钮 |
| `novel` | assistant | AI 气泡内嵌小说正文,`pending` 时流式逐字追加,`pending=false` 后完成 |
新消息追加到底部,`keepResultAtBottom()` 自动滚动跟随。
### SSE 事件 → 对话消息映射
`handleShortNovelEvent` 改为往统一对话流追加/更新消息:
| SSE 事件 | 对话流行为 |
|---|---|
| `status` | 更新底部 loading 提示文案(不追加独立消息) |
| `clarification_card` | `addResultMessage({role:'assistant', kind:'card', card: payload.card, submitted:false})` |
| `outline_created` | `addResultMessage({role:'assistant', kind:'outline', outline: payload.outline, confirmed:false})` |
| `novel_start` | `addResultMessage({role:'assistant', kind:'novel', content:'', pending:true})` |
| `novel_delta` | 往最后一条 `kind:'novel'` 消息的 `content` 追加 `payload.delta` |
| `novel_done` | 标记最后一条 novel 消息 `pending=false`,保存 `scriptId`/`conversationId`/`currentVersionMessageId` |
| `error` | 若 `DAILY_GENERATION_IN_PROGRESS` 且有 `session_id`:保存 `resumeableSessionId`,更新底部 loading 为失败态+继续创作入口;其他错误:追加 `kind:'text', role:'assistant', failed:true` 消息 |
### 用户交互 → 接口调用
每张卡片/大纲消息内部渲染交互按钮,用户操作时**先追加一条 user 消息到对话流,再调接口**:
| 阶段 | 交互 | 追加 user 消息 | 接口调用 |
|---|---|---|---|
| 生成 | ClarificationCard 提交 | `{kind:'text', content: 答案}` | `followupStream(action:'answer_clarification', payload:{answer})` |
| 生成 | 大纲"确认" | `{kind:'text', content:'确认大纲'}` | `followupStream(action:'confirm_outline')` |
| 生成 | 大纲"修改" | `{kind:'text', content: 修改意见}` | `followupStream(action:'modify_outline', payload:{feedback})` |
| 生成 | 失败"继续创作" | `{kind:'text', content:'继续之前的创作'}` | `followupStream(action:'retry')` |
| 后续 | novel_done 后底部输入 | `{kind:'text', content: 修改建议}` | `streamScriptChat`(旧接口,支持改写/继续/版本) |
### 接口切换边界
- **生成流程**(澄清→大纲→小说):用新接口 `startNovelStream` / `followupStream`
- **novel_done 后**:保存记录后,底部输入栏 `sendChat` 切换用现有旧接口 `streamScriptChat` + `createMessage`(已成熟,支持改写/继续/版本切换)
切换时机:`novel_done` 事件处理时,设置 `generationPhase = 'done'`,底部输入栏根据该标志切换接口。两套接口的消息都进同一个 `resultMessages` 对话流,用户无感知。
## 交互态管理
### 卡片消息(kind:'card'
- 渲染 `ClarificationCard`,传入 `:card="message.card"`
- `submitted=false` 时:正常交互,`@submit` 触发 `submitClarification`
- `submitClarification` 内:设置 `message.submitted=true`(禁用卡片),追加 user 消息,调 followupStream
- `submitted=true` 时:卡片显示"已回答:xxx",禁用交互
### 大纲消息(kind:'outline'
- 渲染大纲内容(title/logline/beats/ending+ 确认/修改按钮
- `confirmed=false` 时:显示输入框+确认+修改按钮
- 用户确认/修改后:设置 `message.confirmed=true`(隐藏按钮),追加 user 消息,调 followupStream
- `confirmed=true` 时:只展示大纲内容,不再有按钮
### 小说消息(kind:'novel'
- `pending=true` 时:流式追加 `novel_delta` 文本,显示打字光标
- `pending=false`(novel_done)时:完整小说正文,无光标,可复制/朗读
## chat 视图模板结构
```vue
<view v-else-if="viewState === 'chat'" class="chat-page">
<!-- 顶部历史 + 关闭 -->
<view class="chat-top-actions">...</view>
<!-- 对话流 -->
<scroll-view class="chat-scroll" scroll-y ...>
<view class="chat-scroll-content">
<view v-for="msg in resultMessages" :key="msg.id">
<!-- user text -->
<view v-if="msg.role==='user' && msg.kind==='text'" class="chat-bubble user">
<text>{{ msg.content }}</text>
<text class="bubble-time">{{ msg.time }}</text>
</view>
<!-- assistant card -->
<view v-else-if="msg.kind==='card'" class="chat-bubble system">
<ClarificationCard
v-if="!msg.submitted"
:card="msg.card"
@submit="answerClarification(msg, $event)"
/>
<view v-else class="card-answered">
<text>已回答{{ msg.answer }}</text>
</view>
</view>
<!-- assistant outline -->
<view v-else-if="msg.kind==='outline'" class="chat-bubble system">
<!-- 大纲内联渲染title/logline/beats/ending复用当前 outlining 视图样式 -->
<view v-if="msg.outline?.title" class="outline-title">{{ msg.outline.title }}</view>
<view v-if="msg.outline?.logline" class="outline-logline">{{ msg.outline.logline }}</view>
<view v-if="Array.isArray(msg.outline?.beats)" class="outline-beats">
<view v-for="(beat, idx) in msg.outline.beats" :key="idx" class="beat-item">
<text class="beat-order">{{ beat.order || idx + 1 }}</text>
<view class="beat-content">
<text class="beat-title">{{ beat.title }}</text>
<text v-if="beat.summary" class="beat-summary">{{ beat.summary }}</text>
</view>
</view>
</view>
<view v-if="msg.outline?.ending" class="outline-ending">
<text class="ending-label">结局</text>
<text class="ending-text">{{ msg.outline.ending }}</text>
</view>
<view v-if="!msg.confirmed" class="outline-actions">
<input v-model="msg.feedback" placeholder="如需修改请输入意见" />
<button @click="modifyOutline(msg)">修改大纲</button>
<button @click="confirmOutline(msg)">确认大纲</button>
</view>
</view>
<!-- assistant novel -->
<view v-else-if="msg.kind==='novel'" class="chat-bubble system">
<text>{{ msg.content }}<text v-if="msg.pending" class="typing-cursor">|</text></text>
</view>
<!-- assistant text (status/error) -->
<view v-else class="chat-bubble system">
<text :class="{ failed: msg.failed }">{{ msg.content }}</text>
</view>
</view>
<!-- 底部 loading -->
<view v-if="generating" class="chat-loading">
<view class="loading-orbit streaming">...</view>
<text>正在为你重写人生</text>
</view>
<!-- 失败继续创作入口 -->
<view v-if="generationStatus==='failed' && resumeableSessionId" class="resume-action">
<button @click="resumeSession">继续创作</button>
</view>
<view :id="resultScrollAnchor" class="chat-scroll-anchor"></view>
</view>
</scroll-view>
<!-- 底部输入栏novel_done 后启用 -->
<view class="chat-input-bar">
<textarea v-model="chatInput" @confirm="sendChat" />
<view @click="sendChat">发送</view>
</view>
</view>
```
## 文件改动清单
| 文件 | 操作 | 说明 |
|---|---|---|
| `mini-program/src/pages/main/ScriptView.vue` | 修改 | 删除 clarifying/outlining/novel-generating 独立视图;新增统一 chat 视图;扩展 addResultMessage 支持 kind/card/outline/submitted/confirmed;改造 handleShortNovelEvent 往对话流追加消息;submitClarification/confirmOutline/modifyOutline 改为操作对话消息;sendChat 按 generationPhase 切换接口 |
| `mini-program/src/components/ClarificationCard.vue` | 不变 | 复用,由父组件控制 submitted 态 |
## 不在本次范围内
- 首页 home 的灵感推荐/语音输入 — 保留不变
- MessageCard 的版本切换/改写/继续功能 — 保留不变(novel_done 后用旧接口时复用)
- 后端接口 — 不变(新接口 followupStream + 旧接口 streamScriptChat 都已就绪)
- 历史剧本库 — 保留不变
## 验收标准
- 用户提交心愿后进入对话页,用户消息作为气泡显示
- 澄清卡片作为 AI 消息出现在对话流,用户回答后卡片变为"已回答"态
- 大纲作为 AI 消息出现,确认/修改后按钮消失
- 小说流式生成作为一条 AI 消息逐字出现
- novel_done 后底部输入栏启用,继续修改用旧接口正常工作
- 失败时(DAILY_GENERATION_IN_PROGRESS)对话流内显示继续创作入口
- 整个过程不切换视图,用户始终在同一个对话页面