Conversation
|
Note Other AI code review bot(s) detectedCodeRabbit 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. WalkthroughAdded 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 inenemyAttackforuserHp.This function is called via
setTimeoutfromonRecordingFinished, inheriting the same stale closure issue.userHpat 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/ngroktodevDependencies.
@expo/ngrokis a development tunneling tool and should not be bundled in production. Move it todevDependencies."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.logstatements.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 forprocessing_time_seconds.While
processing_time_secondsis 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 reusingScoreResponsefrom@/utils/score.Per the AI summary,
frontend/utils/score.tsexports aScoreResponseinterface. Defining a separateScoreSummaryhere 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
ScoreResponsein place ofScoreSummaryat line 88.
128-133: Consider cleanup for recording timeout on unmount.If the user flees (navigates away) during the 3-second recording window, this
setTimeoutcontinues and may attemptstopRecordingon 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
⛔ Files ignored due to path filters (1)
frontend/pnpm-lock.yamlis 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 underfrontend/app/(e.g.,frontend/app/(tabs)/_layout.tsx), shared UI infrontend/components/andfrontend/components/ui/, assets infrontend/assets/images/, and utilities/hooks infrontend/hooks/andfrontend/constants/
Expo SDK 54 is in use; match the documented Node/Java versions for native tooling
Files:
frontend/entities/game/mode.tsfrontend/utils/score.tsfrontend/app/battle-game.tsxfrontend/utils/api.tsfrontend/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.tsfrontend/utils/score.tsfrontend/app/battle-game.tsxfrontend/utils/api.tsfrontend/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.tsfrontend/utils/score.tsfrontend/app/battle-game.tsxfrontend/utils/api.tsfrontend/app/solo.tsx
**/*.tsx
📄 CodeRabbit inference engine (AGENTS.md)
React components: Use PascalCase filenames (e.g.,
HelloWave.tsx)
Files:
frontend/app/battle-game.tsxfrontend/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.tsxfrontend/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.jsonfrontend/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
AUTOmode 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
enableMicrophonePermissionis enabled here, whileaudio={false}is set insolo.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 anycast 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}andaudio={false}is appropriate for the use case. The mode selector, recording indicator, and processing overlay are well-implemented with proper conditional rendering.
| // 敵データを取得 | ||
| const enemies = await getEnemies() | ||
| const enemy = enemies.find(e => e.id === enemyId) | ||
| if (enemy) { | ||
| setEnemyMaxHp(enemy.hp) | ||
| setEnemyHp(enemy.hp) | ||
| } |
There was a problem hiding this comment.
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.
| // 敵データを取得 | |
| 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.
| const newEnemyHp = Math.max(enemyHp - damage, 0) | ||
| setEnemyHp(newEnemyHp) | ||
|
|
||
| addLog(`こうげき!${damage}ダメージ!(スコア: ${scoreDamage})`) | ||
|
|
||
| if (newEnemyHp <= 0) { | ||
| setIsGameOver(true) | ||
| setWinner('player') | ||
| addLog('敵を倒した!') | ||
| return | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| // 権限チェックを行う | ||
| 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> | ||
| ) | ||
| } |
There was a problem hiding this comment.
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.
| // 権限チェックを行う | |
| 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.
| 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 | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| // 10秒後に録画停止 | ||
| setTimeout(async () => { | ||
| if (camera.current) { | ||
| await camera.current.stopRecording() | ||
| } | ||
| }, 10000) |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
🧩 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 tsxRepository: 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.
There was a problem hiding this comment.
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.
| [ | ||
| "react-native-vision-camera", | ||
| { | ||
| "cameraPermissionText": "$(PRODUCT_NAME)はカメラへのアクセスが必要です", | ||
| "enableMicrophonePermission": true, | ||
| "microphonePermissionText": "$(PRODUCT_NAME)はマイクへのアクセスが必要です" | ||
| } | ||
| ] | ||
| ], |
There was a problem hiding this comment.
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.
| [ | |
| "react-native-vision-camera", | |
| { | |
| "cameraPermissionText": "$(PRODUCT_NAME)はカメラへのアクセスが必要です", | |
| "enableMicrophonePermission": true, | |
| "microphonePermissionText": "$(PRODUCT_NAME)はマイクへのアクセスが必要です" | |
| } | |
| ] | |
| ], | |
| ] | |
| ], |
frontend/app/battle-game.tsx
Outdated
| 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' |
There was a problem hiding this comment.
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.
| if (!response.ok) { | ||
| const errorText = await response.text() | ||
| console.error('API Error Response:', errorText) | ||
| throw new Error(`HTTP ${response.status}: ${errorText}`) |
There was a problem hiding this comment.
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.
| // 3秒後に録画停止 | ||
| setTimeout(async () => { | ||
| if (camera.current) { | ||
| await camera.current.stopRecording() | ||
| } | ||
| }, 3000) |
There was a problem hiding this comment.
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.
| 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 | ||
| } | ||
|
|
There was a problem hiding this comment.
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.
| 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' |
frontend/app/solo.tsx
Outdated
| 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' |
There was a problem hiding this comment.
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.
frontend/utils/score.ts
Outdated
| const SCORE_API_URL = 'https://sponsored-media-compatibility-captured.trycloudflare.com/api/debug' | ||
|
|
||
| // ダミーモード(開発用) | ||
| const USE_MOCK = true // false にすると本物のAPIを使う |
There was a problem hiding this comment.
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.
| const USE_MOCK = true // false にすると本物のAPIを使う | |
| // 環境変数 VITE_USE_MOCK を true にするとダミーモード、false で本物のAPIを使う | |
| const USE_MOCK = String(import.meta.env.VITE_USE_MOCK).toLowerCase() === 'true'; |
| setTimeout(async () => { | ||
| if (camera.current) { | ||
| await camera.current.stopRecording() | ||
| } | ||
| }, 10000) |
There was a problem hiding this comment.
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.
| const [winner, setWinner] = useState<'player' | 'enemy' | null>(null) | ||
| const [startTime] = useState(Date.now()) | ||
|
|
||
| // Vision Cameraにする |
There was a problem hiding this comment.
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.
| // Vision Cameraにする |
| export const uploadVideoForScore = async ( | ||
| videoUri: string, | ||
| mode: Mode, | ||
| ): Promise<string> => { |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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 inenemyAttackwithuserHp.Similar to the player attack,
userHpis 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.
enemyIdfromuseLocalSearchParams()isstring | string[], bute.idfrom 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:enemyHpcaptured at callback creation time.The
onRecordingFinishedcallback executes after recording completes, but capturesenemyHpfrom whenplayerAttackwas 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 -
hasPermissionisundefinedinitially 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 duplicateScoreResponseinterface.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
setTimeoutis not cleared if the component unmounts during recording, which can cause errors when attempting to callstopRecordingon 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.comURL andUSE_MOCK = trueflag 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 ?? trueThen configure in
app.config.jsorapp.jsonunderextra.
🧹 Nitpick comments (3)
frontend/utils/score.ts (2)
65-69: Theas anycast 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 usingScoreResponsefrom@/utils/scoreinstead of localScoreSummary.A
ScoreResponseinterface already exists infrontend/utils/score.tswith 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
⛔ Files ignored due to path filters (1)
frontend/pnpm-lock.yamlis 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 underfrontend/app/(e.g.,frontend/app/(tabs)/_layout.tsx), shared UI infrontend/components/andfrontend/components/ui/, assets infrontend/assets/images/, and utilities/hooks infrontend/hooks/andfrontend/constants/
Expo SDK 54 is in use; match the documented Node/Java versions for native tooling
Files:
frontend/app/solo.tsxfrontend/utils/score.tsfrontend/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.tsxfrontend/utils/score.tsfrontend/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.tsxfrontend/utils/score.tsfrontend/app/battle-game.tsx
**/*.tsx
📄 CodeRabbit inference engine (AGENTS.md)
React components: Use PascalCase filenames (e.g.,
HelloWave.tsx)
Files:
frontend/app/solo.tsxfrontend/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.tsxfrontend/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.tsxfrontend/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:modecaptured at callback creation time.The
onRecordingFinishedcallback capturesmodefrom 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 formodeto 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, andlast_scoredisplay.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.
| // 権限チェック | ||
| 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> | ||
| ) | ||
| } |
There was a problem hiding this comment.
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.
Summary by CodeRabbit
New Features
Other
✏️ Tip: You can customize this high-level summary in your review settings.