docs: 补充 AI 打字机输出、小程序灵感卡片、脚本主页布局等设计文档和计划
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
|||||||
|
# AI Typewriter Output Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Make all existing AI stream outputs in mini-program, life-script, and web show a themed loading state first, then reveal generated text smoothly one character at a time.
|
||||||
|
|
||||||
|
**Architecture:** Keep backend stream protocol unchanged. Add a small typewriter buffer utility per frontend stack and wire pages/stores to display the typewriter-visible text while retaining the full stream output for saving and final state.
|
||||||
|
|
||||||
|
**Tech Stack:** Vue 3/uni-app, React, Pinia, WebSocket/SSE streaming.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Mini Program Typewriter
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `mini-program/src/composables/useTypewriterStream.js`
|
||||||
|
- Modify: `mini-program/src/pages/main/ScriptView.vue`
|
||||||
|
- Modify: `mini-program/src/pages/main/PathView.vue`
|
||||||
|
- Modify: `mini-program/src/pages/life-event/form.vue`
|
||||||
|
|
||||||
|
- [x] Add a composable with `visibleText`, `isWaiting`, `isStreaming`, `push`, `finish`, `fail`, `reset`, `dispose`.
|
||||||
|
- [x] In `ScriptView.vue`, push each full stream output to the composable and render `visibleText`.
|
||||||
|
- [x] Improve the generating panel with themed loading copy and a typing cursor.
|
||||||
|
- [x] In `PathView.vue` and `life-event/form.vue`, use typewriter visible text for AI output.
|
||||||
|
|
||||||
|
### Task 2: Life Script Typewriter
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `life-script/src/hooks/useTypewriterStream.js`
|
||||||
|
- Modify: `life-script/src/views/ScriptView.jsx`
|
||||||
|
- Modify: `life-script/src/views/PathView.jsx`
|
||||||
|
- Modify: `life-script/src/views/TimelineView.jsx`
|
||||||
|
|
||||||
|
- [x] Add a React hook with the same buffer behavior.
|
||||||
|
- [x] Replace direct `setStreamContent(output)` / `setStreamFeedback(output)` calls with `push(output)`.
|
||||||
|
- [x] Render hook `visibleText` during AI generation.
|
||||||
|
- [x] Add a subtle glass loading state before the first visible character.
|
||||||
|
|
||||||
|
### Task 3: Web Chat Typewriter
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `web/src/stores/chat.ts`
|
||||||
|
|
||||||
|
- [x] Add per-message pending buffers and timer maps inside the chat store.
|
||||||
|
- [x] On `AI_STREAM_DELTA`, append delta to the buffer and start a timer for the target message.
|
||||||
|
- [x] On `AI_STREAM_DONE`, mark the message as done only after the pending buffer is empty.
|
||||||
|
- [x] On error or disconnect, stop timers for affected messages.
|
||||||
|
|
||||||
|
### Task 4: Verification
|
||||||
|
|
||||||
|
**Commands:**
|
||||||
|
- `cd mini-program && npm run build:mp-weixin`
|
||||||
|
- `cd life-script && npm run build`
|
||||||
|
- `cd web && npm run build`
|
||||||
|
- `git diff --check`
|
||||||
|
|
||||||
|
- [x] Mini-program build passes.
|
||||||
|
- [x] life-script build passes.
|
||||||
|
- [x] web build passes.
|
||||||
|
- [x] Diff check reports no new whitespace errors.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Mini Program Inspiration Compact Cards Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Make inspiration cards on the mini-program script generation page compact, single-line, and ellipsis-truncated while preserving full prompt selection behavior.
|
||||||
|
|
||||||
|
**Architecture:** Reuse the existing `recommendations` data and `useRecommendation(item.text)` click path. Change only the presentation in `ScriptView.vue`: remove visible tag text and tighten the card CSS.
|
||||||
|
|
||||||
|
**Tech Stack:** Vue 3 single-file component, uni-app mini-program build.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Compact Inspiration Card Template
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mini-program/src/pages/main/ScriptView.vue`
|
||||||
|
|
||||||
|
- [x] Remove the visible `<text class="recommend-tag">...</text>` from each `recommend-card`.
|
||||||
|
- [x] Keep `@click="useRecommendation(item.text)"` unchanged so the full prompt still fills `wishText`.
|
||||||
|
- [x] Keep the `v-for`, `:key`, `recommend-card`, and `recommend-text` bindings unchanged.
|
||||||
|
|
||||||
|
### Task 2: Single-Line Ellipsis Styling
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mini-program/src/pages/main/ScriptView.vue`
|
||||||
|
|
||||||
|
- [x] Change `.recommend-card` from a tall vertical card to a compact row with `min-height: 68rpx`.
|
||||||
|
- [x] Use centered row alignment and smaller padding.
|
||||||
|
- [x] Add `min-width: 0` so text truncation works inside the grid.
|
||||||
|
- [x] Change `.recommend-text` to one-line ellipsis with `white-space: nowrap`, `overflow: hidden`, and `text-overflow: ellipsis`.
|
||||||
|
- [x] Remove or neutralize unused `.recommend-tag` styling.
|
||||||
|
|
||||||
|
### Task 3: Validate
|
||||||
|
|
||||||
|
**Commands:**
|
||||||
|
- `cd mini-program && npm run build:mp-weixin`
|
||||||
|
- `git diff --check`
|
||||||
|
|
||||||
|
- [x] Mini-program build passes.
|
||||||
|
- [x] Diff check reports no new whitespace errors.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Mini Program Script Generation Feedback Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Add animated progress, long-wait guidance, and graceful failure recovery to the mini-program `心愿实现中` screen.
|
||||||
|
|
||||||
|
**Architecture:** Keep all AI stream calls unchanged and add a small local feedback state in `ScriptView.vue`. The screen uses timer thresholds to distinguish normal waiting, slow waiting, very slow waiting, streaming output, and failure.
|
||||||
|
|
||||||
|
**Tech Stack:** Vue 3 single-file component, uni-app mini-program build, existing `useTypewriterStream` composable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add Generation Feedback State
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mini-program/src/pages/main/ScriptView.vue`
|
||||||
|
|
||||||
|
- [x] Add `generationStatus`, `generationError`, `generationHintIndex`, and `lastSubmitSource` refs.
|
||||||
|
- [x] Add local timers for rotating hints, 8-second slow state, and 20-second very-slow state.
|
||||||
|
- [x] Add `startGenerationFeedback`, `clearGenerationFeedbackTimers`, `markGenerationStreaming`, and `markGenerationFailed` helpers.
|
||||||
|
- [x] Clear timers on success, failure, returning home, and component unmount.
|
||||||
|
|
||||||
|
### Task 2: Update Generation Flow
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mini-program/src/pages/main/ScriptView.vue`
|
||||||
|
|
||||||
|
- [x] Call `startGenerationFeedback()` when `submitWish` enters `viewState = 'generating'`.
|
||||||
|
- [x] Call `markGenerationStreaming()` in the stream `onDelta` callback before pushing typewriter text.
|
||||||
|
- [x] On failure, keep `viewState = 'generating'` and show a failure state instead of immediately returning home with only a toast.
|
||||||
|
- [x] Add `retryGeneration()` and `returnToEdit()` actions for the failure state.
|
||||||
|
|
||||||
|
### Task 3: Update Generation Screen UI
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mini-program/src/pages/main/ScriptView.vue`
|
||||||
|
|
||||||
|
- [x] Replace static `心愿实现中……` copy with computed `generationTitle`.
|
||||||
|
- [x] Add thinking dots when no stream output has arrived.
|
||||||
|
- [x] Add a secondary hint line under the main loading copy.
|
||||||
|
- [x] Add failure actions `再试一次` and `返回修改` when `generationStatus === 'failed'`.
|
||||||
|
|
||||||
|
### Task 4: Add Themed Animations
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mini-program/src/pages/main/ScriptView.vue`
|
||||||
|
|
||||||
|
- [x] Animate `.loading-orbit` with a gentle breathing pulse.
|
||||||
|
- [x] Animate `.loading-orbit::after` as the small orbiting light.
|
||||||
|
- [x] Add animated thinking dots inside the system bubble.
|
||||||
|
- [x] Style the failure copy and actions in the current purple/gold theme.
|
||||||
|
|
||||||
|
### Task 5: Validate
|
||||||
|
|
||||||
|
**Commands:**
|
||||||
|
- `cd mini-program && npm run build:mp-weixin`
|
||||||
|
- `git diff --check`
|
||||||
|
|
||||||
|
- [x] Mini-program build passes.
|
||||||
|
- [x] Diff check reports no new whitespace errors.
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Mini Program Script Home Cosmic Background Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Center the script generation headline and add subtle cosmic background motion without interfering with the page content.
|
||||||
|
|
||||||
|
**Architecture:** Add a decorative background layer inside `ScriptView.vue` and keep all interactive content above it. Use CSS-only planets, star speckles, and meteors so no assets or runtime logic are needed.
|
||||||
|
|
||||||
|
**Tech Stack:** Vue 3 single-file component, uni-app mini-program build, scoped CSS animations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add Background Layer
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mini-program/src/pages/main/ScriptView.vue`
|
||||||
|
|
||||||
|
- [x] Add a non-interactive `cosmic-background` view as the first child of `.script-view`.
|
||||||
|
- [x] Add child views for two planets, two star fields, and three meteors.
|
||||||
|
- [x] Ensure the decorative layer has no event handlers.
|
||||||
|
|
||||||
|
### Task 2: Fix Layering and Headline Alignment
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mini-program/src/pages/main/ScriptView.vue`
|
||||||
|
|
||||||
|
- [x] Set `.script-view` to `position: relative` and `overflow: hidden`.
|
||||||
|
- [x] Set `.wish-home`, `.generation-view`, and `.result-view` to `position: relative; z-index: 1`.
|
||||||
|
- [x] Center `.hero-copy` with `justify-content: center` and `text-align: center`.
|
||||||
|
- [x] Increase the spacing under `.hero-copy` so the inspiration section breathes.
|
||||||
|
|
||||||
|
### Task 3: Add Cosmic Styling
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mini-program/src/pages/main/ScriptView.vue`
|
||||||
|
|
||||||
|
- [x] Style `.cosmic-background` with absolute positioning, low opacity, and `pointer-events: none`.
|
||||||
|
- [x] Style planet elements with radial gradients and slow drift animations.
|
||||||
|
- [x] Style meteor elements with diagonal motion and staggered animation delays.
|
||||||
|
- [x] Style star fields with low-contrast radial gradients.
|
||||||
|
|
||||||
|
### Task 4: Validate
|
||||||
|
|
||||||
|
**Commands:**
|
||||||
|
- `cd mini-program && npm run build:mp-weixin`
|
||||||
|
- `git diff --check`
|
||||||
|
|
||||||
|
- [x] Mini-program build passes.
|
||||||
|
- [x] Diff check reports no new whitespace errors.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Mini Program Script Home Layout Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Make the mini-program script generation home screen keep its headline on one line and place the microphone lower for easier voice input.
|
||||||
|
|
||||||
|
**Architecture:** Reorder existing template blocks in `ScriptView.vue` without changing data flow or event handlers. Adjust the existing hero CSS so the headline is a single non-wrapping row while preserving the current visual theme.
|
||||||
|
|
||||||
|
**Tech Stack:** Vue 3 single-file component, uni-app mini-program build.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Reorder Home Screen Blocks
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mini-program/src/pages/main/ScriptView.vue`
|
||||||
|
|
||||||
|
- [x] Move the `inspiration-section` block so it appears immediately after `hero-copy`.
|
||||||
|
- [x] Keep `wish-input-wrap` after `inspiration-section`.
|
||||||
|
- [x] Keep `profile-boost` after `wish-input-wrap`.
|
||||||
|
- [x] Move `orb-wrap` and `voice-copy` after `profile-boost`.
|
||||||
|
- [x] Verify no event bindings change: `useRecommendation`, `shuffleInspirations`, `submitWish`, `startVoicePress`, `endVoicePress`, `cancelVoicePress`, and `openSocialInsights` remain attached to the same elements.
|
||||||
|
|
||||||
|
### Task 2: Make Headline Single Line
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mini-program/src/pages/main/ScriptView.vue`
|
||||||
|
|
||||||
|
- [x] Update the two headline text nodes into a single row under `.hero-copy`.
|
||||||
|
- [x] Change `.hero-copy` to a horizontal flex container with `white-space: nowrap`.
|
||||||
|
- [x] Reduce `.hero-title` font size enough for common mini-program viewport widths.
|
||||||
|
- [x] Preserve `.hero-highlight` styling.
|
||||||
|
|
||||||
|
### Task 3: Validate
|
||||||
|
|
||||||
|
**Commands:**
|
||||||
|
- `cd mini-program && npm run build:mp-weixin`
|
||||||
|
- `git diff --check`
|
||||||
|
|
||||||
|
- [x] Mini-program build passes.
|
||||||
|
- [x] Diff check reports no new whitespace errors.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# AI 输出逐字显示与等待态优化设计
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
AI 接口耗时较长时,用户不能在空白页面等待;收到流式输出后,文本不能一大段一大段跳出,而要在当前页面风格下逐字平滑显示。优化范围包括 `mini-program`、`life-script` 和 `web` 中所有现有 AI 流式输出入口。
|
||||||
|
|
||||||
|
## 范围
|
||||||
|
|
||||||
|
- 小程序:剧本生成、实现路径生成、人生事件 AI 帮写。
|
||||||
|
- life-script:剧本生成、实现路径生成、人生轨迹 AI 反馈。
|
||||||
|
- web:聊天 WebSocket AI 流式消息。
|
||||||
|
- 不改变后端接口协议,不降低后端流式传输速度。
|
||||||
|
|
||||||
|
## 方案
|
||||||
|
|
||||||
|
各端新增轻量 typewriter 控制器。后端仍按当前速度返回 SSE/WebSocket delta,前端把 delta 放入缓冲区,再用固定节奏输出到可见文本。这样既不拖慢真实请求,也能避免一次性展示大段文字。
|
||||||
|
|
||||||
|
### 输出状态
|
||||||
|
|
||||||
|
- `waiting`:请求已发出但尚未收到首个 delta,显示动态 loading 和中文提示。
|
||||||
|
- `streaming`:已收到输出,逐字显示文本,页面保持可滚动。
|
||||||
|
- `draining`:后端已结束但缓冲区还有文字,继续输出剩余文字。
|
||||||
|
- `done`:缓冲区清空,进入完成态。
|
||||||
|
- `error`:停止逐字输出,保留已显示内容,展示中文错误。
|
||||||
|
|
||||||
|
### 小程序
|
||||||
|
|
||||||
|
小程序新增 `useTypewriterStream` composable,返回可见文本、等待状态、输出状态和控制方法。剧本生成页保留当前紫色宇宙风格,生成态增加星轨 loading、阶段文案和逐字正文。路径生成与 AI 帮写复用同一个 composable。
|
||||||
|
|
||||||
|
### life-script
|
||||||
|
|
||||||
|
life-script 新增 React hook `useTypewriterStream`,把 `onDelta` 的完整输出转为逐字可见输出。剧本、路径、生命事件反馈页面接入该 hook,并保留玻璃拟态视觉风格。
|
||||||
|
|
||||||
|
### web
|
||||||
|
|
||||||
|
web 聊天 store 在 `AI_STREAM_DELTA` 到达时不再直接追加到 `message.content`,而是写入该消息的 pending buffer。定时器每次取若干字符刷到可见消息,`AI_STREAM_DONE` 只标记后端已完成,等缓冲区清空后再置为 `sent`。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- `mini-program npm run build:mp-weixin`
|
||||||
|
- `life-script npm run build`
|
||||||
|
- `web npm run build`
|
||||||
|
- `git diff --check`
|
||||||
|
|
||||||
|
重点人工验证:
|
||||||
|
|
||||||
|
- AI 首 token 前有动态等待态。
|
||||||
|
- 后端一次返回大段文本时,页面仍逐字显示。
|
||||||
|
- 后端完成后不会截断缓冲区剩余文本。
|
||||||
|
- 出错时保留已显示文本并展示错误。
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Mini Program Inspiration Compact Cards Design
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Optimize the mini-program script generation page so inspiration cards use less vertical space while still filling the full selected prompt into the send input.
|
||||||
|
|
||||||
|
## Approved Approach
|
||||||
|
|
||||||
|
Use option A: keep the existing two-column inspiration grid, but make each card a compact single-line candidate.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
- Each inspiration card displays only `item.text`.
|
||||||
|
- Displayed text is constrained to one line.
|
||||||
|
- Overflowing text is hidden with an ellipsis.
|
||||||
|
- The category/tag chip is not shown in the compact card.
|
||||||
|
- Clicking a card still calls `useRecommendation(item.text)` and writes the full text into the send input.
|
||||||
|
- The existing `换一换` action and recommendation source logic remain unchanged.
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
Only `mini-program/src/pages/main/ScriptView.vue` needs to change.
|
||||||
|
|
||||||
|
Update the card template to remove the visible tag node. Update CSS so:
|
||||||
|
|
||||||
|
- `.recommend-card` is a compact row, about `68rpx` high.
|
||||||
|
- `.recommend-text` uses `white-space: nowrap`, `overflow: hidden`, and `text-overflow: ellipsis`.
|
||||||
|
- Grid remains two columns.
|
||||||
|
- Visual theme remains consistent with the current purple glass style.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mini-program
|
||||||
|
npm run build:mp-weixin
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected result: build succeeds with no new errors.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Mini Program Script Generation Feedback Design
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Improve the mini-program `心愿实现中` screen so users can clearly tell that generation is running, understand long waits, and recover gently if the AI stream fails or produces no output.
|
||||||
|
|
||||||
|
## Approved Approach
|
||||||
|
|
||||||
|
Use option A: add a scene-friendly loading state with phased guidance and a graceful failure panel.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
When script generation starts:
|
||||||
|
|
||||||
|
1. The page stays on the current `心愿实现中` screen.
|
||||||
|
2. The existing chat bubble and orbit motif remain.
|
||||||
|
3. The orbit gets breathing/rotating animation and small star-dot motion.
|
||||||
|
4. A rotating hint line appears while no stream text has arrived.
|
||||||
|
5. If no stream text arrives after about 8 seconds, show a gentle slow-wait message.
|
||||||
|
6. If no stream text arrives after about 20 seconds, show a warmer network-slow message, while still waiting for the stream.
|
||||||
|
7. Once stream text arrives, switch to the existing typewriter text output.
|
||||||
|
8. If the call fails or returns empty output, keep the user on the generation screen and show a soft failure state with `再试一次` and `返回修改`.
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
Only `mini-program/src/pages/main/ScriptView.vue` needs to change.
|
||||||
|
|
||||||
|
Add a small local generation feedback state:
|
||||||
|
|
||||||
|
- `generationStatus`: `idle | waiting | slow | verySlow | streaming | failed`
|
||||||
|
- `generationError`: user-facing failure copy
|
||||||
|
- timers for rotating hints and long-wait thresholds
|
||||||
|
|
||||||
|
Do not change backend API calls or stream protocol. `streamAiScene` remains the source of stream output. Existing `useTypewriterStream` continues to render text progressively after the first output arrives.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mini-program
|
||||||
|
npm run build:mp-weixin
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected result: build succeeds with no new errors.
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
# Mini Program Script Home Cosmic Background Design
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Polish the mini-program script generation home page so the headline is centered with comfortable spacing, and the page background gains subtle cosmic motion that matches the current purple/gold style.
|
||||||
|
|
||||||
|
## Approved Approach
|
||||||
|
|
||||||
|
Use option A: a restrained cosmic background.
|
||||||
|
|
||||||
|
## Behavior
|
||||||
|
|
||||||
|
- Center the headline `今天有什么心愿想实现`.
|
||||||
|
- Keep the headline on one line.
|
||||||
|
- Add a slightly larger gap between the headline and the inspiration section.
|
||||||
|
- Add background-only cosmic elements:
|
||||||
|
- a distant planet near the upper-right edge,
|
||||||
|
- a faint smaller planet near the lower-left edge,
|
||||||
|
- a few animated meteors moving diagonally,
|
||||||
|
- subtle star speckles.
|
||||||
|
- Background elements must stay behind page content and must not affect tapping inputs, inspiration cards, buttons, or the microphone.
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
Only `mini-program/src/pages/main/ScriptView.vue` needs to change.
|
||||||
|
|
||||||
|
Add a `cosmic-background` layer as the first child of `.script-view`. Set it to `position: absolute`, `inset: 0`, `pointer-events: none`, and a lower `z-index`.
|
||||||
|
|
||||||
|
Set `.script-view` to `position: relative` and `overflow: hidden`. Set `.wish-home`, `.generation-view`, and `.result-view` to `position: relative; z-index: 1;`.
|
||||||
|
|
||||||
|
Use CSS-only animation for meteors and slow planet drift. Keep opacity low so content remains readable.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mini-program
|
||||||
|
npm run build:mp-weixin
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected result: build succeeds with no new errors.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Mini Program Script Home Layout Design
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Optimize the mini-program script generation home screen so the headline stays on one line and voice input is easier to reach.
|
||||||
|
|
||||||
|
## Approved Layout
|
||||||
|
|
||||||
|
Use the approved A layout:
|
||||||
|
|
||||||
|
1. Keep the headline text `今天有什么 心愿 想实现` on a single line.
|
||||||
|
2. Move the full `灵感一下` section above the input and voice area.
|
||||||
|
3. Move the wish text input, social insight binding card, and press-to-talk microphone below the inspiration section.
|
||||||
|
4. Keep the existing visual theme, colors, copy, analytics behavior, voice events, and generation flow unchanged.
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
Only `mini-program/src/pages/main/ScriptView.vue` needs to change.
|
||||||
|
|
||||||
|
The template should reorder existing blocks instead of recreating them:
|
||||||
|
|
||||||
|
- Header and headline remain at the top.
|
||||||
|
- Inspiration section follows the headline.
|
||||||
|
- Wish input follows inspiration.
|
||||||
|
- Social insight card follows the input.
|
||||||
|
- Microphone orb and voice copy sit at the bottom of the home content.
|
||||||
|
|
||||||
|
CSS should make the headline a single flex row with no wrapping. The text must remain readable on normal mini-program widths, so the headline font size should be reduced from the current two-line hero scale to a single-line scale with `white-space: nowrap`.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mini-program
|
||||||
|
npm run build:mp-weixin
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected result: build succeeds with no new errors.
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
export const useTypewriterStream = ({ interval = 18, step = 1 } = {}) => {
|
||||||
|
const [visibleText, setVisibleText] = useState('');
|
||||||
|
const [targetText, setTargetText] = useState('');
|
||||||
|
const [received, setReceived] = useState(false);
|
||||||
|
const [backendDone, setBackendDone] = useState(false);
|
||||||
|
const [failed, setFailed] = useState(false);
|
||||||
|
const visibleRef = useRef('');
|
||||||
|
const targetRef = useRef('');
|
||||||
|
const doneRef = useRef(false);
|
||||||
|
const failedRef = useRef(false);
|
||||||
|
const timerRef = useRef(null);
|
||||||
|
const waitersRef = useRef([]);
|
||||||
|
|
||||||
|
const stopTimer = useCallback(() => {
|
||||||
|
if (timerRef.current) {
|
||||||
|
window.clearInterval(timerRef.current);
|
||||||
|
timerRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resolveWaiters = useCallback(() => {
|
||||||
|
if (!waitersRef.current.length) return;
|
||||||
|
const waiters = waitersRef.current;
|
||||||
|
waitersRef.current = [];
|
||||||
|
waiters.forEach(resolve => resolve(visibleRef.current));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isFullyRendered = useCallback(() => {
|
||||||
|
return doneRef.current && visibleRef.current.length >= targetRef.current.length;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const tick = useCallback(() => {
|
||||||
|
if (visibleRef.current.length < targetRef.current.length) {
|
||||||
|
const nextLength = Math.min(targetRef.current.length, visibleRef.current.length + step);
|
||||||
|
const nextText = targetRef.current.slice(0, nextLength);
|
||||||
|
visibleRef.current = nextText;
|
||||||
|
setVisibleText(nextText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (doneRef.current || failedRef.current) {
|
||||||
|
stopTimer();
|
||||||
|
if (isFullyRendered() || failedRef.current) resolveWaiters();
|
||||||
|
}
|
||||||
|
}, [isFullyRendered, resolveWaiters, step, stopTimer]);
|
||||||
|
|
||||||
|
const ensureTimer = useCallback(() => {
|
||||||
|
if (!timerRef.current) timerRef.current = window.setInterval(tick, interval);
|
||||||
|
}, [interval, tick]);
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
stopTimer();
|
||||||
|
visibleRef.current = '';
|
||||||
|
targetRef.current = '';
|
||||||
|
doneRef.current = false;
|
||||||
|
failedRef.current = false;
|
||||||
|
setVisibleText('');
|
||||||
|
setTargetText('');
|
||||||
|
setReceived(false);
|
||||||
|
setBackendDone(false);
|
||||||
|
setFailed(false);
|
||||||
|
resolveWaiters();
|
||||||
|
}, [resolveWaiters, stopTimer]);
|
||||||
|
|
||||||
|
const push = useCallback((nextText = '') => {
|
||||||
|
const next = String(nextText || '');
|
||||||
|
if (next.length < visibleRef.current.length) reset();
|
||||||
|
targetRef.current = next;
|
||||||
|
setTargetText(next);
|
||||||
|
setReceived(true);
|
||||||
|
ensureTimer();
|
||||||
|
}, [ensureTimer, reset]);
|
||||||
|
|
||||||
|
const finish = useCallback((finalText) => {
|
||||||
|
if (typeof finalText === 'string') {
|
||||||
|
targetRef.current = finalText;
|
||||||
|
setTargetText(finalText);
|
||||||
|
}
|
||||||
|
doneRef.current = true;
|
||||||
|
setBackendDone(true);
|
||||||
|
if (targetRef.current.length > visibleRef.current.length) ensureTimer();
|
||||||
|
else {
|
||||||
|
stopTimer();
|
||||||
|
resolveWaiters();
|
||||||
|
}
|
||||||
|
}, [ensureTimer, resolveWaiters, stopTimer]);
|
||||||
|
|
||||||
|
const fail = useCallback((message) => {
|
||||||
|
failedRef.current = true;
|
||||||
|
doneRef.current = true;
|
||||||
|
setFailed(true);
|
||||||
|
setBackendDone(true);
|
||||||
|
if (!visibleRef.current && message) {
|
||||||
|
visibleRef.current = message;
|
||||||
|
targetRef.current = message;
|
||||||
|
setVisibleText(message);
|
||||||
|
setTargetText(message);
|
||||||
|
}
|
||||||
|
stopTimer();
|
||||||
|
resolveWaiters();
|
||||||
|
}, [resolveWaiters, stopTimer]);
|
||||||
|
|
||||||
|
const waitForDone = useCallback(() => {
|
||||||
|
if (isFullyRendered() || failedRef.current) {
|
||||||
|
return Promise.resolve(visibleRef.current);
|
||||||
|
}
|
||||||
|
return new Promise(resolve => {
|
||||||
|
waitersRef.current.push(resolve);
|
||||||
|
ensureTimer();
|
||||||
|
});
|
||||||
|
}, [ensureTimer, isFullyRendered]);
|
||||||
|
|
||||||
|
useEffect(() => stopTimer, [stopTimer]);
|
||||||
|
|
||||||
|
return useMemo(() => ({
|
||||||
|
visibleText,
|
||||||
|
targetText,
|
||||||
|
isWaiting: !received && !backendDone && !failed,
|
||||||
|
isStreaming: received && (visibleText.length < targetText.length || !backendDone),
|
||||||
|
isDraining: backendDone && visibleText.length < targetText.length,
|
||||||
|
isDone: backendDone && visibleText.length >= targetText.length && !failed,
|
||||||
|
reset,
|
||||||
|
push,
|
||||||
|
finish,
|
||||||
|
waitForDone,
|
||||||
|
fail
|
||||||
|
}), [backendDone, failed, finish, push, received, reset, targetText, visibleText, waitForDone, fail]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useTypewriterStream;
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
export const useTypewriterStream = ({ interval = 24, step = 1 } = {}) => {
|
||||||
|
const visibleText = ref('')
|
||||||
|
const targetText = ref('')
|
||||||
|
const received = ref(false)
|
||||||
|
const backendDone = ref(false)
|
||||||
|
const failed = ref(false)
|
||||||
|
let timer = null
|
||||||
|
let waiters = []
|
||||||
|
|
||||||
|
const isWaiting = computed(() => !received.value && !backendDone.value && !failed.value)
|
||||||
|
const isStreaming = computed(() => received.value && (visibleText.value.length < targetText.value.length || !backendDone.value))
|
||||||
|
const isDraining = computed(() => backendDone.value && visibleText.value.length < targetText.value.length)
|
||||||
|
const isDone = computed(() => backendDone.value && visibleText.value.length >= targetText.value.length && !failed.value)
|
||||||
|
|
||||||
|
const stopTimer = () => {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveWaiters = () => {
|
||||||
|
if (!waiters.length) return
|
||||||
|
const currentWaiters = waiters
|
||||||
|
waiters = []
|
||||||
|
currentWaiters.forEach(resolve => resolve(visibleText.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFullyRendered = () => {
|
||||||
|
return backendDone.value && visibleText.value.length >= targetText.value.length
|
||||||
|
}
|
||||||
|
|
||||||
|
const tick = () => {
|
||||||
|
if (visibleText.value.length < targetText.value.length) {
|
||||||
|
const nextLength = Math.min(targetText.value.length, visibleText.value.length + step)
|
||||||
|
visibleText.value = targetText.value.slice(0, nextLength)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (backendDone.value || failed.value) {
|
||||||
|
stopTimer()
|
||||||
|
if (isFullyRendered() || failed.value) {
|
||||||
|
resolveWaiters()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensureTimer = () => {
|
||||||
|
if (!timer) {
|
||||||
|
timer = setInterval(tick, interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
stopTimer()
|
||||||
|
visibleText.value = ''
|
||||||
|
targetText.value = ''
|
||||||
|
received.value = false
|
||||||
|
backendDone.value = false
|
||||||
|
failed.value = false
|
||||||
|
resolveWaiters()
|
||||||
|
}
|
||||||
|
|
||||||
|
const push = (nextText = '') => {
|
||||||
|
const next = String(nextText || '')
|
||||||
|
if (next.length < visibleText.value.length) {
|
||||||
|
reset()
|
||||||
|
}
|
||||||
|
received.value = true
|
||||||
|
targetText.value = next
|
||||||
|
ensureTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
const finish = (finalText) => {
|
||||||
|
if (typeof finalText === 'string') {
|
||||||
|
targetText.value = finalText
|
||||||
|
}
|
||||||
|
backendDone.value = true
|
||||||
|
if (targetText.value.length > visibleText.value.length) {
|
||||||
|
ensureTimer()
|
||||||
|
} else {
|
||||||
|
stopTimer()
|
||||||
|
resolveWaiters()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fail = (message) => {
|
||||||
|
failed.value = true
|
||||||
|
backendDone.value = true
|
||||||
|
if (!visibleText.value && message) {
|
||||||
|
visibleText.value = message
|
||||||
|
targetText.value = message
|
||||||
|
}
|
||||||
|
stopTimer()
|
||||||
|
resolveWaiters()
|
||||||
|
}
|
||||||
|
|
||||||
|
const waitForDone = () => {
|
||||||
|
if (isFullyRendered() || failed.value) {
|
||||||
|
return Promise.resolve(visibleText.value)
|
||||||
|
}
|
||||||
|
return new Promise(resolve => {
|
||||||
|
waiters.push(resolve)
|
||||||
|
ensureTimer()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
visibleText,
|
||||||
|
targetText,
|
||||||
|
isWaiting,
|
||||||
|
isStreaming,
|
||||||
|
isDraining,
|
||||||
|
isDone,
|
||||||
|
push,
|
||||||
|
finish,
|
||||||
|
waitForDone,
|
||||||
|
fail,
|
||||||
|
reset,
|
||||||
|
dispose: stopTimer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useTypewriterStream
|
||||||
Reference in New Issue
Block a user