257 lines
8.6 KiB
Markdown
257 lines
8.6 KiB
Markdown
# AI 建议回复功能重构说明
|
||
|
||
## 重构日期
|
||
|
||
2025-11-17
|
||
|
||
## 最新更新
|
||
|
||
2025-11-17 - 优化请求逻辑,确保每次只发送 3 次 API 请求
|
||
|
||
## 问题描述
|
||
|
||
重构前的 AI 建议功能存在以下问题:
|
||
|
||
1. **状态管理复杂**:维护了大量状态(`allSuggestions`、`loadedPages`、`batchNo`、`excContentList`、`isPageLoading`、`isRequesting` 等)
|
||
2. **页面切换逻辑复杂**:需要跟踪哪些页面已加载,切换时判断是否显示骨架屏
|
||
3. **显示逻辑不清晰**:`isLoading` 只在首次加载时为 true,分页切换时骨架屏显示不正确
|
||
4. **用户体验问题**:会出现 AI 辅助提示为空的情况(如图所示)
|
||
|
||
## 重构目标
|
||
|
||
1. **简化状态管理**:只保留必要的状态
|
||
2. **统一交互逻辑**:每次打开建议面板时,重置到第1页并重新获取数据
|
||
3. **清晰的骨架屏显示**:数据未加载完成时始终显示骨架屏
|
||
|
||
## 重构内容
|
||
|
||
### 1. `useAiReplySuggestions` Hook 重构
|
||
|
||
#### 状态简化
|
||
|
||
**重构前:**
|
||
|
||
```typescript
|
||
const [allSuggestions, setAllSuggestions] = useState<ReplySuggestion[]>([])
|
||
const [loadedPages, setLoadedPages] = useState<Set<number>>(new Set([1]))
|
||
const [isPageLoading, setIsPageLoading] = useState(false)
|
||
const [isRequesting, setIsRequesting] = useState(false)
|
||
// ... 其他状态
|
||
```
|
||
|
||
**重构后:**
|
||
|
||
```typescript
|
||
const [pageData, setPageData] = useState<Map<number, ReplySuggestion[]>>(new Map())
|
||
const [loadingPages, setLoadingPages] = useState<Set<number>>(new Set())
|
||
// ... 其他必要状态
|
||
```
|
||
|
||
#### 核心逻辑变化
|
||
|
||
**重构前:**
|
||
|
||
- `showSuggestions` 函数会根据多种条件判断是否需要获取数据
|
||
- 首次获取第1页数据,然后静默获取第2、3页
|
||
- 页面切换时复杂的加载状态判断
|
||
|
||
**重构后:**
|
||
|
||
- **每次调用 `showSuggestions` 都重置到第1页并重新获取所有数据**
|
||
- 按页存储数据到 `Map` 中,逻辑更清晰
|
||
- 页面切换时自动检查并加载缺失的页面数据
|
||
|
||
### 2. 数据获取流程优化
|
||
|
||
#### 重构前流程
|
||
|
||
1. 获取第1页 → 立即展示
|
||
2. 静默获取第2页 → 更新 `loadedPages`
|
||
3. 静默获取第3页 → 更新 `loadedPages`
|
||
4. 用户切换页面时检查 `loadedPages`
|
||
|
||
#### 重构后流程
|
||
|
||
1. 清空旧数据
|
||
2. 获取第1页 → 存入 `pageData.set(1, ...)`
|
||
3. 获取第2页 → 存入 `pageData.set(2, ...)`
|
||
4. 获取第3页 → 存入 `pageData.set(3, ...)`
|
||
5. 用户切换页面时从 `pageData` 读取,无数据则显示骨架屏
|
||
|
||
### 3. 骨架屏显示逻辑
|
||
|
||
**重构前:**
|
||
|
||
```typescript
|
||
const displaySuggestions = isCurrentPageLoaded ? suggestions : Array.from({ length: 3 }, ...);
|
||
```
|
||
|
||
**重构后:**
|
||
|
||
```typescript
|
||
const suggestions =
|
||
isCurrentPageLoading || !currentPageSuggestions
|
||
? Array.from({ length: 3 }, (_, index) => ({
|
||
id: `skeleton-${currentPage}-${index}`,
|
||
text: '',
|
||
isSkeleton: true,
|
||
}))
|
||
: currentPageSuggestions
|
||
```
|
||
|
||
UI 组件根据 `isSkeleton` 标志渲染骨架屏或真实内容。
|
||
|
||
### 4. UI 组件更新
|
||
|
||
`AiReplySuggestions.tsx` 更新:
|
||
|
||
- 添加 `isSkeleton` 字段到 `ReplySuggestion` 接口
|
||
- 检查 `suggestions.some(s => s.isSkeleton)` 决定是否显示骨架屏
|
||
- 骨架屏添加 `animate-pulse` 动画效果
|
||
|
||
## 重构优势
|
||
|
||
### 1. **简化的状态管理**
|
||
|
||
- 使用 `Map<number, ReplySuggestion[]>` 按页存储数据,结构清晰
|
||
- 移除了 `isRequesting`、`isPageLoading`、`isCurrentPageLoaded` 等冗余状态
|
||
- 只需维护 `loadingPages: Set<number>` 来跟踪正在加载的页面
|
||
|
||
### 2. **智能的数据管理**
|
||
|
||
- **有新 AI 消息时**:重置到第1页并重新获取数据
|
||
- **没有新消息时**:保留之前的建议内容和当前页面位置
|
||
- 避免了"空白建议"的问题
|
||
- 数据加载状态清晰可见(骨架屏动画)
|
||
|
||
### 3. **更可靠的数据加载**
|
||
|
||
- 智能判断是否需要重新获取数据(基于最后一条 AI 消息 ID)
|
||
- 首次打开或数据为空时自动获取
|
||
- 减少了因状态不同步导致的 bug
|
||
|
||
### 4. **更好的代码可维护性**
|
||
|
||
- 代码从 312 行减少到 200 行
|
||
- 逻辑流程更清晰直观
|
||
- 减少了状态依赖和副作用
|
||
|
||
## 使用示例
|
||
|
||
### 场景1:首次打开建议面板(渐进式加载)
|
||
|
||
1. **点击建议按钮** → 面板打开,重置到第1页,显示骨架屏
|
||
2. **~500ms 后第1页数据返回** → ✨ **立即展示第1页的3条建议**(用户可以开始浏览)
|
||
3. **后台继续获取第2、3页** → 不阻塞用户交互
|
||
4. **点击下一页** → 切换到第2页
|
||
- 如果第2页数据已加载 → 立即显示
|
||
- 如果第2页数据未加载 → 显示骨架屏(通常第2页已在后台加载完成)
|
||
|
||
### 场景2:关闭后再次打开(无新消息)
|
||
|
||
1. **关闭建议面板**
|
||
2. **再次点击建议按钮** → 面板打开
|
||
3. **保留上次的内容和页面位置**(比如之前在第2页,现在仍在第2页)
|
||
4. **无需重新加载数据**,立即显示缓存的建议
|
||
|
||
### 场景3:收到新 AI 消息后打开
|
||
|
||
1. **用户发送消息,AI 回复**
|
||
2. **点击建议按钮** → 面板打开
|
||
3. **检测到新的 AI 消息** → 重置到第1页,显示骨架屏
|
||
4. **重新获取所有建议数据**(基于最新的对话内容)
|
||
|
||
### 场景4:面板已打开时收到新 AI 消息
|
||
|
||
1. **建议面板已打开**(比如用户正在浏览第2页)
|
||
2. **用户发送消息,AI 回复**
|
||
3. **自动刷新建议数据** → **保持在当前页面**(第2页),不强制切回第1页
|
||
4. **渐进式加载新数据**,用户可以继续浏览当前页
|
||
|
||
## API 返回值变化
|
||
|
||
Hook 返回的接口保持不变,确保向后兼容:
|
||
|
||
```typescript
|
||
return {
|
||
suggestions, // ReplySuggestion[] (包含 isSkeleton 标志)
|
||
currentPage, // number
|
||
totalPages, // number
|
||
isLoading, // boolean (只要有页面在加载就为 true)
|
||
isVisible, // boolean
|
||
showSuggestions, // () => void
|
||
hideSuggestions, // () => void
|
||
handlePageChange, // (page: number) => void
|
||
}
|
||
```
|
||
|
||
移除的返回值:
|
||
|
||
- `isPageLoading`
|
||
- `isCurrentPageLoaded`
|
||
- `refreshSuggestions`
|
||
|
||
## 关键优化点
|
||
|
||
### 避免多次请求的设计
|
||
|
||
**问题**:原始实现会触发多次重复请求
|
||
|
||
**解决方案**:
|
||
|
||
1. **统一的 `fetchAllData` 函数**:一次性顺序获取3页数据,使用局部变量传递 `batchNo` 和 `excContentList`
|
||
2. **防重复调用保护**:在 `fetchAllData` 开始时检查 `loadingPages.size > 0`,如果已有加载则跳过
|
||
3. **移除分页独立加载**:删除了 `fetchPageData` 函数和 `handlePageChange` 中的数据获取逻辑
|
||
4. **简化页面切换**:`handlePageChange` 只负责切换 `currentPage`,不触发数据加载
|
||
|
||
### 渐进式加载流程
|
||
|
||
采用**渐进式加载**策略,让用户尽早看到数据,提升体验:
|
||
|
||
```typescript
|
||
fetchAllData() {
|
||
// 1. 检查防重复
|
||
if (loadingPages.size > 0) return;
|
||
|
||
// 2. 标记所有页面为加载中
|
||
setLoadingPages(new Set([1, 2, 3]));
|
||
|
||
// 3. 获取第1页 → 立即展示
|
||
const response1 = await genSupContentV2({ aiId, excContentList: [] });
|
||
setPageData(new Map([[1, response1]])); // ✨ 立即展示第1页
|
||
setLoadingPages(new Set([2, 3])); // 标记第1页已完成
|
||
|
||
// 4. 获取第2页 → 追加展示
|
||
const response2 = await genSupContentV2({ aiId, batchNo, excContentList: [response1] });
|
||
setPageData(prev => prev.set(2, response2)); // ✨ 追加第2页
|
||
setLoadingPages(new Set([3])); // 标记第2页已完成
|
||
|
||
// 5. 获取第3页 → 追加展示
|
||
const response3 = await genSupContentV2({ aiId, batchNo, excContentList: [response1, response2] });
|
||
setPageData(prev => prev.set(3, response3)); // ✨ 追加第3页
|
||
setLoadingPages(new Set()); // 全部完成
|
||
}
|
||
```
|
||
|
||
**关键优势**:
|
||
|
||
- 🚀 **第1页数据到达后立即展示**,用户无需等待所有数据
|
||
- 📊 **后续页面数据追加展示**,不影响用户浏览第1页
|
||
- ⏱️ **感知加载时间更短**,提升用户体验
|
||
- 🔄 **页面2、3可以并行渲染**,用户切换时自动显示骨架屏
|
||
|
||
## 注意事项
|
||
|
||
1. **精确的请求次数**:每次调用 `fetchAllData` 只会发送 **3 次** API 请求(第1、2、3页)
|
||
2. **智能缓存策略**:没有新 AI 消息时,复用已有数据,不发送请求
|
||
3. **网络失败处理**:任一页面加载失败会中断整个流程并清空数据
|
||
4. **Coin 不足处理**:任何页面触发 Coin 不足错误都会关闭整个建议面板
|
||
5. **防重复保护**:通过 `loadingPages.size` 检查防止并发调用
|
||
|
||
## 测试建议
|
||
|
||
1. 测试正常流程:打开建议 → 浏览3页 → 关闭 → 再次打开
|
||
2. 测试网络慢场景:确认骨架屏正确显示
|
||
3. 测试 Coin 不足场景:确认面板正确关闭
|
||
4. 测试新消息场景:发送消息后面板已打开时自动刷新
|