# 「随心」主题（Suixin Theme）技术计划（plan）

## 0. 目标回顾

在 Home 现有「风景 / 纯色」主题基础上新增第三种主题「随心」：

- **输入**：问卷生成的用户画像 `U`（本地存储）
- **输出**：Home 背景推荐颜色（以纯色为主）
- **规则**：复用「个性化背景颜色推荐算法」的 Base Theme/Neutral Theme 与 Hard Rules
- **计算时机**：
  - **冷启动（App 进程级）**：计算一次并锁定 Base Theme
  - **切换文案（Home 上滑切下一条）**：在同一 Base Theme 内更新一次“当前颜色”
- **持久化**：主题选择与「随心」计算状态均持久化（避免回到 Home/重进页面时丢失）
- **多语言**：TC（zh-TW）+ EN

## 1. 现状梳理（与改动点）

### 1.1 现有主题切换

- `ThemeMode` 当前为 `'scenery' | 'color'`
- `ThemeModal` 弹窗提供 2 个卡片切换
- `Home` 根据 `themeMode`：
  - `scenery`：背景图 + 默认底色
  - `color`：从 `THEME_COLORS` 按 `index` 轮换纯色
- `ui.theme.mode` 已在 `AsyncStorage` 持久化

### 1.2 用户画像输入已就绪

客户端已将问卷映射为 `UserProfileV1_2(_Extended)` 并持久化（`user.profileScoring`），关键字段：

- `stage.unknown`（Hard Rule：unknown → Neutral）
- `need`（稀疏 one-hot：`{ [needTag]: 1 }` 或 `{}`）
- `emotion_score: number | null`
- `profile_confidence: number`
- `profile_answered`

## 2. 技术方案总览

### 2.1 「随心」算法在 Home 的落地形态

Home 不具备“长文案阅读页的 scroll”，因此采用**“渐变单点采样”**来复用算法的连续插值模型：

- Base Theme 仍按 `need / stage` 选定并锁定（Theme Lock）
- 每次切换文案时，生成一个 \(t \in [0, 1]\)，并计算：

\[
Color(t) = lerp(Color\_top, Color\_bottom, t)
\]

- 输出为单一 `hex` 纯色，作为 Home `backgroundColor`
- 全程 **不跨 need、不跨主题色系**，仅在同主题内移动

### 2.2 持久化与“只在冷启动/切换文案时计算”

为同时满足“持久化”与“冷启动时计算”：

- **持久化内容**：锁定的 `base_theme_id` + 用于生成 \(t\) 的 `seed` + 当前 `step_index` + `last_color`
- **冷启动计算**：当检测到“新一轮 App 启动会话”时，重新选择并锁定 `base_theme_id`，并重置/更新 `seed` 与 `step_index`
- **切换文案计算**：仅递增 `step_index`，在同一 `base_theme_id` 下更新 `last_color`

> 说明：冷启动检测以“进程级首次进入 Home”为准（工程实现阶段会在 `_layout` 或全局单例中生成 boot 标记）。

## 3. 数据结构与存储设计

### 3.1 扩展主题枚举

- 将 `ThemeMode` 扩展为：`'scenery' | 'color' | 'suixin'`
- 存储 key：沿用 `ui.theme.mode`

### 3.2 新增「随心」状态存储

新增本地存储 key（建议）：

- `ui.theme.suixin.state`

数据结构（建议）：

```ts
type SuixinThemeStateV1 = {
  schema_version: 1;
  saved_at: string;            // ISO8601
  base_theme_id: 'neutral' | 'emotional_support' | 'parenting_pressure' | 'self_worth' | 'anxiety_relief' | 'rest_balance';
  seed: string;                // 用于生成 t 的稳定种子（可由 profile + 日期等派生）
  step_index: number;          // 每切换一条文案 +1
  last_color: string;          // "#RRGGBB"
};
```

### 3.3 冷启动会话标记（Boot ID）

为实现“仅冷启动时重置 base theme/seed”，新增一个进程级 boot 标记（实现二选一）：

- **方案 A（推荐）**：在 `app/_layout.tsx` 首次挂载时生成 `boot_id` 并写入内存单例（不落盘）
- **方案 B**：写入 `AsyncStorage`（例如 `app.boot.lastSeenAt`）并结合“本次运行内存标记”判定首次进入 Home

计划优先采用方案 A：逻辑清晰且不污染存储。

## 4. 颜色算法实现细节（Home 版本）

### 4.1 主题色盘常量

在客户端新增一个颜色模块（例如 `client/src/features/suixinTheme/`），内置：

- Base Theme（5 套）+ Neutral（1 套）
- 与设计文档保持一致的 `hex` 值

### 4.2 Base Theme 选择（锁定）

输入：`UserProfileScoring`

输出：`base_theme_id`

规则：

- 若 `stage.unknown === 1` → `neutral`
- 若 `need` 为空 `{}` → `neutral`
- 否则取 `Object.keys(need)[0]`：
  - 若 key 在枚举内 → 对应 Base Theme
  - 否则 → `neutral`

### 4.3 t 的生成与“低感知变化”

为了让“切换文案”带来“流动感”但不跳变，采用**小步进**策略：

- 定义 `N = 12`（可调）：表示从 \(0 \to 1\) 的分段数
- 每次切换文案：`step_index += 1`
- 计算：`t = (step_index % N) / (N - 1)`

> 该策略保证 \(t\) 在 \([0, 1]\) 内缓慢移动；到达 1 后回到 0 会有一次跳变。为进一步降低跳变，可改为往返波形：
>
> - `phase = step_index % (2*(N-1))`
> - `t = phase <= (N-1) ? phase/(N-1) : (2*(N-1)-phase)/(N-1)`

实现阶段默认采用**往返波形**，避免回卷突跳。

### 4.4 lerp 计算（严格线性）

- `lerp` 仅允许线性插值
- 颜色空间：先使用 sRGB 的逐通道线性插值（实现简单、可控）；若后续需要更自然，可升级到线性空间插值，但仍保持线性模型

### 4.5 emotion/confidence 的约束接入

本期按“安全优先”策略落地：

- 若 `emotion_score === null` 或 `emotion_score <= 0.3`：输出不做任何微扰（纯 lerp 结果）
- 亮度微扰（\(\Delta L \le \pm 2\%\)）与饱和度上限为可选增强；若落地，将以 `profile_confidence` 作为开关条件，并确保不改变色系

## 5. UI 与交互实现计划

### 5.1 ThemeModal：新增第三个主题卡片

- 在 `client/components/home/ThemeModal.tsx`：
  - `ThemeMode` 扩展为包含 `'suixin'`
  - 新增 `ThemeCard`：标题使用 i18n（如 `t('theme.suixin')`）
  - 布局改造：由 2 卡横排改为 **3 卡自适应**（`flexWrap` 或减小 gap/宽度），确保小屏不溢出
  - 预览图：一期可复用 `theme_color.png` 作为占位；若有设计资源再替换为 `theme_suixin.png`

### 5.2 Home：新增主题分支与颜色计算时机

在 `client/app/(app)/home.tsx`：

- 将 `themeMode === 'suixin'` 作为第三分支：
  - 背景为纯色（`backgroundColor = suixinColor`）
  - 不显示风景图
- **冷启动**：Home 首次进入时，读取用户画像与 `suixin.state`：
  - 若检测到新 boot 会话：重算并写入 `suixin.state`
  - 否则：直接使用持久化的 `last_color`
- **切换文案**：在现有 `triggerNextContent` 成功切换索引后：
  - 若当前主题为 `suixin`：递增 `step_index`，计算新的 `last_color`，并持久化

### 5.3 收藏（Favorites）背景记录兼容

`FavoriteItem.background` 当前对 `color` 存 `hex`，对 `scenery` 存图片索引。

- `suixin` 同样存 `hex`，与 `color` 分支一致即可

## 6. i18n 计划（TC / EN）

在 `client/src/i18n/locales/all.json` 增加：

- `theme.suixin`
- （可选）`theme.suixinDesc`（若 UI 后续展示描述）

英文命名采用语义化方案（本计划建议）：

- EN：`theme.suixin = "Ease"`
- TC：`theme.suixin = "隨心"`

> 若后续品牌希望保留音译，也可改为 EN=`Suixin`，不影响技术实现。

## 7. 兼容性与迁移

- `ThemeMode` 的存储值新增 `'suixin'`：
  - 旧版本只会存 `'scenery'|'color'`，升级后兼容
  - 若读取到未知值，继续回退 `'scenery'`
- 新增 `ui.theme.suixin.state`：
  - 若不存在，首次进入随心主题时初始化

## 8. 测试计划（最小可回归）

### 8.1 单元测试（推荐）

为颜色算法模块增加用例（可放在 `client/src/features/suixinTheme/__tests__/`）：

- `stage.unknown=1` → 必选 `neutral`
- `need={}` → 必选 `neutral`
- `need={rest_balance:1}` → 选 `rest_balance` Base Theme
- `step_index` 递增 → `t` 按往返波形变化且始终在 \([0,1]\)
- `emotion_score=null` / `<=0.3` → 不触发微扰逻辑

### 8.2 手动验收（与 spec 对齐）

- ThemeModal 能看到第三个主题并可切换
- 冷启动进入 Home：随心背景根据画像选定主题色系
- 上滑切换文案：背景色在同主题内缓慢变化（无跨主题跳色）
- `stage.unknown=1` 或 `need` 跳过：背景为 Neutral Theme
- 切换语言：主题名称在 TC/EN 下正确显示

## 9. 风险与对策

- **三卡布局拥挤**：采用 `flexWrap`/缩小卡片尺寸，必要时改为横向滚动
- **“持久化”与“冷启动重算”矛盾**：以“状态落盘 + 冷启动重置 base theme/seed”方式兼容两者
- **颜色可读性风险**：一期先用主题中间色/插值结果，避免过饱和；必要时增加对比度检查（后续迭代）

## 10. 里程碑拆分（实现顺序）

- **M1：基础接入**
  - 扩展 `ThemeMode`，ThemeModal 增加第三项与 i18n
  - Home 增加 `suixin` 分支，背景可显示（先用 neutral 兜底）
- **M2：算法落地 + 持久化**
  - 新增 suixin 颜色模块（Base/Neutral、pickTheme、lerp、t 生成）
  - 新增 `suixin.state` 存取与冷启动/切换文案更新
- **M3：回归与体验优化**
  - 收藏背景记录兼容
  - 测试补齐与边界修正（unknown/跳过/缺画像）

