iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Build on Google AI

單鐵的人生如履薄冰!AI 教練 APP 30天開發旅程,你說能走到最後嗎?系列 第 20

Day 20 | 介面實作 (3):打造課表診療室,實作疲勞度打卡與文字發問介面

  • 分享至 

  • xImage
  •  

前言:計畫永遠趕不上變化,靜態課表的死穴

昨天成功把 JSON 轉化為極具質感的課表 UI。看著 4 週訓練計畫,感覺跑者隨時都可以準備跑了。
但在實際狀況裡,備賽過程從來不是一帆風順的。比如晚上突然要臨時加班到半夜只能跑 20 分鐘;或是週末去打了一場高強度網球賽導致大腿痠痛無法下樓梯。此時若強行執行原本 14 公里的配速跑,極易導致受傷。

傳統的 PDF 課表無法因應突發狀況。但 KAKERU 擁有 Gemini AI 大腦!今天我們要打造「AI 教練診療室 (Clinic 頁面)」,收集跑者的生理疲勞與生活突發事件,為明天的「動態重算課表」鋪路!

觀念解說:對齊後端 POST 協議

在後端架構中,動態調整課表走的是專屬端點 POST /adjust。為了讓 AI 能精準重算剩餘課表,前端在診療室需要打包以下 Context:

  1. 疲勞指數選擇器 (1~5 分):直覺的 Emoji 表情按鈕。
  2. 生活突發與狀況描述 (Context):文字框搭配橫向滑動的「加班/痠痛/出差」快捷標籤。
  3. 當前進度與原始課表:自動附帶目前第幾週、星期幾與 Zustand 裡的既有課表。

動手實作 1:建構疲勞度與生活情境快捷標籤 (ClinicScreen)

在 src/app/clinic.tsx 中,我們定義 1~5 分疲勞度與涵蓋「生理疲勞」及「生活突發」的快捷標籤:

const FATIGUE_OPTIONS = [
  { level: 1, label: '極佳', emoji: '🤩', desc: '體能充沛' },
  { level: 2, label: '良好', emoji: '🙂', desc: '狀態良好' },
  { level: 3, label: '普通', emoji: '😐', desc: '正常體能' },
  { level: 4, label: '疲勞', emoji: '😮‍💨', desc: '肌肉痠緊' },
  { level: 5, label: '極累', emoji: '😫', desc: '體能透支' },
];

const QUICK_TAGS = [
  '💻 臨時加班,今天只能跑 30 分鐘',
  '🎾 週末打球大腿極度痠痛,想改休息',
  '🦵 右膝外側微痛,想改做核心與伸展',
  '🌧️ 外面下大雨且無跑步機,想改室內訓練',
  '✈️ 明後天出差聚會,想將長距離移到週末',
];

export default function ClinicScreen() {
  const [fatigueLevel, setFatigueLevel] = useState(3);
  const [feedbackText, setFeedbackText] = useState('');

  return (
    <View style={styles.section}>
      {/* 1. 疲勞度按鈕列 */}
      <View style={styles.emojiContainer}>
        {FATIGUE_OPTIONS.map((item) => (
          <TouchableOpacity
            key={item.level}
            style={[styles.emojiButton, fatigueLevel === item.level && styles.emojiButtonActive]}
            onPress={() => setFatigueLevel(item.level)}
          >
            <Text style={styles.emojiIcon}>{item.emoji}</Text>
            <Text style={[styles.emojiLabel, fatigueLevel === item.level && styles.emojiLabelActive]}>
              {item.label}
            </Text>
          </TouchableOpacity>
        ))}
      </View>

      {/* 2. 生活突發與痠痛快捷標籤 (橫向滑動節省空間) */}
      <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.quickTagsScroll}>
        {QUICK_TAGS.map((tag, idx) => (
          <TouchableOpacity key={idx} style={styles.quickTagChip} onPress={() => setFeedbackText(tag)}>
            <Text style={styles.quickTagText}>+ {tag}</Text>
          </TouchableOpacity>
        ))}
      </ScrollView>
    </View>
  );
}

動手實作 2:多行文字框、鍵盤體驗優化與後端 Payload 封裝

為了防止手機鍵盤彈出時遮擋輸入框,我們在 ScrollView 啟用了原生 automaticallyAdjustKeyboardInsets 與 keyboardDismissMode="on-drag",並在送出時將資料打包為符合後端 AdjustScheduleRequest 的格式:

const currentPlan = useStore((state) => state.currentPlan);
const selectedWeek = useStore((state) => state.selectedWeek);
const sessionId = useStore((state) => state.sessionId);

const handleSubmitCheckin = () => {
  const selectedOption = FATIGUE_OPTIONS.find((o) => o.level === fatigueLevel);
  const dayOfWeek = new Date().getDay() === 0 ? 7 : new Date().getDay(); // 1=週一 ... 7=週日

  // 封裝完全對齊後端 POST /adjust 協議
  const adjustPayload = {
    sessionId: sessionId || undefined,
    originalScheduleJson: JSON.stringify(currentPlan?.data || {}),
    userFeedback: `[疲勞度: ${fatigueLevel}/5 - ${selectedOption?.label}] ${feedbackText.trim() || '因應今日狀況請微調'}`,
    currentWeek: selectedWeek || 1,
    currentDay: dayOfWeek,
  };

  console.log('🩺 打包完成,準備發送給 AI 教練:', adjustPayload);
};

動手實作 3:UI 樣式美化

const styles = StyleSheet.create({
  emojiContainer: { flexDirection: 'row', justifyContent: 'space-between', gap: 8 },
  emojiButton: { flex: 1, alignItems: 'center', paddingVertical: 14, borderRadius: 14, backgroundColor: '#FFF', borderWidth: 1, borderColor: '#E5E7EB' },
  emojiButtonActive: { backgroundColor: '#FFF0EB', borderColor: '#FF5722' },
  emojiLabelActive: { color: '#FF5722', fontWeight: '800' },
  quickTagsScroll: { flexDirection: 'row', gap: 8, paddingVertical: 2 },
  quickTagChip: { backgroundColor: '#FFF', borderWidth: 1, borderColor: '#E2E8F0', paddingHorizontal: 12, paddingVertical: 8, borderRadius: 12 },
  textArea: { backgroundColor: '#FFF', borderRadius: 14, padding: 16, minHeight: 120, borderWidth: 1, borderColor: '#E5E7EB' },
  submitButton: { backgroundColor: '#1E2430', paddingVertical: 16, borderRadius: 14, alignItems: 'center' },
  submitButtonText: { color: '#FFF', fontSize: 16, fontWeight: '800' },
});

現在打開 Expo Go 點擊「診療室」分頁,就能看到疲勞度表情按鈕、橫向快捷情境標籤與流暢不遮擋的文字框!
介面如下:
https://ithelp.ithome.com.tw/upload/images/20260829/20165043cKpMeArUPg.png

點選調整課表後:
https://ithelp.ithome.com.tw/upload/images/20260829/20165043ZQf3lQOTdn.png

今日總結與明日預告

今天打造了「AI 教練診療室」介面,不僅涵蓋了生理疲勞回報,更兼顧了跑者日常中常遇到的「臨時加班、出差」等突發狀況,並在前端將通訊協定封裝就緒。

明天(Day 21)將正式呼叫 Cloud Run 上的 API 端點! 當後端 Gemini 接收到「臨時加班 / 大腿痠痛」的請求後,會如何重新動態調整後續課表?重算後的資料又該如何無縫刷新回 Zustand 與手機畫面?我們明天見真章!

爛笑話角落

(午餐時間)
友人:今天要吃啥?
:今天得餓死…
友人:蛤為什麼要餓死,back number演唱會周邊買太多?
:今天鐵人賽 Day20。
友人:...
友人:0分超爛,去餓薯好了。


上一篇
Day 19 | 介面實作 (2):告別生硬 JSON!拆解 UI 元件並完美綁定 AI 課表資料
下一篇
Day 21 | AI 動態重算實戰:將狀態發送至雲端,智慧合併新課表並無縫刷新 UI
系列文
單鐵的人生如履薄冰!AI 教練 APP 30天開發旅程,你說能走到最後嗎?22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言