276 lines
9.7 KiB
Markdown
276 lines
9.7 KiB
Markdown
|
|
# Home Composer Redesign Implementation Plan
|
|||
|
|
|
|||
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|||
|
|
|
|||
|
|
**Goal:** 重做 Home 输入组件,统一为单胶囊容器并稳定语音长按交互,消除布局漂移,同时保持 `+` 与发送等既有业务逻辑不变。
|
|||
|
|
|
|||
|
|
**Architecture:** 采用“单容器双模式”方案:`HomeScreen` 继续持有业务状态与动作,新增 `HomeComposer` 专注 UI 与手势分发;中间区域用受控状态在文本/按住说话/录音中/识别中之间切换,外层高度固定。录音提示与声波动画都内聚在主容器内部渲染,避免额外布局占位。
|
|||
|
|
|
|||
|
|
**Tech Stack:** Flutter, flutter_bloc, lucide_icons, design tokens (`AppColors`/`AppSpacing`/`AppRadius`), widget test
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Task 1: 建立 HomeComposer 组件骨架与参数契约
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `apps/lib/features/home/ui/widgets/home_composer.dart`
|
|||
|
|
- Modify: `apps/lib/features/home/ui/screens/home_screen.dart`
|
|||
|
|
- Test: `apps/test/features/home/ui/widgets/home_composer_test.dart`
|
|||
|
|
|
|||
|
|
**Step 1: 写失败测试(渲染结构)**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
testWidgets('renders one unified rounded composer container', (tester) async {
|
|||
|
|
// pump HomeComposer with minimum required callbacks/states
|
|||
|
|
// expect: one root container, plus button, center content slot, right action slot
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: 运行测试确认失败**
|
|||
|
|
|
|||
|
|
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "renders one unified rounded composer container"`
|
|||
|
|
Expected: FAIL(`HomeComposer` 未定义或结构不匹配)
|
|||
|
|
|
|||
|
|
**Step 3: 写最小实现**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
class HomeComposer extends StatelessWidget {
|
|||
|
|
const HomeComposer({
|
|||
|
|
super.key,
|
|||
|
|
required this.isHoldToSpeakMode,
|
|||
|
|
required this.isRecording,
|
|||
|
|
required this.isTranscribing,
|
|||
|
|
required this.hasMessage,
|
|||
|
|
required this.isWaitingAgent,
|
|||
|
|
required this.onTapPlus,
|
|||
|
|
required this.onTapRightAction,
|
|||
|
|
required this.centerChild,
|
|||
|
|
});
|
|||
|
|
// unified capsule container with left/center/right slots
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: 再跑测试确认通过**
|
|||
|
|
|
|||
|
|
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "renders one unified rounded composer container"`
|
|||
|
|
Expected: PASS
|
|||
|
|
|
|||
|
|
**Step 5: 小步提交(仅用户明确要求时)**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add apps/lib/features/home/ui/widgets/home_composer.dart apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/home/ui/widgets/home_composer_test.dart
|
|||
|
|
git commit -m "refactor: extract unified home composer container"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 2: 完成右侧图标状态映射(activity/keyboard/send/stop)
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `apps/lib/features/home/ui/widgets/home_composer.dart`
|
|||
|
|
- Modify: `apps/lib/features/home/ui/screens/home_screen.dart`
|
|||
|
|
- Test: `apps/test/features/home/ui/widgets/home_composer_test.dart`
|
|||
|
|
|
|||
|
|
**Step 1: 写失败测试(图标状态机)**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
testWidgets('right action icon follows state priority', (tester) async {
|
|||
|
|
// waiting > hasMessage > holdToSpeakMode > textMode
|
|||
|
|
// expect LucideIcons.square/send/keyboard/activity respectively
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: 运行测试确认失败**
|
|||
|
|
|
|||
|
|
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "right action icon follows state priority"`
|
|||
|
|
Expected: FAIL(图标选择逻辑尚未完整实现)
|
|||
|
|
|
|||
|
|
**Step 3: 最小实现图标决策**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
IconData resolveRightIcon(...) {
|
|||
|
|
if (isWaitingAgent) return LucideIcons.square;
|
|||
|
|
if (hasMessage) return LucideIcons.send;
|
|||
|
|
return isHoldToSpeakMode ? LucideIcons.keyboard : LucideIcons.activity;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: 再跑测试确认通过**
|
|||
|
|
|
|||
|
|
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "right action icon follows state priority"`
|
|||
|
|
Expected: PASS
|
|||
|
|
|
|||
|
|
**Step 5: 小步提交(仅用户明确要求时)**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add apps/lib/features/home/ui/widgets/home_composer.dart apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/home/ui/widgets/home_composer_test.dart
|
|||
|
|
git commit -m "refactor: stabilize composer right action icon mapping"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 3: 实现中间区域双模式替换并固定高度
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `apps/lib/features/home/ui/widgets/home_composer.dart`
|
|||
|
|
- Modify: `apps/lib/features/home/ui/screens/home_screen.dart`
|
|||
|
|
- Test: `apps/test/features/home/ui/widgets/home_composer_test.dart`
|
|||
|
|
|
|||
|
|
**Step 1: 写失败测试(模式切换不改变高度)**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
testWidgets('composer height remains stable across mode switches', (tester) async {
|
|||
|
|
// measure size in text mode and hold-to-speak mode
|
|||
|
|
// expect equal heights
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: 运行测试确认失败**
|
|||
|
|
|
|||
|
|
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "composer height remains stable across mode switches"`
|
|||
|
|
Expected: FAIL(当前结构切换时高度波动)
|
|||
|
|
|
|||
|
|
**Step 3: 最小实现(AnimatedSwitcher + fixed constraints)**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
SizedBox(
|
|||
|
|
height: composerHeight,
|
|||
|
|
child: AnimatedSwitcher(
|
|||
|
|
duration: switchDuration,
|
|||
|
|
child: isHoldToSpeakMode ? holdToSpeakChild : textInputChild,
|
|||
|
|
),
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: 再跑测试确认通过**
|
|||
|
|
|
|||
|
|
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "composer height remains stable across mode switches"`
|
|||
|
|
Expected: PASS
|
|||
|
|
|
|||
|
|
**Step 5: 小步提交(仅用户明确要求时)**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add apps/lib/features/home/ui/widgets/home_composer.dart apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/home/ui/widgets/home_composer_test.dart
|
|||
|
|
git commit -m "refactor: keep composer layout stable during mode switch"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 4: 实现长按录音交互(开始/上滑取消/松开发送)
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `apps/lib/features/home/ui/screens/home_screen.dart`
|
|||
|
|
- Modify: `apps/lib/features/home/ui/widgets/home_composer.dart`
|
|||
|
|
- Test: `apps/test/features/home/ui/widgets/home_composer_test.dart`
|
|||
|
|
|
|||
|
|
**Step 1: 写失败测试(录音提示只在 recording)**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
testWidgets('recording hint appears only while recording', (tester) async {
|
|||
|
|
// idle hold-to-speak: no hint
|
|||
|
|
// recording: show "松开发送,上滑取消"
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: 运行测试确认失败**
|
|||
|
|
|
|||
|
|
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "recording hint appears only while recording"`
|
|||
|
|
Expected: FAIL
|
|||
|
|
|
|||
|
|
**Step 3: 最小实现录音流程映射**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
onLongPressStart => HapticFeedback.lightImpact() + onHoldStart();
|
|||
|
|
onLongPressMoveUpdate => if (dy < threshold) onHoldCancel();
|
|||
|
|
onLongPressEnd => onHoldEnd(autoSend: true);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: 再跑测试确认通过**
|
|||
|
|
|
|||
|
|
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "recording hint appears only while recording"`
|
|||
|
|
Expected: PASS
|
|||
|
|
|
|||
|
|
**Step 5: 小步提交(仅用户明确要求时)**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add apps/lib/features/home/ui/widgets/home_composer.dart apps/lib/features/home/ui/screens/home_screen.dart apps/test/features/home/ui/widgets/home_composer_test.dart
|
|||
|
|
git commit -m "feat: rework hold-to-speak interaction with stable recording state"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 5: 视觉重构为轻拟物胶囊(仅 tokens)
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `apps/lib/features/home/ui/widgets/home_composer.dart`
|
|||
|
|
- Modify: `apps/lib/core/theme/design_tokens.dart`(仅当现有 token 不足时新增)
|
|||
|
|
- Test: `apps/test/features/home/ui/widgets/home_composer_test.dart`
|
|||
|
|
|
|||
|
|
**Step 1: 写失败测试(主容器统一性)**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
testWidgets('plus, center and right action are inside same capsule', (tester) async {
|
|||
|
|
// find one capsule host and verify children are descendants
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: 运行测试确认失败**
|
|||
|
|
|
|||
|
|
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "plus, center and right action are inside same capsule"`
|
|||
|
|
Expected: FAIL
|
|||
|
|
|
|||
|
|
**Step 3: 最小视觉实现(不硬编码)**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// use AppColors/AppSpacing/AppRadius and existing shadow tokens
|
|||
|
|
// no hardcoded color/spacing/radius/size
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: 再跑测试确认通过**
|
|||
|
|
|
|||
|
|
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart --plain-name "plus, center and right action are inside same capsule"`
|
|||
|
|
Expected: PASS
|
|||
|
|
|
|||
|
|
**Step 5: 小步提交(仅用户明确要求时)**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add apps/lib/features/home/ui/widgets/home_composer.dart apps/lib/core/theme/design_tokens.dart apps/test/features/home/ui/widgets/home_composer_test.dart
|
|||
|
|
git commit -m "refactor: redesign home composer with neumorphic capsule style"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Task 6: 集成回归与文档同步
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `apps/lib/features/home/ui/screens/home_screen.dart`
|
|||
|
|
- Modify: `docs/runtime/runtime-route.md`(若交互说明有变化)
|
|||
|
|
|
|||
|
|
**Step 1: 运行目标测试文件**
|
|||
|
|
|
|||
|
|
Run: `flutter test test/features/home/ui/widgets/home_composer_test.dart`
|
|||
|
|
Expected: PASS
|
|||
|
|
|
|||
|
|
**Step 2: 运行 Home 相关回归测试(若新增)**
|
|||
|
|
|
|||
|
|
Run: `flutter test test/features/home`
|
|||
|
|
Expected: PASS(若目录存在)
|
|||
|
|
|
|||
|
|
**Step 3: 运行应用侧基础回归**
|
|||
|
|
|
|||
|
|
Run: `flutter test`
|
|||
|
|
Expected: PASS 或仅存在与本改动无关的已知失败
|
|||
|
|
|
|||
|
|
**Step 4: 记录验证结论**
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
- 输入组件统一容器:通过
|
|||
|
|
- 模式切换稳定:通过
|
|||
|
|
- 录音提示条件:通过
|
|||
|
|
- + 按钮/发送逻辑:通过
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 5: 小步提交(仅用户明确要求时)**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add apps/lib/features/home/ui/screens/home_screen.dart apps/lib/features/home/ui/widgets/home_composer.dart apps/test/features/home/ui/widgets/home_composer_test.dart docs/runtime/runtime-route.md
|
|||
|
|
git commit -m "test: add regression coverage for home composer redesign"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 实施注意事项
|
|||
|
|
|
|||
|
|
- 保持 `HomeScreen` 作为业务状态单一来源,避免在 `HomeComposer` 内部复制业务状态。
|
|||
|
|
- 录音中 (`recording`) 禁止触发模式切换,防止并发手势引发错位。
|
|||
|
|
- 严格遵守 `apps/AGENTS.md`:不硬编码视觉值,必须使用 design tokens。
|
|||
|
|
- 用户反馈统一使用 `Toast.show(...)`,不得引入 `SnackBar`。
|