Skip to content

feat:add camera#15

Merged
Rikublender3d merged 3 commits intomainfrom
feat/add-camera
Dec 13, 2025
Merged

feat:add camera#15
Rikublender3d merged 3 commits intomainfrom
feat/add-camera

Conversation

@Rikublender3d
Copy link
Copy Markdown
Contributor

@Rikublender3d Rikublender3d commented Dec 13, 2025

Summary by CodeRabbit

  • New Features

    • Camera-based recording across modes (AUTO, RUNNING, SQUAT, PUSHUP) with permission prompts, on-screen mode selector, recording indicator, and processing overlay.
    • Solo training: 10s recording sessions with score display (max/avg/last) and processing feedback.
    • Battles: attacks now use video-derived scores to calculate damage; attack controls disabled while recording/processing.
  • Other

    • Enemy HP values adjusted for gameplay balance.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Dec 13, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Added Vision Camera-based recording and scoring: camera/microphone permissions, device selection, recording lifecycle, video upload to scoring API (mock/real), mode selection, and score-driven damage/application in battle and solo screens.

Changes

Cohort / File(s) Summary
Configuration & Dependencies
frontend/app.json, frontend/package.json
Added react-native-vision-camera plugin entry with camera/microphone permission texts; added dependencies expo-camera and @expo/ngrok.
Entities & Utilities
frontend/entities/game/mode.ts, frontend/utils/api.ts, frontend/utils/score.ts
Added AUTO to Mode enum; updated dummy enemy HP values; added ScoreResponse type and uploadVideoForScore (mock/real upload, mock generator, error handling).
Screens / UI Components
frontend/app/battle-game.tsx, frontend/app/solo.tsx
Integrated camera preview and permission/device handling; recording start/stop (10s auto-stop in solo); upload → parse → apply score flow; UI overlays for recording/processing; mode selector and disabled states during async operations; damage calculation from returned scores.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as App Screen (Battle/Solo)
    participant Camera as Vision Camera
    participant Upload as uploadVideoForScore (Client)
    participant API as Score Service

    User->>UI: select mode & tap Record
    UI->>Camera: request permission / start preview
    UI->>Camera: start recording
    activate Camera
    Note over UI,Camera: Recording (user performs exercise)
    Camera-->>Camera: auto-stop or manual stop
    deactivate Camera
    UI->>UI: show processing overlay
    UI->>Upload: send videoUri + mode
    activate Upload
    Upload->>API: POST video + mode (or generate mock)
    API-->>Upload: return score JSON
    deactivate Upload
    Upload-->>UI: raw response string
    UI->>UI: parse score, compute damage, update state/UI
    UI-->>User: show score summary / battle result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Focus review on camera lifecycle and cleanup in battle-game.tsx / solo.tsx to avoid media leaks or race conditions.
  • Validate uploadVideoForScore parsing, error propagation, and mock vs real API behavior in frontend/utils/score.ts.
  • Confirm Mode.AUTO usage across screens and any type impact.
  • Check updated dummy HP values in frontend/utils/api.ts align with intended game balance.

Poem

🐰 I hopped into the camera's view,
I recorded a squat, a jump, a cue.
Videos sent to distant skies,
Returned as scores and victory cries.
Hooray — we train, we battle, we grew!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat:add camera' is concise and directly related to the main changes, which involve integrating Vision Camera throughout the codebase for recording and scoring functionality.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/add-camera

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/app/battle-game.tsx (1)

142-153: Stale closure in enemyAttack for userHp.

This function is called via setTimeout from onRecordingFinished, inheriting the same stale closure issue. userHp at line 144 may not reflect the current state.

   const enemyAttack = () => {
     const damage = Math.floor(Math.random() * 15) + 5
-    const newUserHp = Math.max(userHp - damage, 0)
-    setUserHp(newUserHp)
-    addLog(`敵の攻撃!${damage}ダメージ!`)
-
-    if (newUserHp <= 0) {
-      setIsGameOver(true)
-      setWinner('enemy')
-      addLog('やられた...')
-    }
+    setUserHp((currentHp) => {
+      const newUserHp = Math.max(currentHp - damage, 0)
+      addLog(`敵の攻撃!${damage}ダメージ!`)
+
+      if (newUserHp <= 0) {
+        setIsGameOver(true)
+        setWinner('enemy')
+        addLog('やられた...')
+      }
+      return newUserHp
+    })
   }
🧹 Nitpick comments (6)
frontend/package.json (1)

14-14: Move @expo/ngrok to devDependencies.

@expo/ngrok is a development tunneling tool and should not be bundled in production. Move it to devDependencies.

  "dependencies": {
-   "@expo/ngrok": "^4.1.3",
    "@expo/vector-icons": "^15.0.3",
  "devDependencies": {
+   "@expo/ngrok": "^4.1.3",
    "@types/react": "~19.1.0",
frontend/utils/score.ts (2)

3-6: Extract configuration to environment variables.

Hardcoding the API URL and mock flag makes it difficult to switch environments. Consider using Expo's environment configuration or constants.

-const SCORE_API_URL = 'https://sponsored-media-compatibility-captured.trycloudflare.com/api/debug'
+import Constants from 'expo-constants'

-// ダミーモード(開発用)
-const USE_MOCK = true  // false にすると本物のAPIを使う
+const SCORE_API_URL = Constants.expoConfig?.extra?.scoreApiUrl ?? 'http://localhost:8000/api/debug'
+const USE_MOCK = Constants.expoConfig?.extra?.useMockScore ?? __DEV__

36-42: Add explicit return type and remove debug logging.

Per coding guidelines, exported functions should have explicit return types. Also, consider removing or guarding debug console.log statements.

 export const uploadVideoForScore = async (
   videoUri: string,
   mode: Mode,
-): Promise<string> => {
-  console.log('Video URI:', videoUri)
-  console.log('Mode:', mode)
-  console.log('Mock mode:', USE_MOCK)
+): Promise<string> => {
+  if (__DEV__) {
+    console.log('Video URI:', videoUri)
+    console.log('Mode:', mode)
+    console.log('Mock mode:', USE_MOCK)
+  }
frontend/app/solo.tsx (1)

186-191: Add defensive check for processing_time_seconds.

While processing_time_seconds is typed as non-nullable, API responses can be unpredictable. Consider adding a fallback for robustness.

           <View style={styles.scoreRow}>
             <Text style={styles.scoreLabel}>処理時間:</Text>
             <Text style={styles.scoreValue}>
-              {scoreResult.processing_time_seconds.toFixed(2)}秒
+              {scoreResult.processing_time_seconds?.toFixed(2) ?? 'N/A'}秒
             </Text>
           </View>
frontend/app/battle-game.tsx (2)

10-13: Consider reusing ScoreResponse from @/utils/score.

Per the AI summary, frontend/utils/score.ts exports a ScoreResponse interface. Defining a separate ScoreSummary here creates duplication and risks divergence if the API response shape changes.

-interface ScoreSummary {
-  max_score: number | null
-  avg_score: number | null
-}
+import { uploadVideoForScore, ScoreResponse } from '@/utils/score'

Then use ScoreResponse in place of ScoreSummary at line 88.


128-133: Consider cleanup for recording timeout on unmount.

If the user flees (navigates away) during the 3-second recording window, this setTimeout continues and may attempt stopRecording on an unmounted component, potentially causing a memory leak warning.

Store the timeout ID and clear it on component unmount:

const recordingTimeoutRef = useRef<NodeJS.Timeout | null>(null)

// In playerAttack:
recordingTimeoutRef.current = setTimeout(async () => {
  if (camera.current) {
    await camera.current.stopRecording()
  }
}, 3000)

// Add cleanup effect:
useEffect(() => {
  return () => {
    if (recordingTimeoutRef.current) {
      clearTimeout(recordingTimeoutRef.current)
    }
  }
}, [])
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c7f634f and c7ef5c3.

⛔ Files ignored due to path filters (1)
  • frontend/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • frontend/app.json (1 hunks)
  • frontend/app/battle-game.tsx (10 hunks)
  • frontend/app/solo.tsx (2 hunks)
  • frontend/entities/game/mode.ts (1 hunks)
  • frontend/package.json (2 hunks)
  • frontend/utils/api.ts (1 hunks)
  • frontend/utils/score.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
frontend/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

frontend/**/*.{ts,tsx}: Frontend Expo Router screens live under frontend/app/ (e.g., frontend/app/(tabs)/_layout.tsx), shared UI in frontend/components/ and frontend/components/ui/, assets in frontend/assets/images/, and utilities/hooks in frontend/hooks/ and frontend/constants/
Expo SDK 54 is in use; match the documented Node/Java versions for native tooling

Files:

  • frontend/entities/game/mode.ts
  • frontend/utils/score.ts
  • frontend/app/battle-game.tsx
  • frontend/utils/api.ts
  • frontend/app/solo.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript; prefer explicit return types on exported functions and components

Files:

  • frontend/entities/game/mode.ts
  • frontend/utils/score.ts
  • frontend/app/battle-game.tsx
  • frontend/utils/api.ts
  • frontend/app/solo.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use 2-space indentation, single quotes, and trailing commas per ESLint/Prettier defaults

Files:

  • frontend/entities/game/mode.ts
  • frontend/utils/score.ts
  • frontend/app/battle-game.tsx
  • frontend/utils/api.ts
  • frontend/app/solo.tsx
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

React components: Use PascalCase filenames (e.g., HelloWave.tsx)

Files:

  • frontend/app/battle-game.tsx
  • frontend/app/solo.tsx
frontend/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Route files must follow Expo Router patterns (e.g., app/modal.tsx)

Files:

  • frontend/app/battle-game.tsx
  • frontend/app/solo.tsx
🧠 Learnings (3)
📚 Learning: 2025-12-08T15:54:59.341Z
Learnt from: CR
Repo: befoafsuger/rizap2025 PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-08T15:54:59.341Z
Learning: Applies to frontend/**/*.{ts,tsx} : Expo SDK 54 is in use; match the documented Node/Java versions for native tooling

Applied to files:

  • frontend/package.json
  • frontend/app.json
📚 Learning: 2025-12-08T15:54:59.341Z
Learnt from: CR
Repo: befoafsuger/rizap2025 PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-08T15:54:59.341Z
Learning: Frontend: from `frontend/`, run `npm install` then `npm run start` (Expo dev server), or platform targets via `npm run ios` / `npm run android` / `npm run web`. `npm run lint` uses Expo's ESLint preset. `npm run reset-project` wipes starter code—use cautiously.

Applied to files:

  • frontend/package.json
📚 Learning: 2025-12-08T15:54:59.341Z
Learnt from: CR
Repo: befoafsuger/rizap2025 PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-08T15:54:59.341Z
Learning: Applies to frontend/**/*.{ts,tsx} : Frontend Expo Router screens live under `frontend/app/` (e.g., `frontend/app/(tabs)/_layout.tsx`), shared UI in `frontend/components/` and `frontend/components/ui/`, assets in `frontend/assets/images/`, and utilities/hooks in `frontend/hooks/` and `frontend/constants/`

Applied to files:

  • frontend/app/battle-game.tsx
🧬 Code graph analysis (3)
frontend/utils/score.ts (1)
score/app.py (2)
  • api_score (169-206)
  • analyze_video (45-166)
frontend/app/battle-game.tsx (2)
frontend/utils/api.ts (1)
  • getEnemies (138-156)
frontend/utils/score.ts (1)
  • uploadVideoForScore (36-93)
frontend/app/solo.tsx (2)
frontend/utils/score.ts (2)
  • ScoreResponse (8-17)
  • uploadVideoForScore (36-93)
frontend/components/themed/Text.tsx (1)
  • Text (4-11)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Agent
🔇 Additional comments (6)
frontend/utils/api.ts (1)

110-135: LGTM!

The HP value adjustments (350, 400, 450) align with the new score-based damage calculation system. The increase in enemy HP balances the game mechanics with the camera-based scoring flow.

frontend/entities/game/mode.ts (1)

12-13: LGTM!

The AUTO mode follows the existing enum naming convention and provides a sensible default for automatic exercise detection in the camera-based flows.

frontend/app.json (1)

44-51: LGTM!

The Vision Camera plugin configuration is properly set up with localized Japanese permission texts. Note that enableMicrophonePermission is enabled here, while audio={false} is set in solo.tsx — this is fine as it provides flexibility for future audio features without requiring another permission prompt.

frontend/utils/score.ts (1)

63-67: Type assertion is acceptable here.

The as any cast for FormData file objects is a known workaround for React Native's FormData type definitions. This pattern is correct for file uploads.

frontend/app/solo.tsx (1)

110-162: Camera integration and UI flow look well-structured.

The permission handling, device selection, recording lifecycle with visual indicators, and mode selection are implemented correctly. The separation of recording/processing states provides clear UX feedback.

frontend/app/battle-game.tsx (1)

215-258: Camera integration and UI overlays look good.

The Camera setup with video={true} and audio={false} is appropriate for the use case. The mode selector, recording indicator, and processing overlay are well-implemented with proper conditional rendering.

Comment on lines +45 to +51
// 敵データを取得
const enemies = await getEnemies()
const enemy = enemies.find(e => e.id === enemyId)
if (enemy) {
setEnemyMaxHp(enemy.hp)
setEnemyHp(enemy.hp)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Type mismatch may cause enemy lookup to fail.

enemyId from useLocalSearchParams() is a string | string[], but e.id from the API is likely a number. Strict equality (===) between different types will always be false, causing the enemy to never be found and HP to remain at default.

       // 敵データを取得
       const enemies = await getEnemies()
-      const enemy = enemies.find(e => e.id === enemyId)
+      const enemy = enemies.find(e => String(e.id) === enemyId)
       if (enemy) {
         setEnemyMaxHp(enemy.hp)
         setEnemyHp(enemy.hp)
+      } else {
+        console.warn(`Enemy with id ${enemyId} not found`)
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 敵データを取得
const enemies = await getEnemies()
const enemy = enemies.find(e => e.id === enemyId)
if (enemy) {
setEnemyMaxHp(enemy.hp)
setEnemyHp(enemy.hp)
}
// 敵データを取得
const enemies = await getEnemies()
const enemy = enemies.find(e => String(e.id) === enemyId)
if (enemy) {
setEnemyMaxHp(enemy.hp)
setEnemyHp(enemy.hp)
} else {
console.warn(`Enemy with id ${enemyId} not found`)
}
🤖 Prompt for AI Agents
In frontend/app/battle-game.tsx around lines 45 to 51, the enemy lookup uses
strict equality between e.id (likely a number) and enemyId (string | string[]),
causing the find to fail; normalize enemyId first (handle string[] by taking the
first element), then either parse it to a number (const idNum = Number(idStr))
and use e.id === idNum, or compare strings consistently (String(e.id) ===
idStr); update the code to guard for invalid/undefined id before calling find
and handle parse failure (e.g., skip lookup or fallback) so the enemy is
reliably found and HP set.

Comment on lines +102 to +112
const newEnemyHp = Math.max(enemyHp - damage, 0)
setEnemyHp(newEnemyHp)

addLog(`こうげき!${damage}ダメージ!(スコア: ${scoreDamage})`)

if (newEnemyHp <= 0) {
setIsGameOver(true)
setWinner('player')
addLog('敵を倒した!')
return
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Stale closure: enemyHp captured at call time, not when callback executes.

The onRecordingFinished callback executes 3+ seconds after playerAttack is invoked. The enemyHp value used at line 102 is captured from the closure when the attack started, not the current state. If state changes during recording (e.g., from concurrent updates), the HP calculation will be incorrect.

Use functional state updater to access current state:

-            const newEnemyHp = Math.max(enemyHp - damage, 0)
-            setEnemyHp(newEnemyHp)
-
-            addLog(`こうげき!${damage}ダメージ!(スコア: ${scoreDamage})`)
-
-            if (newEnemyHp <= 0) {
-              setIsGameOver(true)
-              setWinner('player')
-              addLog('敵を倒した!')
-              return
-            }
-
-            setTimeout(() => enemyAttack(), 800)
+            setEnemyHp((currentHp) => {
+              const newEnemyHp = Math.max(currentHp - damage, 0)
+              addLog(`こうげき!${damage}ダメージ!(スコア: ${scoreDamage})`)
+
+              if (newEnemyHp <= 0) {
+                setIsGameOver(true)
+                setWinner('player')
+                addLog('敵を倒した!')
+              } else {
+                setTimeout(() => enemyAttack(), 800)
+              }
+              return newEnemyHp
+            })

Note: enemyAttack at line 142 has the same stale closure issue with userHp and should also use functional updaters.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const newEnemyHp = Math.max(enemyHp - damage, 0)
setEnemyHp(newEnemyHp)
addLog(`こうげき!${damage}ダメージ!(スコア: ${scoreDamage})`)
if (newEnemyHp <= 0) {
setIsGameOver(true)
setWinner('player')
addLog('敵を倒した!')
return
}
setEnemyHp((currentHp) => {
const newEnemyHp = Math.max(currentHp - damage, 0)
addLog(`こうげき!${damage}ダメージ!(スコア: ${scoreDamage})`)
if (newEnemyHp <= 0) {
setIsGameOver(true)
setWinner('player')
addLog('敵を倒した!')
} else {
setTimeout(() => enemyAttack(), 800)
}
return newEnemyHp
})
🤖 Prompt for AI Agents
In frontend/app/battle-game.tsx around lines 102-112 (and similarly for
enemyAttack near line 142), the code uses the stale closed-over enemyHp/userHp
when calculating damage; change to use functional state updaters: call
setEnemyHp(prev => { const newHp = Math.max(prev - damage, 0); if (newHp <= 0) {
setIsGameOver(true); setWinner('player'); addLog('敵を倒した!'); } return newHp; });
and for userHp call setUserHp(prev => { const newHp = Math.max(prev - damage,
0); if (newHp <= 0) { setIsGameOver(true); setWinner('enemy');
addLog('プレイヤーは倒された!'); } return newHp; }); and move related addLog/score logic
into those updater blocks or trigger them based on the returned newHp so updates
use the current state at execution time.

Comment on lines +178 to +188
// 権限チェックを行う
if (!hasPermission) {
return (
<View style={styles.container}>
<Text style={styles.infoLabel}>カメラ権限が必要です</Text>
<TouchableOpacity style={styles.commandButton} onPress={requestPermission}>
<Text style={styles.commandText}>権限を許可</Text>
</TouchableOpacity>
</View>
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Permission UI shown during initial loading state.

hasPermission from useCameraPermission is undefined while the permission status is being determined. The check !hasPermission treats undefined as falsy, showing the permission request UI prematurely even if permission was already granted.

+  // Show loading while permission status is being determined
+  if (hasPermission === undefined) {
+    return (
+      <View style={styles.container}>
+        <ActivityIndicator size="large" color="#fff" />
+      </View>
+    )
+  }
+
   // 権限チェックを行う
-  if (!hasPermission) {
+  if (hasPermission === false) {
     return (
       <View style={styles.container}>
         <Text style={styles.infoLabel}>カメラ権限が必要です</Text>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 権限チェックを行う
if (!hasPermission) {
return (
<View style={styles.container}>
<Text style={styles.infoLabel}>カメラ権限が必要です</Text>
<TouchableOpacity style={styles.commandButton} onPress={requestPermission}>
<Text style={styles.commandText}>権限を許可</Text>
</TouchableOpacity>
</View>
)
}
// Show loading while permission status is being determined
if (hasPermission === undefined) {
return (
<View style={styles.container}>
<ActivityIndicator size="large" color="#fff" />
</View>
)
}
// 権限チェックを行う
if (hasPermission === false) {
return (
<View style={styles.container}>
<Text style={styles.infoLabel}>カメラ権限が必要です</Text>
<TouchableOpacity style={styles.commandButton} onPress={requestPermission}>
<Text style={styles.commandText}>権限を許可</Text>
</TouchableOpacity>
</View>
)
}
🤖 Prompt for AI Agents
In frontend/app/battle-game.tsx around lines 178 to 188, the current check
treats undefined (loading) as falsy and shows the permission-request UI
prematurely; change the render logic to explicitly handle the three states: if
hasPermission === undefined show a loading placeholder (or null) while
permission is being resolved, if hasPermission === false render the
permission-request UI and call requestPermission on button press, and if
hasPermission === true proceed with the normal camera UI; ensure you only use
strict comparisons (===) to distinguish undefined vs false.

Comment on lines +9 to +18
interface ScoreResponse {
frames_processed: number
input_fps: number
duration_seconds: number
mode: string
max_score: number | null
avg_score: number | null
last_score: number | null
processing_time_seconds: number
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove duplicate ScoreResponse interface.

This interface is already defined and exported in frontend/utils/score.ts. Import it instead of duplicating.

 import { Mode } from '@/entities/game/mode'
-import { uploadVideoForScore } from '@/utils/score'
-
-interface ScoreResponse {
-  frames_processed: number
-  input_fps: number
-  duration_seconds: number
-  mode: string
-  max_score: number | null
-  avg_score: number | null
-  last_score: number | null
-  processing_time_seconds: number
-}
+import { uploadVideoForScore, ScoreResponse } from '@/utils/score'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface ScoreResponse {
frames_processed: number
input_fps: number
duration_seconds: number
mode: string
max_score: number | null
avg_score: number | null
last_score: number | null
processing_time_seconds: number
}
import { Mode } from '@/entities/game/mode'
import { uploadVideoForScore, ScoreResponse } from '@/utils/score'
🤖 Prompt for AI Agents
In frontend/app/solo.tsx around lines 9 to 18, there is a duplicate
ScoreResponse interface that already exists and is exported from
frontend/utils/score.ts; remove this local interface declaration and replace it
with an import: import { ScoreResponse } from 'frontend/utils/score' (adjust the
relative path if needed), then ensure all usages in this file reference the
imported type and run TypeScript to verify no missing or incorrect imports.

Comment on lines +77 to +82
// 10秒後に録画停止
setTimeout(async () => {
if (camera.current) {
await camera.current.stopRecording()
}
}, 10000)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clean up timeout on unmount to prevent memory leaks and stale calls.

The setTimeout is not cleaned up if the component unmounts during recording. This can cause errors when trying to call stopRecording on an unmounted camera ref.

+import { useState, useRef, useEffect } from 'react'
+
 export default function SoloScreen() {
   const router = useRouter()
+  const recordingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
 
+  useEffect(() => {
+    return () => {
+      if (recordingTimeoutRef.current) {
+        clearTimeout(recordingTimeoutRef.current)
+      }
+    }
+  }, [])
+
   // ... in handleStartRecording:
-      // 10秒後に録画停止
-      setTimeout(async () => {
+      recordingTimeoutRef.current = setTimeout(async () => {
         if (camera.current) {
           await camera.current.stopRecording()
         }
       }, 10000)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In frontend/app/solo.tsx around lines 77 to 82, the setTimeout that calls
camera.current.stopRecording is not cancelled on unmount; save the timeout ID
(e.g., to a ref or local variable) when you call setTimeout and add a cleanup to
the component's useEffect (or equivalent unmount handler) that calls
clearTimeout(timeoutId); also guard the timeout callback to check camera.current
before calling stopRecording (or skip calling if unmounted) so no stopRecording
is invoked after unmount.

"@rneui/themed": "^4.0.0-rc.8",
"@supabase/supabase-js": "^2.87.1",
"expo": "~54.0.27",
"expo-camera": "^17.0.10",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for expo-camera imports in TypeScript/TSX files (excluding lock files and package.json)
echo "=== Searching for expo-camera imports ==="
rg "from ['\"]expo-camera" frontend --type ts --type tsx
rg "import.*expo-camera" frontend --type ts --type tsx
echo ""
echo "=== Checking for any expo-camera references in code (non-lock files) ==="
rg "expo-camera" frontend --type ts --type tsx

Repository: befoafsuger/rizap2025

Length of output: 281


🏁 Script executed:

#!/bin/bash
# Also check if there are any references in JS files or other source files
echo "=== Checking all source files for expo-camera ==="
rg "expo-camera" frontend --glob "*.ts" --glob "*.tsx" --glob "*.js" --glob "*.jsx" -g "!node_modules" -g "!*.lock" -g "!package.json"

Repository: befoafsuger/rizap2025

Length of output: 114


Remove expo-camera as it is unused in the codebase.

The package is listed as a dependency but has no imports or references in any source files. Since react-native-vision-camera is already used for camera functionality (line 45), expo-camera is redundant and should be removed from package.json.

🤖 Prompt for AI Agents
In frontend/package.json around line 23, remove the unused "expo-camera":
"^17.0.10" dependency because the project uses react-native-vision-camera for
camera functionality; update package.json by deleting that dependency entry and
then run the package manager (npm/yarn/pnpm) to update lockfile and node_modules
(e.g., npm install or yarn install) to ensure the dependency is fully removed.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds camera functionality to the fitness tracking application, enabling video recording for solo training and battle modes. The implementation integrates a camera library to record user movements, send videos to a scoring API for analysis, and display results.

Key Changes:

  • Created a new scoring API utility (frontend/utils/score.ts) with mock data support for development
  • Integrated camera recording into solo training and battle game screens
  • Added login screen UI component with basic validation
  • Updated enemy HP values and added AUTO mode to the Mode enum

Reviewed changes

Copilot reviewed 7 out of 9 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
frontend/utils/score.ts New utility for uploading videos to scoring API with mock mode for development
frontend/utils/api.ts Increased enemy HP values (350, 400, 450) for better game balance
frontend/entities/game/mode.ts Added AUTO mode enum value for automatic exercise detection
frontend/app/solo.tsx Complete rewrite to integrate camera recording with 10-second video capture and score display
frontend/app/battle-game.tsx Integrated camera recording into battle attacks with 3-second video capture
frontend/app/login-screen.tsx New login UI component with email/password inputs using DotGothic16 font
frontend/package.json Added expo-camera (^17.0.10) and @expo/ngrok (^4.1.3) dependencies
frontend/pnpm-lock.yaml Updated lockfile with new dependencies; removed formik and hono packages
frontend/app.json Added react-native-vision-camera plugin configuration with camera permissions
Files not reviewed (1)
  • frontend/pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +44 to 52
[
"react-native-vision-camera",
{
"cameraPermissionText": "$(PRODUCT_NAME)はカメラへのアクセスが必要です",
"enableMicrophonePermission": true,
"microphonePermissionText": "$(PRODUCT_NAME)はマイクへのアクセスが必要です"
}
]
],
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The configuration adds 'react-native-vision-camera' plugin, but the package.json adds 'expo-camera' dependency. These are different camera libraries. The plugin configuration should match the actual camera library being used - either change the plugin to expo-camera's plugin or change the package dependency to react-native-vision-camera.

Suggested change
[
"react-native-vision-camera",
{
"cameraPermissionText": "$(PRODUCT_NAME)はカメラへのアクセスが必要です",
"enableMicrophonePermission": true,
"microphonePermissionText": "$(PRODUCT_NAME)はマイクへのアクセスが必要です"
}
]
],
]
],

Copilot uses AI. Check for mistakes.
import { getCurrentUser, createBattleLog } from '@/utils/api'
import { getCurrentUser, createBattleLog, getEnemies } from '@/utils/api'
import { Text } from '@/components/themed'
import { Camera, useCameraDevice, useCameraPermission } from 'react-native-vision-camera'
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code imports from 'react-native-vision-camera' but the package.json adds 'expo-camera'. These are different camera libraries with incompatible APIs. The imports should be changed to use expo-camera's Camera component and hooks instead of react-native-vision-camera.

Copilot uses AI. Check for mistakes.
Comment on lines +79 to +82
if (!response.ok) {
const errorText = await response.text()
console.error('API Error Response:', errorText)
throw new Error(`HTTP ${response.status}: ${errorText}`)
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error response is checked using response.ok, but when an error occurs, the error message construction uses HTTP status and error text without providing a user-friendly message. Consider wrapping this in a more descriptive error message or creating a custom error class for API failures.

Copilot uses AI. Check for mistakes.
Comment on lines +128 to +133
// 3秒後に録画停止
setTimeout(async () => {
if (camera.current) {
await camera.current.stopRecording()
}
}, 3000)
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The recording timeout is implemented using setTimeout with a hardcoded 3-second delay. If the component unmounts before the timeout completes, this could cause a memory leak or attempt to access a null camera ref. Consider storing the timeout ID and clearing it in a cleanup function.

Copilot uses AI. Check for mistakes.
Comment on lines +7 to 19
import { uploadVideoForScore } from '@/utils/score'

interface ScoreResponse {
frames_processed: number
input_fps: number
duration_seconds: number
mode: string
max_score: number | null
avg_score: number | null
last_score: number | null
processing_time_seconds: number
}

Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ScoreResponse interface is duplicated here when it's already defined and exported from @/utils/score.ts. This creates code duplication and potential inconsistency. Consider importing the interface from the score utility module instead of redefining it.

Suggested change
import { uploadVideoForScore } from '@/utils/score'
interface ScoreResponse {
frames_processed: number
input_fps: number
duration_seconds: number
mode: string
max_score: number | null
avg_score: number | null
last_score: number | null
processing_time_seconds: number
}
import { uploadVideoForScore, ScoreResponse } from '@/utils/score'

Copilot uses AI. Check for mistakes.
import { StyleSheet, View, TouchableOpacity, ActivityIndicator } from 'react-native'
import { useRouter } from 'expo-router'
import { Text } from '@/components/themed'
import { Camera, useCameraDevice, useCameraPermission } from 'react-native-vision-camera'
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code imports from 'react-native-vision-camera' but the package.json adds 'expo-camera'. These are different camera libraries with incompatible APIs. The imports should be changed to use expo-camera's Camera component and hooks instead of react-native-vision-camera.

Copilot uses AI. Check for mistakes.
const SCORE_API_URL = 'https://sponsored-media-compatibility-captured.trycloudflare.com/api/debug'

// ダミーモード(開発用)
const USE_MOCK = true // false にすると本物のAPIを使う
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The USE_MOCK flag is hardcoded to true, which means the real API will never be used even if the mock mode comment suggests changing it to false. This should be moved to environment configuration to enable toggling between mock and real API without code changes.

Suggested change
const USE_MOCK = true // false にすると本物のAPIを使う
// 環境変数 VITE_USE_MOCK を true にするとダミーモード、false で本物のAPIを使う
const USE_MOCK = String(import.meta.env.VITE_USE_MOCK).toLowerCase() === 'true';

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +82
setTimeout(async () => {
if (camera.current) {
await camera.current.stopRecording()
}
}, 10000)
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The recording timeout is implemented using setTimeout with a hardcoded 10-second delay. If the component unmounts before the timeout completes, this could cause a memory leak or attempt to access a null camera ref. Consider storing the timeout ID and clearing it in a cleanup function.

Copilot uses AI. Check for mistakes.
const [winner, setWinner] = useState<'player' | 'enemy' | null>(null)
const [startTime] = useState(Date.now())

// Vision Cameraにする
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says "Vision Cameraにする" (convert to Vision Camera) which suggests this is a placeholder or incomplete migration comment. This comment should be removed or updated to reflect the actual implementation state.

Suggested change
// Vision Cameraにする

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +39
export const uploadVideoForScore = async (
videoUri: string,
mode: Mode,
): Promise<string> => {
Copy link

Copilot AI Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The uploadVideoForScore function lacks documentation explaining its parameters, return value, and behavior. Consider adding a JSDoc comment explaining what the function does, the expected format of videoUri, valid Mode values, and what the returned string contains (JSON format).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/app/battle-game.tsx (1)

150-161: Stale closure in enemyAttack with userHp.

Similar to the player attack, userHp is captured at function creation time. Use functional updater:

 const enemyAttack = () => {
   const damage = Math.floor(Math.random() * 15) + 5
-  const newUserHp = Math.max(userHp - damage, 0)
-  setUserHp(newUserHp)
-  addLog(`敵の攻撃!${damage}ダメージ!`)
-
-  if (newUserHp <= 0) {
-    setIsGameOver(true)
-    setWinner('enemy')
-    addLog('やられた...')
-  }
+  setUserHp((currentHp) => {
+    const newUserHp = Math.max(currentHp - damage, 0)
+    addLog(`敵の攻撃!${damage}ダメージ!`)
+
+    if (newUserHp <= 0) {
+      setIsGameOver(true)
+      setWinner('enemy')
+      addLog('やられた...')
+    }
+    return newUserHp
+  })
 }
♻️ Duplicate comments (8)
frontend/app/battle-game.tsx (5)

37-37: Remove outdated TODO comment.

The comment "Vision Cameraにする" (convert to Vision Camera) is no longer applicable since Vision Camera is now implemented.

-  // Vision Cameraにする
+  // Vision Camera

54-60: Type mismatch may cause enemy lookup to fail.

enemyId from useLocalSearchParams() is string | string[], but e.id from the API is likely a number. Strict equality will fail.

       const enemies = await getEnemies()
-      const enemy = enemies.find((e) => e.id === enemyId)
+      const parsedId = Array.isArray(enemyId) ? enemyId[0] : enemyId
+      const enemy = enemies.find((e) => String(e.id) === parsedId)
       if (enemy) {
         setEnemyMaxHp(enemy.hp)
         setEnemyHp(enemy.hp)
+      } else {
+        console.warn(`Enemy with id ${enemyId} not found`)
       }

111-123: Stale closure: enemyHp captured at callback creation time.

The onRecordingFinished callback executes after recording completes, but captures enemyHp from when playerAttack was called. Use functional state updater:

-            const newEnemyHp = Math.max(enemyHp - damage, 0)
-            setEnemyHp(newEnemyHp)
-
-            addLog(`こうげき!${damage}ダメージ!(スコア: ${scoreDamage})`)
-
-            if (newEnemyHp <= 0) {
-              setIsGameOver(true)
-              setWinner('player')
-              addLog('敵を倒した!')
-              return
-            }
-
-            setTimeout(() => enemyAttack(), 800)
+            setEnemyHp((currentHp) => {
+              const newEnemyHp = Math.max(currentHp - damage, 0)
+              addLog(`こうげき!${damage}ダメージ!(スコア: ${scoreDamage})`)
+
+              if (newEnemyHp <= 0) {
+                setIsGameOver(true)
+                setWinner('player')
+                addLog('敵を倒した!')
+              } else {
+                setTimeout(() => enemyAttack(), 800)
+              }
+              return newEnemyHp
+            })

137-142: Clean up timeout on unmount.

Same issue as solo.tsx - the recording timeout should be stored in a ref and cleared on unmount to prevent memory leaks and stale calls.


186-199: Permission UI shown during initial loading state.

Same issue as solo.tsx - hasPermission is undefined initially while permission status is loading. Add explicit handling for the loading state.

+  if (hasPermission === undefined) {
+    return (
+      <View style={styles.container}>
+        <ActivityIndicator size="large" color="#fff" />
+      </View>
+    )
+  }
+
   // 権限チェックを行う
-  if (!hasPermission) {
+  if (hasPermission === false) {
frontend/app/solo.tsx (2)

18-27: Remove duplicate ScoreResponse interface.

This interface is already defined and exported in frontend/utils/score.ts. Import it instead of redefining.

 import { Mode } from '@/entities/game/mode'
-import { uploadVideoForScore } from '@/utils/score'
-
-interface ScoreResponse {
-  frames_processed: number
-  input_fps: number
-  duration_seconds: number
-  mode: string
-  max_score: number | null
-  avg_score: number | null
-  last_score: number | null
-  processing_time_seconds: number
-}
+import { uploadVideoForScore, ScoreResponse } from '@/utils/score'

86-91: Clean up timeout on unmount to prevent memory leaks.

The setTimeout is not cleared if the component unmounts during recording, which can cause errors when attempting to call stopRecording on an unmounted camera ref.

+import { useState, useRef, useEffect } from 'react'
+
 export default function SoloScreen() {
   const router = useRouter()
+  const recordingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
+
+  useEffect(() => {
+    return () => {
+      if (recordingTimeoutRef.current) {
+        clearTimeout(recordingTimeoutRef.current)
+      }
+    }
+  }, [])

Then update the setTimeout:

-      setTimeout(async () => {
+      recordingTimeoutRef.current = setTimeout(async () => {
         if (camera.current) {
           await camera.current.stopRecording()
         }
       }, 10000)
frontend/utils/score.ts (1)

3-7: Move configuration to environment variables.

The hardcoded trycloudflare.com URL and USE_MOCK = true flag should be environment-driven to avoid code changes for deployment and enable safe toggling between mock/real modes.

-const SCORE_API_URL =
-  'https://sponsored-media-compatibility-captured.trycloudflare.com/api/debug'
-
-// ダミーモード(開発用)
-const USE_MOCK = true // false にすると本物のAPIを使う
+import Constants from 'expo-constants'
+
+const SCORE_API_URL = Constants.expoConfig?.extra?.scoreApiUrl ?? ''
+const USE_MOCK = Constants.expoConfig?.extra?.useMockScore ?? true

Then configure in app.config.js or app.json under extra.

🧹 Nitpick comments (3)
frontend/utils/score.ts (2)

65-69: The as any cast is a known React Native FormData workaround.

React Native's FormData typing doesn't match the native implementation that accepts { uri, type, name } objects. This is acceptable but consider adding a brief comment explaining why.

   formData.append('file', {
     uri: videoUri,
     type: 'video/mp4',
     name: filename,
-  } as any)
+  } as any) // React Native FormData accepts { uri, type, name } objects

73-93: Consider adding a timeout to the fetch request.

The API call has no timeout, which could leave the UI in a processing state indefinitely if the server is unresponsive.

+  const controller = new AbortController()
+  const timeoutId = setTimeout(() => controller.abort(), 30000) // 30s timeout
+
   try {
     const response = await fetch(SCORE_API_URL, {
       method: 'POST',
       body: formData,
+      signal: controller.signal,
     })
+    clearTimeout(timeoutId)
frontend/app/battle-game.tsx (1)

19-22: Consider using ScoreResponse from @/utils/score instead of local ScoreSummary.

A ScoreResponse interface already exists in frontend/utils/score.ts with more fields. Using a partial type or Pick would maintain consistency:

-interface ScoreSummary {
-  max_score: number | null
-  avg_score: number | null
-}
+import { uploadVideoForScore, ScoreResponse } from '@/utils/score'
+
+type ScoreSummary = Pick<ScoreResponse, 'max_score' | 'avg_score'>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c7ef5c3 and 785f44c.

⛔ Files ignored due to path filters (1)
  • frontend/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • frontend/app/battle-game.tsx (10 hunks)
  • frontend/app/solo.tsx (2 hunks)
  • frontend/entities/game/mode.ts (1 hunks)
  • frontend/utils/api.ts (2 hunks)
  • frontend/utils/score.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/entities/game/mode.ts
  • frontend/utils/api.ts
🧰 Additional context used
📓 Path-based instructions (5)
frontend/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

frontend/**/*.{ts,tsx}: Frontend Expo Router screens live under frontend/app/ (e.g., frontend/app/(tabs)/_layout.tsx), shared UI in frontend/components/ and frontend/components/ui/, assets in frontend/assets/images/, and utilities/hooks in frontend/hooks/ and frontend/constants/
Expo SDK 54 is in use; match the documented Node/Java versions for native tooling

Files:

  • frontend/app/solo.tsx
  • frontend/utils/score.ts
  • frontend/app/battle-game.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript; prefer explicit return types on exported functions and components

Files:

  • frontend/app/solo.tsx
  • frontend/utils/score.ts
  • frontend/app/battle-game.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use 2-space indentation, single quotes, and trailing commas per ESLint/Prettier defaults

Files:

  • frontend/app/solo.tsx
  • frontend/utils/score.ts
  • frontend/app/battle-game.tsx
**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

React components: Use PascalCase filenames (e.g., HelloWave.tsx)

Files:

  • frontend/app/solo.tsx
  • frontend/app/battle-game.tsx
frontend/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Route files must follow Expo Router patterns (e.g., app/modal.tsx)

Files:

  • frontend/app/solo.tsx
  • frontend/app/battle-game.tsx
🧠 Learnings (2)
📚 Learning: 2025-12-08T15:54:59.341Z
Learnt from: CR
Repo: befoafsuger/rizap2025 PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-08T15:54:59.341Z
Learning: Applies to frontend/**/*.{ts,tsx} : Expo SDK 54 is in use; match the documented Node/Java versions for native tooling

Applied to files:

  • frontend/app/solo.tsx
  • frontend/app/battle-game.tsx
📚 Learning: 2025-12-08T15:54:59.341Z
Learnt from: CR
Repo: befoafsuger/rizap2025 PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-08T15:54:59.341Z
Learning: Applies to frontend/**/*.{ts,tsx} : Frontend Expo Router screens live under `frontend/app/` (e.g., `frontend/app/(tabs)/_layout.tsx`), shared UI in `frontend/components/` and `frontend/components/ui/`, assets in `frontend/assets/images/`, and utilities/hooks in `frontend/hooks/` and `frontend/constants/`

Applied to files:

  • frontend/app/battle-game.tsx
🧬 Code graph analysis (2)
frontend/app/solo.tsx (2)
frontend/utils/score.ts (2)
  • ScoreResponse (9-18)
  • uploadVideoForScore (37-94)
frontend/components/themed/Text.tsx (1)
  • Text (4-11)
frontend/app/battle-game.tsx (3)
frontend/utils/api.ts (1)
  • getEnemies (138-156)
frontend/utils/score.ts (1)
  • uploadVideoForScore (37-94)
frontend/components/themed/Text.tsx (1)
  • Text (4-11)
🔇 Additional comments (5)
frontend/app/solo.tsx (3)

51-96: Stale closure: mode captured at callback creation time.

The onRecordingFinished callback captures mode from the closure when recording starts. If the user could change mode during recording (currently disabled via UI), the uploaded mode would be stale. While the UI correctly disables mode changes during recording (Line 147), consider using a ref for mode to ensure robustness if that constraint is later removed.


130-137: Camera configuration looks correct.

The Vision Camera is properly configured with video={true}, audio={false}, and the front camera device. The ref setup enables programmatic recording control.


174-203: Score display handles null values appropriately.

Good use of optional chaining and nullish coalescing for max_score, avg_score, and last_score display.

frontend/utils/score.ts (1)

9-18: ScoreResponse interface is well-defined.

This is the canonical definition that should be imported by other files (solo.tsx, battle-game.tsx) instead of duplicating.

frontend/app/battle-game.tsx (1)

226-234: Camera configuration is consistent with solo.tsx.

The Vision Camera setup with video={true}, audio={false}, and front camera is correct and consistent with the solo screen implementation.

Comment on lines +98 to +108
// 権限チェック
if (!hasPermission) {
return (
<View style={styles.container}>
<Text style={styles.title}>カメラ権限が必要です</Text>
<TouchableOpacity style={styles.button} onPress={requestPermission}>
<Text style={styles.buttonText}>権限を許可</Text>
</TouchableOpacity>
</View>
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Permission UI shown during initial loading state.

hasPermission from useCameraPermission is undefined while permission status is being determined. The check !hasPermission treats undefined as falsy, showing the permission request UI prematurely.

+  if (hasPermission === undefined) {
+    return (
+      <View style={styles.container}>
+        <ActivityIndicator size="large" color="#fff" />
+      </View>
+    )
+  }
+
   // 権限チェック
-  if (!hasPermission) {
+  if (hasPermission === false) {
     return (

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In frontend/app/solo.tsx around lines 98 to 108, the current check uses
`!hasPermission` which treats `undefined` (loading) as false and shows the
permission request UI prematurely; update the logic to explicitly handle three
states: if hasPermission === undefined render a loading indicator (or null)
while permission is being determined, if hasPermission === false render the
permission request UI, and only proceed when hasPermission === true; adjust the
conditional(s) accordingly so undefined is not treated as denial.

@Rikublender3d Rikublender3d merged commit 87f5c47 into main Dec 13, 2025
1 check passed
@coderabbitai coderabbitai bot mentioned this pull request Dec 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants