Files
digital-pilates/docs/food-confirmation-implementation.md
richarjiang d52981ab29 feat: 实现聊天取消功能,提升用户交互体验
- 在教练页面中添加用户取消发送或终止回复的能力
- 更新发送按钮状态,支持发送和取消状态切换
- 在流式回复中显示取消按钮,允许用户中断助手的生成
- 增强请求管理,添加请求序列号和有效性验证,防止延迟响应影响用户体验
- 优化错误处理,区分用户主动取消和网络错误,提升系统稳定性
- 更新相关文档,详细描述取消功能的实现和用户体验设计
2025-08-18 18:59:23 +08:00

337 lines
9.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 饮食记录确认流程客户端实现
## 概述
已完成对客户端的改动支持饮食记录确认流程的新API结构。实现了对非流式JSON响应的处理能够正确显示食物选择选项并支持用户确认选择。
## 主要改动
### 1. 数据结构扩展
`app/(tabs)/coach.tsx` 中添加了新的类型定义:
```typescript
// AI选择选项数据结构
type AiChoiceOption = {
id: string;
label: string;
value: any;
recommended?: boolean;
};
// 餐次类型
type MealType = 'breakfast' | 'lunch' | 'dinner' | 'snack';
// 食物确认选项数据结构
type FoodConfirmationOption = {
id: string;
label: string;
foodName: string;
portion: string;
calories: number;
mealType: MealType;
nutritionData: {
proteinGrams?: number;
carbohydrateGrams?: number;
fatGrams?: number;
fiberGrams?: number;
};
};
// AI响应数据结构
type AiResponseData = {
content: string;
choices?: AiChoiceOption[];
interactionType?: 'text' | 'food_confirmation' | 'selection';
pendingData?: any;
context?: any;
};
```
### 2. 消息结构扩展
扩展了 `ChatMessage` 类型以支持新的字段:
```typescript
type ChatMessage = {
id: string;
role: Role;
content: string;
attachments?: MessageAttachment[];
choices?: AiChoiceOption[]; // 新增:选择选项
interactionType?: string; // 新增:交互类型
pendingData?: any; // 新增:待确认数据
context?: any; // 新增:上下文信息
};
```
### 3. 响应处理逻辑
#### 支持非流式JSON响应
修改了 `sendRequestInternal` 函数以检测和处理非流式JSON响应
```typescript
// 如果是非流式请求直接调用API并处理响应
if (!body.stream) {
try {
const response = await api.post<{ conversationId?: string; data?: AiResponseData; text?: string }>('/api/ai-coach/chat', body);
// 处理响应
if (response.data) {
// 结构化响应(可能包含选择选项)
const assistantMsg: ChatMessage = {
id: assistantId,
role: 'assistant',
content: response.data.content,
choices: response.data.choices,
interactionType: response.data.interactionType,
pendingData: response.data.pendingData,
context: response.data.context,
};
// ... 更新消息状态
}
// ...
}
}
```
#### 流式响应中的JSON检测
在流式响应处理中添加了JSON检测逻辑
```typescript
const onChunk = (chunk: string) => {
// 尝试解析是否为JSON结构化数据可能是确认选项
try {
const parsed = JSON.parse(chunk);
if (parsed && parsed.data && parsed.data.choices) {
// 处理结构化响应(包含选择选项)
// ... 创建带选择选项的消息
return;
}
} catch {
// 不是JSON继续作为普通文本处理
}
// ... 正常的文本流处理
};
```
### 4. 选择选项UI组件
`renderBubbleContent` 函数中添加了选择选项的渲染逻辑:
```typescript
// 检查是否有选择选项需要显示
if (item.choices && item.choices.length > 0 && item.interactionType === 'food_confirmation') {
return (
<View style={{ gap: 12 }}>
<Markdown style={markdownStyles} mergeStyle>
{item.content || ''}
</Markdown>
<View style={styles.choicesContainer}>
{item.choices.map((choice) => (
<TouchableOpacity
key={choice.id}
style={[
styles.choiceButton,
choice.recommended && styles.choiceButtonRecommended
]}
onPress={() => handleChoiceSelection(choice, item)}
>
<View style={styles.choiceContent}>
<Text style={[
styles.choiceLabel,
choice.recommended && styles.choiceLabelRecommended
]}>
{choice.label}
</Text>
{choice.recommended && (
<View style={styles.recommendedBadge}>
<Text style={styles.recommendedText}>推荐</Text>
</View>
)}
</View>
</TouchableOpacity>
))}
</View>
</View>
);
}
```
### 5. 选择确认逻辑
实现了 `handleChoiceSelection` 函数处理用户选择:
```typescript
async function handleChoiceSelection(choice: AiChoiceOption, message: ChatMessage) {
try {
// 构建确认请求
const confirmationText = `我选择记录${choice.label}`;
// 发送确认消息,包含选择的数据
await sendStreamWithConfirmation(confirmationText, choice.id, {
selectedOption: choice.value,
imageUrl: message.pendingData?.imageUrl
});
} catch (e: any) {
Alert.alert('选择失败', e?.message || '选择失败,请重试');
}
}
```
### 6. 确认数据发送
添加了 `sendStreamWithConfirmation` 函数:
```typescript
async function sendStreamWithConfirmation(text: string, selectedChoiceId: string, confirmationData: any) {
const historyForServer = convertToServerMessages(messages);
const cid = ensureConversationId();
const body = {
conversationId: cid,
messages: [...historyForServer, { role: 'user' as const, content: text }],
selectedChoiceId,
confirmationData,
stream: false, // 确认阶段使用非流式
};
await sendRequestInternal(body, text);
}
```
### 7. UI样式
添加了选择选项相关的样式:
```typescript
choicesContainer: {
gap: 8,
},
choiceButton: {
backgroundColor: 'rgba(255,255,255,0.9)',
borderWidth: 1,
borderColor: 'rgba(187,242,70,0.3)',
borderRadius: 12,
padding: 12,
},
choiceButtonRecommended: {
borderColor: 'rgba(187,242,70,0.6)',
backgroundColor: 'rgba(187,242,70,0.1)',
},
// ... 其他相关样式
```
## 使用流程
1. **用户发送饮食图片**:用户点击"#记饮食"并上传食物图片
2. **AI识别返回选项**服务器返回食物识别结果和确认选项非流式JSON
3. **显示选择选项**:客户端渲染食物选择选项,推荐项目带有特殊标识
4. **用户选择确认**:用户点击某个选项
5. **发送确认数据**:客户端发送确认请求,包含选择的食物数据
6. **记录完成**:服务器记录饮食数据并返回确认消息
## 兼容性
- 保持对现有流式文本响应的完全兼容
- 自动检测响应类型JSON vs 文本流)
- 旧的饮食记录方式(文字输入)继续正常工作
- 缓存和会话管理支持新的消息结构
## 技术要点
1. **响应类型检测**客户端能够自动识别JSON结构化响应和普通文本流
2. **状态管理**:新增状态正确地保存到本地缓存
3. **用户体验**:选择选项有清晰的视觉反馈,推荐选项突出显示
4. **错误处理**:完整的错误处理机制,确保用户体验流畅
5. **性能优化**:避免不必要的重渲染,保持界面响应性
## 选择状态管理与用户体验优化
### 防重复点击机制
为了防止用户重复发送消息,实现了完善的状态管理:
```typescript
const [selectedChoices, setSelectedChoices] = useState<Record<string, string>>({}); // messageId -> choiceId
const [pendingChoiceConfirmation, setPendingChoiceConfirmation] = useState<Record<string, boolean>>({}); // messageId -> loading
```
### 选择状态逻辑
1. **状态检查**:点击前检查是否已选择
2. **立即锁定**:点击后立即设置选中状态,防止重复点击
3. **视觉反馈**:选中项显示特殊样式,其他选项变为禁用状态
4. **加载指示**发送过程中显示loading动画
5. **错误恢复**:发送失败时重置状态,允许重新选择
### UI状态展示
- **正常状态**:常规样式,可点击
- **推荐状态**:特殊边框和背景色,显示"推荐"标签
- **选中状态**:深色边框,绿色背景,显示"已选择"标签
- **禁用状态**:灰色透明,不可点击
- **加载状态**:显示旋转加载动画
### 触觉反馈
- **选择时**`Haptics.selectionAsync()` - 轻微选择反馈
- **成功时**`Haptics.NotificationFeedbackType.Success` - 成功通知
- **失败时**`Haptics.NotificationFeedbackType.Error` - 错误通知
### 代码示例
```typescript
// 状态管理
const isSelected = selectedChoices[item.id] === choice.id;
const isAnySelected = selectedChoices[item.id] != null;
const isPending = pendingChoiceConfirmation[item.id];
const isDisabled = isAnySelected && !isSelected;
// 点击处理
onPress={() => {
if (!isDisabled && !isPending) {
Haptics.selectionAsync();
handleChoiceSelection(choice, item);
}
}}
// 选择处理逻辑
async function handleChoiceSelection(choice: AiChoiceOption, message: ChatMessage) {
// 防重复检查
if (selectedChoices[message.id] != null) {
return;
}
// 立即设置状态
setSelectedChoices(prev => ({ ...prev, [message.id]: choice.id }));
setPendingChoiceConfirmation(prev => ({ ...prev, [message.id]: true }));
try {
// 发送确认
await sendStreamWithConfirmation(confirmationText, choice.id, confirmationData);
// 成功反馈
setPendingChoiceConfirmation(prev => ({ ...prev, [message.id]: false }));
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
} catch (e) {
// 失败时重置状态
setSelectedChoices(prev => {
const { [message.id]: _, ...rest } = prev;
return rest;
});
setPendingChoiceConfirmation(prev => {
const { [message.id]: _, ...rest } = prev;
return rest;
});
// 错误反馈
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
Alert.alert('选择失败', e?.message || '选择失败,请重试');
}
}
```
这个实现完全符合API文档的要求支持饮食记录的两阶段确认流程并保持了与现有功能的兼容性。同时提供了优秀的用户体验包括防重复点击、状态反馈、触觉反馈等功能。