- 在教练页面中添加用户取消发送或终止回复的能力 - 更新发送按钮状态,支持发送和取消状态切换 - 在流式回复中显示取消按钮,允许用户中断助手的生成 - 增强请求管理,添加请求序列号和有效性验证,防止延迟响应影响用户体验 - 优化错误处理,区分用户主动取消和网络错误,提升系统稳定性 - 更新相关文档,详细描述取消功能的实现和用户体验设计
337 lines
9.8 KiB
Markdown
337 lines
9.8 KiB
Markdown
# 饮食记录确认流程客户端实现
|
||
|
||
## 概述
|
||
|
||
已完成对客户端的改动,支持饮食记录确认流程的新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文档的要求,支持饮食记录的两阶段确认流程,并保持了与现有功能的兼容性。同时提供了优秀的用户体验,包括防重复点击、状态反馈、触觉反馈等功能。
|