feat: 更新 AI 教练聊天界面和个人信息页面

- 在 AI 教练聊天界面中添加训练记录分析功能,允许用户基于近期训练记录获取分析建议
- 更新 Redux 状态管理,集成每日步数和卡路里目标
- 在个人信息页面中优化用户头像显示,支持从库中选择头像
- 修改首页布局,添加可拖动的教练徽章,提升用户交互体验
- 更新样式以适应新功能的展示和交互
This commit is contained in:
richarjiang
2025-08-13 10:09:55 +08:00
parent f3e6250505
commit 5814044cee
8 changed files with 278 additions and 54 deletions

View File

@@ -5,6 +5,7 @@ import React, { useMemo, useRef, useState } from 'react';
import {
ActivityIndicator,
FlatList,
Image,
KeyboardAvoidingView,
Platform,
StyleSheet,
@@ -20,6 +21,7 @@ import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { useAppSelector } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import type { CheckinRecord } from '@/store/checkinSlice';
type Role = 'user' | 'assistant';
@@ -29,6 +31,8 @@ type ChatMessage = {
content: string;
};
const COACH_AVATAR = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/avatar/imageCoach01.jpeg';
export default function AICoachChatScreen() {
const router = useRouter();
const params = useLocalSearchParams<{ name?: string }>();
@@ -47,11 +51,13 @@ export default function AICoachChatScreen() {
const listRef = useRef<FlatList<ChatMessage>>(null);
const planDraft = useAppSelector((s) => s.trainingPlan?.draft);
const checkin = useAppSelector((s) => (s as any).checkin);
const chips = useMemo(() => [
{ key: 'posture', label: '体态评估', action: () => router.push('/ai-posture-assessment') },
{ key: 'plan', label: 'AI制定训练计划', action: () => handleQuickPlan() },
], [router, planDraft]);
{ key: 'analyze', label: '分析运动记录', action: () => handleAnalyzeRecords() },
], [router, planDraft, checkin]);
function scrollToEnd() {
requestAnimationFrame(() => {
@@ -109,6 +115,45 @@ export default function AICoachChatScreen() {
send(prompt);
}
function buildTrainingSummary(): string {
const entries = Object.values(checkin?.byDate || {}) as CheckinRecord[];
if (!entries.length) return '';
const recent = entries.sort((a: any, b: any) => String(b.date).localeCompare(String(a.date))).slice(0, 14);
let totalSessions = 0;
let totalExercises = 0;
let totalCompleted = 0;
const categoryCount: Record<string, number> = {};
const exerciseCount: Record<string, number> = {};
for (const rec of recent) {
if (!rec?.items?.length) continue;
totalSessions += 1;
for (const it of rec.items) {
totalExercises += 1;
if (it.completed) totalCompleted += 1;
categoryCount[it.category] = (categoryCount[it.category] || 0) + 1;
exerciseCount[it.name] = (exerciseCount[it.name] || 0) + 1;
}
}
const topCategories = Object.entries(categoryCount).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([k, v]) => `${k}×${v}`);
const topExercises = Object.entries(exerciseCount).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([k, v]) => `${k}×${v}`);
return [
`统计周期:最近${recent.length}天(按有记录日计 ${totalSessions} 天)`,
`记录条目:${totalExercises},完成标记:${totalCompleted}`,
topCategories.length ? `高频类别:${topCategories.join('')}` : '',
topExercises.length ? `高频动作:${topExercises.join('')}` : '',
].filter(Boolean).join('\n');
}
function handleAnalyzeRecords() {
const summary = buildTrainingSummary();
if (!summary) {
send('我还没有可分析的打卡记录,请先在“每日打卡”添加并完成一些训练记录,然后帮我分析近期训练表现与改进建议。');
return;
}
const prompt = `请基于以下我的近期训练记录进行分析输出1整体训练负荷与节奏2动作与肌群的均衡性指出偏多/偏少3容易忽视的恢复与热身建议4后续一周的优化建议频次/时长/动作方向)。\n\n${summary}`;
send(prompt);
}
function renderItem({ item }: { item: ChatMessage }) {
const isUser = item.role === 'user';
return (
@@ -118,9 +163,7 @@ export default function AICoachChatScreen() {
style={[styles.row, { justifyContent: isUser ? 'flex-end' : 'flex-start' }]}
>
{!isUser && (
<View style={[styles.avatar, { backgroundColor: theme.primary }]}>
<Text style={styles.avatarText}>AI</Text>
</View>
<Image source={{ uri: COACH_AVATAR }} style={styles.avatar} />
)}
<View
style={[
@@ -282,7 +325,7 @@ const styles = StyleSheet.create({
},
inputRow: {
flexDirection: 'row',
alignItems: 'flex-end',
alignItems: 'center',
padding: 8,
borderWidth: 1,
borderRadius: 16,
@@ -292,8 +335,10 @@ const styles = StyleSheet.create({
flex: 1,
fontSize: 15,
maxHeight: 120,
minHeight: 40,
paddingHorizontal: 8,
paddingVertical: 6,
textAlignVertical: 'center',
},
sendBtn: {
width: 40,