---
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）对话流内显示继续创作入口
- 整个过程不切换视图，用户始终在同一个对话页面
