Skip to content

Conversation

@tegnike
Copy link
Owner

@tegnike tegnike commented Jan 10, 2026

Summary

  • 展示会やデジタルサイネージでの無人運用に便利な機能を追加
  • 人感検知(Presence Detection): Webカメラで来場者を自動検知し挨拶を開始
  • アイドルモード(Idle Mode): 会話がない時間が続くと自動発話
  • デモ端末モード(Kiosk Mode): 設定画面へのアクセス制限、NGワードフィルター

Test plan

  • ビルドが成功すること
  • 全てのテストがパスすること(649テスト成功)
  • 設定画面でPresence/Idle/Kioskの設定が表示されること
  • デモ端末モード有効時に設定画面へのアクセスが制限されること

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • プレゼンス検出(管理・指標・デバッグプレビュー)、アイドル自動発話インジケータ、キオスク(オーバーレイ・パスコード・ガイダンス)を追加。
  • Settings

    • 設定に「プレゼンス」「アイドル」「キオスク」タブを追加し、感度・タイムアウト・挨拶文・間隔・再生モード・感情・パスコード・最大入力長・NGワード等を編集可能に。
  • Translations

    • 日本語訳を大幅拡充・修正(メモリ表記・会話履歴文言変更ほか多数のキー追加/整理)。
  • Tests

    • コンポーネント、フック、統合の包括的な単体・統合テスト群を多数追加。

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

## 概要
展示会やデジタルサイネージでの無人運用に便利な機能を追加

## 主な機能
- **人感検知(Presence Detection)**: Webカメラで来場者を自動検知し挨拶を開始
- **アイドルモード(Idle Mode)**: 会話がない時間が続くと自動発話
- **デモ端末モード(Kiosk Mode)**: 設定画面へのアクセス制限、NGワードフィルター

## 変更ファイル
- 新規: src/features/kiosk/, src/features/idle/, src/features/presence/
- 新規: src/hooks/useKioskMode.ts, useIdleMode.ts, usePresenceDetection.ts
- 新規: src/components/presenceManager.tsx, idleManager.tsx
- 更新: settings.ts, home.ts, index.tsx, menu.tsx, messageInput.tsx

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Jan 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
aituber-kit Error Error Jan 14, 2026 8:39am

@coderabbitai
Copy link

coderabbitai bot commented Jan 10, 2026

Walkthrough

プレゼンス検出(カメラ + face-api.js)、アイドル自動発話、キオスクモード(パスコード・フルスクリーン・NGワード)を新規導入し、それらを操作するフック、コンポーネント、ストア拡張、多数の単体/統合テスト、および日本語翻訳キーを追加・更新しました。

Changes

コホート / ファイル(s) 変更内容
翻訳ファイル
locales/ja/translation.json
Presence/Idle/Kiosk 関連日本語キー大量追加、Memory 表記変更・旧キー削除、ConversationHistoryInfo 表現更新(短期記憶表現など)。
Presence 型 / フック / コンポーネント
src/features/presence/presenceTypes.ts, src/hooks/usePresenceDetection.ts, src/components/presenceIndicator.tsx, src/components/presenceDebugPreview.tsx, src/components/presenceManager.tsx, src/__tests__/.../presence*.test.ts*, src/__tests__/integration/presenceDetectionIntegration.test.tsx
型定義・エラー型追加、face-api.js を使ったカメラ検出ループと FSM、start/stop/completeGreeting API、表示コンポーネントとデバッグプレビュー、関連テスト群を追加。
Idle 型 / フック / コンポーネント / 設定
src/features/idle/idleTypes.ts, src/hooks/useIdleMode.ts, src/components/idleManager.tsx, src/components/settings/idleSettings.tsx, src/__tests__/.../idle*.test.ts*, src/features/stores/settings.ts
Idle の型・デフォルト・ユーティリティ、useIdleMode(タイマー・発話選択・時間帯対応)、IdleManager UI、Idle 設定画面、設定ストア統合、関連テストを追加。
Kiosk 型 / フック / 機能群
src/features/kiosk/kioskTypes.ts, src/hooks/useKioskMode.ts, src/features/kiosk/passcodeDialog.tsx, src/features/kiosk/kioskOverlay.tsx, src/features/kiosk/guidanceMessage.tsx, src/components/settings/kioskSettings.tsx, src/components/messageInput.tsx, src/components/menu.tsx, src/__tests__/.../kiosk*.test.ts*, src/__tests__/integration/kioskModeIntegration.test.ts
Kiosk 設定とデフォルト、バリデータ、パスコードダイアログ(試行制限/ロックアウト)、オーバーレイ(フルスクリーン誘導・Esc長押し起動)、ガイダンス、MessageInput での入力バリデーション導入、メニューの設定アクセス制御、関連テストを追加。
共通フック / ユーティリティ
src/hooks/useFullscreen.ts, src/hooks/useEscLongPress.ts, src/hooks/useDemoMode.ts, src/utils/demoMode.ts, src/__tests__/hooks/*.test.ts
フルスクリーン管理フック、Esc長押し検出フック、デモモード判定フック/ユーティリティを追加。
ストア拡張 / メニュー統合
src/features/stores/settings.ts, src/features/stores/home.ts, src/features/stores/menu.ts, src/components/settings/index.tsx
SettingsStore に Presence/Idle/Kiosk 設定を追加、HomeStore に presenceState/presenceError/lastDetectionTime を追加、メニューの設定タブキー拡張、Settings UI に新タブを統合。
ページ統合 / その他 UI
src/pages/index.tsx, src/components/*, src/components/settings/*
Home に PresenceManager / IdleManager / KioskOverlay を組み込み、Settings 画面に Presence/Idle/Kiosk タブを追加。
テスト群
src/__tests__/**
presence/idle/kiosk フロー向けの大量の単体・統合テスト追加(face-api.js・メディアデバイス・フックのモック含む)。
ビルド/依存
next.config.js, package.json
クライアントビルドでの fs フォールバック無効化(webpack 設定追加)、依存に face-api.js@^0.22.2 を追加。

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant KioskOverlay
    participant PasscodeDialog
    participant SettingsStore
    participant MessageInput

    User->>KioskOverlay: Esc 長押し
    KioskOverlay->>PasscodeDialog: ダイアログを開く
    User->>PasscodeDialog: パスコード入力
    PasscodeDialog->>PasscodeDialog: 検証
    alt 正しい
        PasscodeDialog->>SettingsStore: kioskTemporaryUnlock = true
        PasscodeDialog->>User: ロック解除
        User->>MessageInput: 設定へアクセス
    else 間違い
        PasscodeDialog->>PasscodeDialog: 試行回数増加 / ロックアウト
        PasscodeDialog->>User: エラー表示
    end
Loading
sequenceDiagram
    participant PresenceManager
    participant usePresenceDetection
    participant Camera
    participant DetectionLoop
    participant HomeStore

    PresenceManager->>usePresenceDetection: startDetection()
    usePresenceDetection->>Camera: getUserMedia()
    Camera-->>usePresenceDetection: video stream
    usePresenceDetection->>usePresenceDetection: load TinyFaceDetector
    DetectionLoop->>DetectionLoop: setInterval (感度依存)
    DetectionLoop->>usePresenceDetection: detectSingleFace()
    alt 顔検出
        usePresenceDetection->>HomeStore: presenceState = detected -> greeting -> conversation-ready
        usePresenceDetection->>PresenceManager: onPersonDetected/onGreetingStart
    else 顔なし
        usePresenceDetection->>HomeStore: presenceState = idle
    end
Loading
sequenceDiagram
    participant useIdleMode
    participant IdleTimer
    participant PhraseSelector
    participant SpeakCharacter
    participant HomeStore

    useIdleMode->>IdleTimer: 初期化 (idleInterval)
    IdleTimer->>useIdleMode: 1秒毎にtick
    alt カウントダウン到達
        useIdleMode->>useIdleMode: canSpeak() チェック
        alt 発話可能
            useIdleMode->>PhraseSelector: フレーズ選択 (順次/ランダム/時間帯)
            PhraseSelector-->>useIdleMode: phrase
            useIdleMode->>HomeStore: idleState = speaking
            useIdleMode->>SpeakCharacter: speak(phrase)
            SpeakCharacter-->>useIdleMode: onComplete
            useIdleMode->>HomeStore: idleState = waiting
        else
            useIdleMode->>useIdleMode: 発話スキップ(条件不一致)
        end
    else
        IdleTimer->>useIdleMode: 秒数デクリメント
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PRタイトル「feat: Kiosk/Idle/Presence機能を追加(Split-4)」は、変更セットの主要な目的を明確に要約している。3つの重要な新機能(Kiosk/Idle/Presence)の追加を簡潔かつ具体的に説明している。
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/realtime-api-split-4-kiosk-presence


📜 Recent review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 31fec53 and 7c922e8.

📒 Files selected for processing (1)
  • locales/ja/translation.json
🧰 Additional context used
📓 Path-based instructions (1)
locales/ja/**/*.json

📄 CodeRabbit inference engine (CLAUDE.md)

言語ファイルの更新は日本語(/locales/ja/)のみ行う

Files:

  • locales/ja/translation.json
⏰ 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: claude-review
🔇 Additional comments (5)
locales/ja/translation.json (5)

129-129: LGTM!

「短期的な記憶」という表現は、新しいメモリ機能との用語の一貫性を保っており、適切な翻訳です。


443-464: LGTM!

メモリ設定関連の翻訳は適切です。テンプレート構文({{count}})も正しく使用されており、既存の翻訳パターンと一貫性があります。


465-488: LGTM!

人感検知設定の翻訳は適切です。検出感度オプション(低/中/高)には間隔情報が含まれており、ユーザーにとって分かりやすい表現になっています。


489-528: LGTM!

アイドルモード設定の翻訳は適切です。感情ラベル(Emotion_*)はLive2D設定セクション(Lines 329-334)の既存の感情名と一貫性があり、ユーザー体験の統一性が保たれています。


529-555: LGTM!

キオスクモード設定の翻訳は適切です。「デモ端末モード」という翻訳は展示会やデジタルサイネージの文脈で分かりやすく、パスコード関連のUIテキスト(残り試行回数、ロック状態)もユーザーフレンドリーな表現になっています。

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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

@claude
Copy link
Contributor

claude bot commented Jan 10, 2026

Claude finished @tegnike's task —— View job


PR Review: Kiosk/Idle/Presence Features

This is an impressive and comprehensive implementation of three major features for unattended operation. The codebase shows excellent engineering practices with comprehensive test coverage (59 test files) and well-structured code.

Review Checklist:

  • Review core feature implementations (hooks and components)
  • Analyze security implications of kiosk mode
  • Evaluate presence detection implementation
  • Check test coverage and quality
  • Review performance considerations
  • Examine state management changes
  • Provide comprehensive feedback

Strengths

1. Code Quality & Architecture

  • Excellent separation of concerns: Each feature has its own types, hooks, components, and tests
  • Consistent patterns: All three features follow the same architectural patterns (types → hooks → components → tests)
  • TypeScript usage: Strong type safety throughout with proper interfaces and type guards
  • React best practices: Proper use of useCallback, useMemo, refs, and effect cleanup

2. Test Coverage

  • Comprehensive testing: 59 test files with unit, integration, and component tests
  • 100% feature coverage: All new hooks, components, and utilities have corresponding tests
  • Realistic test scenarios: Tests cover edge cases, error conditions, and integration scenarios
  • Proper mocking: Well-structured mocks for external dependencies (face-api.js, MediaDevices)

3. Performance Optimizations

  • Efficient presence detection: Configurable detection intervals (150ms-500ms) based on sensitivity
  • Proper cleanup: All timers, intervals, and media streams are properly cleaned up
  • Optimized re-renders: Strategic use of useCallback and useMemo to prevent unnecessary renders
  • Lazy loading: Components are conditionally rendered based on feature enablement

4. User Experience

  • Intuitive kiosk mode: ESC long-press (2s) for admin access is discoverable yet secure
  • Progressive security: Rate limiting (3 attempts, 30s lockout) and input validation
  • Comprehensive presence detection: Multiple states (idle → detected → greeting → conversation-ready)
  • Smart idle management: Considers AI processing, speaking state, and presence detection

⚠️ Areas for Improvement

1. Security Considerations

Passcode Security (src/features/kiosk/passcodeDialog.tsx:97):

  • Good: Rate limiting, lockout mechanism, input masking
  • ⚠️ Concern: Plain text passcode comparison in client-side code
  • 📋 Recommendation: Consider hashing passcodes client-side (though this has limited security benefit for client-side apps)

Input Validation (src/components/messageInput.tsx:325-332):

  • Good: Proper validation with user feedback
  • ⚠️ Concern: NG words are processed case-insensitively but stored in plain text
  • 📋 Recommendation: Consider more sophisticated content filtering if needed

2. Performance Considerations

Face Detection Loop (src/hooks/usePresenceDetection.ts:342-355):

  • Good: Configurable intervals, proper cleanup
  • ⚠️ Concern: Continuous camera processing could impact battery life
  • 📋 Recommendation: Consider implementing smart detection pausing or reduced frame rate during inactivity

Memory Management (src/hooks/usePresenceDetection.ts:195-206):

  • Good: Stream cleanup is implemented
  • 📋 Suggestion: Monitor for potential memory leaks with face-api.js model loading

3. Accessibility & UX

ESC Key Handling (src/hooks/useEscLongPress.ts:42-77):

  • Good: Prevents accidental activation with 2s duration
  • ⚠️ Concern: No visual feedback during long-press
  • 📋 Recommendation: Consider adding visual progress indicator

Camera Permissions (src/hooks/usePresenceDetection.ts:146-193):

  • Good: Comprehensive error handling for different permission states
  • 📋 Suggestion: Consider graceful degradation when camera is not available

4. Code Structure

Large Hook Files:

  • usePresenceDetection.ts (406 lines) and useIdleMode.ts (323 lines) are quite large
  • 📋 Recommendation: Consider extracting some logic into separate utility functions

Type Definitions (src/features/presence/presenceTypes.ts):

  • Good: Proper type guards and constants
  • 📋 Minor: Could benefit from JSDoc comments for complex types

🔧 Minor Issues

  1. Translation Keys: Some hardcoded Japanese text in passcodeDialog.tsx:163,170 should use translation keys
  2. Console Logging: Debug logs in presence detection should be removable in production builds
  3. Magic Numbers: Some timeouts (500ms in passcodeDialog.tsx:70) could be constants

🚀 Exceptional Features

1. Integration Design

The three features work together seamlessly:

  • Presence detection triggers idle mode reset
  • Kiosk mode applies input validation without breaking other features
  • All features respect AI processing states

2. Developer Experience

  • Comprehensive TypeScript types with proper exports
  • Clear component interfaces and hook returns
  • Excellent test coverage with realistic scenarios
  • Well-documented requirements tracing in tests

3. Production Ready

  • Environment variable support for configuration
  • Proper error boundaries and fallback states
  • Performance monitoring capabilities (debug modes)
  • Graceful feature degradation

🎯 Overall Assessment

This is exceptional work that demonstrates:

  • Professional-grade architecture and code organization
  • Comprehensive testing and quality assurance
  • Strong security awareness with appropriate mitigations
  • Performance-conscious implementation with optimization opportunities identified
  • Production-ready features suitable for deployment

The codebase quality is very high, and the features are well-implemented for their intended use cases (digital signage, exhibitions, unattended operation).

Recommendation: ✅ Approve with minor suggestions

The identified areas for improvement are mostly enhancements rather than blockers, and the overall implementation quality justifies merging this PR.


Copy link

@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: 17

Caution

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

⚠️ Outside diff range comments (2)
src/pages/index.tsx (1)

39-60: 配列の不安定性により useEffect が不要に再実行されます

characterPresets 配列がコンポーネント本体で定義されているため、レンダリングごとに新しい参照が作成されます。これにより、行62の useEffect が依存配列の変更を検出し、不要に再実行されます。

♻️ 推奨される修正方法

方法1: useMemoでラップ

+  const characterPresets = useMemo(() => [
-  const characterPresets = [
     {
       key: 'characterPreset1',
       value: settingsStore((s) => s.characterPreset1),
     },
     // ... 他のプリセット
-  ]
+  ], []) // または適切な依存配列

方法2: useEffect内に移動

   useEffect(() => {
+    const characterPresets = [
+      {
+        key: 'characterPreset1',
+        value: settingsStore.getState().characterPreset1,
+      },
+      // ... 他のプリセット
+    ]
+
     const handleKeyDown = (event: KeyboardEvent) => {
       // ...
     }
     // ...
-  }, [characterPresets, t])
+  }, [t])

パイプライン警告に基づく指摘

src/components/settings/index.tsx (1)

52-85: activeSettingsTab が未知値だった場合のフォールバックが欲しい(永続化/互換性)
tabIconMapping[activeTab]undefined になると maskImage: url(undefined) になり得ます。activeTabtabs に存在するキーへ正規化(なければ 'description')するか、renderTabContentdefault を追加すると安全です。

提案パッチ(fallback)
   const activeTab = menuStore((state) => state.activeSettingsTab)
+  const safeActiveTab: TabKey =
+    (tabs.find((t) => t.key === activeTab)?.key as TabKey) ?? 'description'
@@
-  const currentTab = tabs.find((tab) => tab.key === activeTab)
+  const currentTab = tabs.find((tab) => tab.key === safeActiveTab)
-  const renderTabContent = () => {
-    switch (activeTab) {
+  const renderTabContent = () => {
+    switch (safeActiveTab) {
@@
       case 'speechInput':
         return <SpeechInput />
+      default:
+        return <Description />
     }
   }

Also applies to: 177-208, 210-262

🤖 Fix all issues with AI agents
In @src/__tests__/components/settings/kioskSettings.test.tsx:
- Around line 17-32: The mocked zustand store's setState only accepts a partial
object, but real setState can accept a function updater; update the mock inside
the jest.mock for '@/features/stores/settings' so setState checks if the
received arg is a function and, if so, calls it with the current state (from the
mock's getState), merges the result into the internal mock state, and returns
accordingly; reference the mocked setState and getState and the mockSetState
helper so the mock both updates its internal state variable and records the
received argument(s) for assertions.

In @src/__tests__/hooks/useDemoMode.test.ts:
- Around line 40-47: The test for memoization in the useDemoMode hook is using
toEqual (value equality) instead of toBe (reference identity); update the test
in useDemoMode.test.ts so the assertion after rerender uses
expect(result.current).toBe(firstResult) to verify the same object reference is
returned, keeping the rest of the test (setting
process.env.NEXT_PUBLIC_DEMO_MODE, renderHook(() => useDemoMode()), capturing
firstResult, and calling rerender()) unchanged.

In @src/__tests__/hooks/useFullscreen.test.ts:
- Around line 14-20: The test suite leaks the previous test's captured listener
because fullscreenChangeHandler is not reset in beforeEach; update the
beforeEach to reinitialize fullscreenChangeHandler to null (in the same spot
where mockFullscreenElement is reset) so each test starts with a clean state;
ensure the same reset is applied for the second block mentioned (lines around
41-51) where fullscreenChangeHandler is used to avoid cross-test listener
retention.

In @src/__tests__/hooks/useKioskMode.test.ts:
- Around line 15-24: テストが DEFAULT_KIOSK_CONFIG.kioskGuidanceMessage と
DEFAULT_KIOSK_CONFIG.kioskGuidanceTimeout を参照していますが、型と定義側にそれらが無いため undefined
になります。対応としては、kioskTypes.ts の KioskModeSettings 型に kioskGuidanceMessage と
kioskGuidanceTimeout を追加し、DEFAULT_KIOSK_CONFIG 定義にも適切なデフォルト値を設定するか、テストの
settingsStore.setState 呼び出しから kioskGuidanceMessage と kioskGuidanceTimeout
の参照を削除して期待値を明確にしてください。変更箇所の目印は DEFAULT_KIOSK_CONFIG, KioskModeSettings, and the
test file useKioskMode.test.ts の settingsStore.setState ブロックです。

In @src/__tests__/utils/demoMode.test.ts:
- Around line 8-17: テスト内で process.env を beforeEach で上書きしているが復元が afterAll
のみだと他テストに影響する可能性があるため、beforeEach/beforeAll の代わりに各テスト後に必ず元に戻すよう変更してください;具体的には
demoMode.test.ts の beforeEach が使っている originalEnv と process.env
の丸ごと差し替えをやめ、テストで変更する特定の環境変数キーだけを退避(例: const savedEnv = { KEY_A:
process.env.KEY_A, ... } in beforeEach)し、afterEach 内でその savedEnv
を使って個別キーを復元し、不要なキーは delete することで環境を元に戻す(afterAll は残さず afterEach を追加して
process.env の全体差し替えを避ける)。

In @src/components/idleManager.tsx:
- Around line 14-24: The current use of console.log in the useIdleMode callbacks
(onIdleSpeechStart, onIdleSpeechComplete, onIdleSpeechInterrupted) should be
guarded so it doesn't spam production logs; replace those direct console.log
calls with a conditional check using NODE_ENV !== 'production' or an existing
debug option/flag (or a centralized logger with a debug level) and only emit the
messages when debugging is enabled, keeping the same message content and
callback hooks intact.

In @src/components/menu.tsx:
- Around line 237-245: The canAccessSettings check around the Settings
IconButton is redundant because effectiveShowControlPanel implies (!isKioskMode
|| isTemporaryUnlocked) which matches canAccessSettings; remove the outer
"canAccessSettings &&" guard so the IconButton block (the div with IconButton,
onClick calling setShowSettings(true)) is rendered solely under
effectiveShowControlPanel, but keep the defensive "showSettings &&
canAccessSettings &&" guard that renders the Settings component elsewhere to
prevent rendering if access is revoked after showSettings was set.

In @src/components/presenceDebugPreview.tsx:
- Around line 49-86: getBoxStyle currently reads videoRef.current during render
(via videoRef and detectionResult) which violates React rules; instead create a
state like boundingBoxStyle and compute/update it inside a useEffect that
depends on detectionResult, videoRef.current (or a ref-ready flag) and scale,
using detectionResult.boundingBox to compute mirroredX and width/height there,
then apply that state in the JSX (style={boundingBoxStyle}) and keep getBoxStyle
either removed or returning an empty object to avoid synchronous ref access
during render; reference symbols: getBoxStyle, videoRef, detectionResult,
presenceDebugMode, shouldShowBoundingBox, scale, and the bounding-box div's
style prop.

In @src/components/settings/idleSettings.tsx:
- Around line 128-143: handleMovePhrase mutates phrase objects in-place by
assigning p.order = i; instead create new phrase objects to preserve
immutability: after swapping build newPhrases = newPhrases.map((p, i) => ({
...p, order: i })) (or otherwise clone each item) and then call
settingsStore.setState({ idlePhrases: newPhrases }); this ensures no direct
mutation of existing objects and triggers predictable renders.

In @src/features/idle/idleTypes.ts:
- Around line 43-50: The fallback ID generator uses the deprecated
String.prototype.substr; update the id generation expression that contains
Math.random().toString(36).substr(2, 9) to use a non-deprecated method (e.g.,
substring or slice) — replace substr(2, 9) with either substring(2, 11) or
slice(2, 11) so the produced length remains the same, keeping the surrounding
template literal and the crypto.randomUUID branch intact.

In @src/features/kiosk/kioskOverlay.tsx:
- Around line 24-34: The current unlock flow relies only on useEscLongPress and
keyboard Enter, which fails on touch-only devices; add an on-screen touch
hotspot (e.g., a small transparent component in a corner) that listens for
longPress/tap and calls setShowPasscodeDialog when isKioskMode &&
!isTemporaryUnlocked (same guard as the useEscLongPress), and update the
PasscodeDialog component to provide a touch-friendly submit mechanism (on-screen
keypad/buttons and a visible submit button) so it does not require a physical
keyboard; reference useEscLongPress, setShowPasscodeDialog, isKioskMode,
isTemporaryUnlocked, kioskPasscode and PasscodeDialog when implementing these
changes.

In @src/features/kiosk/kioskTypes.ts:
- Around line 9-21: Add the missing kioskGuidanceMessage (string) and
kioskGuidanceTimeout (number) properties to the KioskModeSettings interface and
ensure the DEFAULT_KIOSK_CONFIG constant includes those keys with sensible
defaults; update the declarations around KioskModeSettings and
DEFAULT_KIOSK_CONFIG so code referencing kioskGuidanceMessage and
kioskGuidanceTimeout (e.g., tests in kioskModeIntegration.test.ts) compiles and
reads the expected defaults.

In @src/features/kiosk/passcodeDialog.tsx:
- Around line 167-172: The UI displays remaining attempts twice because both
t('Kiosk.PasscodeRemainingAttempts', { count: remainingAttempts }) and the
hardcoded string 残り{remainingAttempts}回 are rendered; remove the duplicate by
rendering only the translation or, if you need a fallback when the translation
is missing, replace the hardcoded string with a conditional that checks the
translator result (e.g., use i18n.exists('Kiosk.PasscodeRemainingAttempts') or
compare t(...) to the key) and render the fallback only when the translation is
absent; update the JSX around isLocked, attempts, remainingAttempts and the t
call accordingly.

In @src/hooks/useEscLongPress.ts:
- Around line 39-77: When `enabled` flips to false the hook can leave
isKeyDownRef.current true and isHolding true if Escape was held; update the
effect so that when enabled is false (and in the cleanup) you explicitly reset
isKeyDownRef.current = false, call setIsHolding(false), and clearTimer(); ensure
the returned cleanup also does these resets in addition to removing the event
listeners so re-enabling the hook can respond to subsequent keydown events.

In @src/hooks/useIdleMode.ts:
- Around line 279-288: The updater currently calls triggerSpeech inside the
state updater (setSecondsUntilNextSpeech) which causes a side effect inside a
pure update function; change the setInterval callback so it only updates state
and does not call triggerSpeech from within the updater: inside the interval,
declare a local flag (e.g. shouldTrigger = false), call
setSecondsUntilNextSpeech(prev => { if (prev <= 1) { shouldTrigger = true;
return idleInterval } return prev - 1 }), then after that call, if shouldTrigger
is true call triggerSpeech(); this keeps triggerSpeech out of the state updater
while preserving the same behavior for timerRef/current,
setSecondsUntilNextSpeech, triggerSpeech and idleInterval.

In @src/hooks/useKioskMode.ts:
- Around line 34-116: kioskMaxInputLength can be 0/NaN/undefined and currently
will make every non-empty input invalid; compute an effective positive finite
limit (e.g. const effectiveMaxInputLength = Number.isFinite(kioskMaxInputLength)
&& kioskMaxInputLength > 0 ? kioskMaxInputLength : undefined), use
effectiveMaxInputLength instead of kioskMaxInputLength for maxInputLength and in
validateInput's length check/message, and update the useCallback/useMemo
dependency arrays to reference effectiveMaxInputLength rather than raw
kioskMaxInputLength so behavior remains robust when the store contains invalid
values.
🧹 Nitpick comments (32)
src/hooks/useDemoMode.ts (1)

10-12: useMemoは不要です(静的な環境変数)

process.env.NEXT_PUBLIC_DEMO_MODEはNext.jsのビルド時に文字列リテラルに置き換えられる静的な値です。実行時に変化しないため、useMemoでメモ化する必要はありません。

♻️ 提案されるリファクタリング
 export function useDemoMode(): {
   isDemoMode: boolean
 } {
-  const isDemoMode = useMemo(() => {
-    return process.env.NEXT_PUBLIC_DEMO_MODE === 'true'
-  }, [])
+  const isDemoMode = process.env.NEXT_PUBLIC_DEMO_MODE === 'true'

   return { isDemoMode }
 }
src/__tests__/hooks/useDemoMode.test.ts (1)

12-14: 環境変数のクリーンアップタイミングを修正してください。

afterAll を使用すると、すべてのテストが終了した後に一度だけ実行されます。これにより、他のテストスイートに影響を与える可能性があります。各テストケース後に環境変数をリセットするため、afterEach を使用することを推奨します。

♻️ 推奨される修正
-  afterAll(() => {
+  afterEach(() => {
     process.env = originalEnv
   })
src/__tests__/components/settings/kioskSettings.test.tsx (2)

17-32: ボタン取得が順序依存でテストが壊れやすいです(StatusOff配列の添字)
getAllByText('StatusOff')[0] や「最後のボタンがNGワード」前提は、UIの並び替えだけで壊れます。ラベル(設定項目名)配下にスコープしてボタン取得するか、data-testid を付けて特定してください。

修正案(例:closestでセクションにスコープしてクリック対象を特定)
-      const toggleButtons = screen.getAllByText('StatusOff')
-      fireEvent.click(toggleButtons[0])
+      const section = screen.getByText('KioskModeEnabled').closest('div')
+      expect(section).toBeTruthy()
+      fireEvent.click(screen.getByText('StatusOff', { selector: 'button' }))

Also applies to: 55-91


20-31: settingsStore.setState の呼び出し内容まで検証した方が安心です
現状「呼ばれたこと」中心で、トグル/NGワード等の更新値の回帰を取り逃がしやすいです(特にNGワードは parse 結果)。

修正案(例:トグルとNGワード配列の期待値を厳密化)
 it('should toggle kiosk mode when button is clicked', () => {
   render(<KioskSettings />)
   const toggleButtons = screen.getAllByText('StatusOff')
   fireEvent.click(toggleButtons[0])
-  expect(mockSetState).toHaveBeenCalled()
+  expect(mockSetState).toHaveBeenCalledWith({ kioskModeEnabled: true })
 })

 it('should call setState when NG words input is blurred', () => {
   ...
   fireEvent.change(input, { target: { value: 'bad, word, test' } })
   fireEvent.blur(input)
-  // Check that setState was called with kioskNgWords
-  const calls = mockSetState.mock.calls
-  const ngWordsCall = calls.find(
-    (call: any[]) => call[0] && 'kioskNgWords' in call[0]
-  )
-  expect(ngWordsCall).toBeDefined()
+  expect(mockSetState).toHaveBeenCalledWith({
+    kioskNgWords: ['bad', 'word', 'test'],
+  })
 })

Also applies to: 85-90, 105-110, 184-202

src/components/presenceIndicator.tsx (2)

20-36: Record<PresenceState, ...> で網羅できているので ?? STATE_CONFIG.idle は実質不要です
将来 PresenceState が広がった時の保険として残すのはアリですが、今の型付けだと到達不能なので意図をコメントしておくと読み手が迷いません。


53-61: インジケータのa11y方針(装飾か情報か)を明確にしたいです
今はテキストがあるのでドットは装飾扱いで aria-hidden="true" にする、またはドット自体に aria-label を付ける等、どちらかに寄せると良いです。

修正案(ドットを装飾扱いにする例)
       <div
         data-testid="presence-indicator-dot"
+        aria-hidden="true"
         className={`w-3 h-3 rounded-full ${color} ${shouldPulse ? 'animate-pulse' : ''}`}
       />
src/__tests__/hooks/useFullscreen.test.ts (1)

193-203: cleanup検証はOKですが、可能なら「同一関数参照でremoveされた」まで寄せたいです
現状 expect.any(Function) なので、実装が変わっても通りやすいです(致命ではない)。

src/__tests__/features/presence/presenceSettings.test.ts (1)

10-19: デフォルト値をテスト側に重複定義していてドリフトしやすいです
設計書由来の値をこのファイルで固定すると、実装側のデフォルト更新時に「どちらが正か」判断が難しくなります。可能なら src/features/stores/settings.ts(またはpresenceのdefaults)から import して “source of truth” を1箇所に寄せてください。

Also applies to: 21-36, 53-73

src/features/kiosk/guidanceMessage.tsx (1)

16-37: クリック領域とdismiss挙動が暗黙的(pointer-events-none + 親onClick

現状は「メッセージをクリックすると(バブリングで)dismiss」になっていますが、意図が読み取りづらく、将来スタイル変更で挙動が崩れやすいです。メッセージ側にonClickを付け、必要なら背景クリックは別途制御する方が安全です。あわせてキーボード操作のためbutton化(またはrole="button" + tabIndex + onKeyDown)も検討してください。

Proposed change
 export const GuidanceMessage: React.FC<GuidanceMessageProps> = ({
   message,
   visible,
   onDismiss,
 }) => {
   if (!visible) return null

   return (
     <div
       data-testid="guidance-message"
       className="fixed inset-0 flex items-center justify-center pointer-events-none z-40 animate-fade-in text-center text-3xl"
-      onClick={onDismiss}
     >
-      <div
+      <button
+        type="button"
         className="font-bold text-white drop-shadow-lg cursor-pointer pointer-events-auto animate-pulse-slow"
+        onClick={onDismiss}
         style={{
           textShadow: '0 2px 8px rgba(0, 0, 0, 0.5)',
         }}
       >
         {message}
-      </div>
+      </button>
     </div>
   )
 }
src/__tests__/features/idle/idleTypes.test.ts (2)

1-6: コメント(“TDD: RED phase”)が現状とズレて見える

PR本文では「全テスト成功(完了)」なので、ここは“RED phase”ではなく単にテスト説明に直す方が混乱が減ります。


71-88: createIdlePhraseのユニーク性テストはタイミング依存を避けるとより堅い

いまでも十分通りそうですが、将来id生成ロジックが変わった時の安定性のため、crypto.randomUUIDをモックして期待値を固定する形も検討余地があります。

src/utils/demoMode.ts (2)

16-22: “サーバーサイド用”の説明とNEXT_PUBLIC_が不整合

NEXT_PUBLIC_DEMO_MODEはクライアントにも露出する前提の命名なので、docstringを「クライアント/サーバー両方」か「ビルド時に展開される」旨に合わせるか、サーバー専用ならDEMO_MODE等に寄せるのが分かりやすいです。


29-36: エラーレスポンスの機械判読性を上げるならfeatureNameフィールド追加も検討

messageだけだと呼び出し側で分岐しづらいので、必要ならfeatureNameを別フィールドで返すと扱いやすいです(互換性次第)。

src/components/settings/presenceSettings.tsx (3)

8-12: イベント型のReact.ChangeEvent参照はimportに寄せると安全

環境によってはReact名前空間が解決できないケースがあるので、import type { ChangeEvent } from 'react'等に寄せるのが無難です。

Proposed change
+import type { ChangeEvent } from 'react'
 import { useTranslation } from 'react-i18next'
 import settingsStore, {
   PresenceDetectionSensitivity,
 } from '@/features/stores/settings'
 import { TextButton } from '../textButton'
@@
-  const handleGreetingMessageChange = (
-    e: React.ChangeEvent<HTMLTextAreaElement>
-  ) => {
+  const handleGreetingMessageChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
     settingsStore.setState({ presenceGreetingMessage: e.target.value })
   }

   const handleDepartureTimeoutChange = (
-    e: React.ChangeEvent<HTMLInputElement>
+    e: ChangeEvent<HTMLInputElement>
   ) => {
@@
-  const handleCooldownTimeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+  const handleCooldownTimeChange = (e: ChangeEvent<HTMLInputElement>) => {
@@
-  const handleSensitivityChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
+  const handleSensitivityChange = (e: ChangeEvent<HTMLSelectElement>) => {
     settingsStore.setState({
       presenceDetectionSensitivity: e.target
         .value as PresenceDetectionSensitivity,
     })
   }

Also applies to: 40-68


46-60: 数値入力はmin/maxだけだとストアに範囲外が入り得るのでクランプ推奨

DOM改変や手入力でmax超えが入り得るので、Math.min/Math.maxでクランプしてからsetStateする方が堅いです(とくにタイムアウト系)。

Also applies to: 132-166


62-67: PresenceDetectionSensitivityはcastではなくホワイトリスト検証だと安心

select由来なので基本OKですが、将来の変更や外部からの値注入を考えると、low/medium/high以外は無視するガードがあるとより安全です。

Also applies to: 177-187

src/components/idleManager.tsx (1)

26-37: 状態分岐(isIdleActiveidleState === 'disabled')は冗長に見えるので整理余地

useIdleMode側の契約次第ですが、isIdleActivedisabledを内包しているなら片方に寄せると読みやすいです。

src/components/settings/idleSettings.tsx (1)

22-29: 型定義との重複を避ける

EMOTION_OPTIONS配列はsrc/features/idle/idleTypes.tsEmotionType型定義と重複しています。型から配列を導出するか、共通の定数として定義することで保守性が向上します。

♻️ 推奨される改善

src/features/idle/idleTypes.tsでEMOTION_OPTIONSを定義してexportし、ここでimportする方法を検討してください:

// src/features/idle/idleTypes.ts に追加
export const EMOTION_OPTIONS: EmotionType[] = [
  'neutral',
  'happy',
  'sad',
  'angry',
  'relaxed',
  'surprised',
] as const
  import {
    IdlePhrase,
    IdlePlaybackMode,
    EmotionType,
    createIdlePhrase,
    clampIdleInterval,
    IDLE_INTERVAL_MIN,
    IDLE_INTERVAL_MAX,
+   EMOTION_OPTIONS,
  } from '@/features/idle/idleTypes'

- const EMOTION_OPTIONS: EmotionType[] = [
-   'neutral',
-   'happy',
-   'sad',
-   'angry',
-   'relaxed',
-   'surprised',
- ]
src/components/presenceManager.tsx (2)

29-34: 非同期処理の順序に注意

onGreetingStartコールバック内でawait handleSendChat(message)の後にcompleteGreeting()を呼び出していますが、チャット送信が失敗した場合でも挨拶完了状態に遷移します。エラーハンドリングの追加を検討してください。

♻️ 推奨される改善
  const {
    startDetection,
    stopDetection,
    completeGreeting,
    videoRef,
    detectionResult,
    isDetecting,
  } = usePresenceDetection({
    onGreetingStart: async (message: string) => {
-     // 挨拶メッセージをAIに送信
-     await handleSendChat(message)
-     // 挨拶完了
-     completeGreeting()
+     try {
+       // 挨拶メッセージをAIに送信
+       await handleSendChat(message)
+       // 挨拶完了
+       completeGreeting()
+     } catch (error) {
+       console.error('Failed to send greeting message:', error)
+       // エラー時も状態をリセットするか、リトライロジックを追加
+       completeGreeting()
+     }
    },
  })

61-70: デバッグプレビューの条件付きレンダリング

presenceDebugModeが無効の場合にopacity-0 pointer-events-noneを使用する実装は機能的ですが、デバッグモードが無効の場合はコンポーネント自体をレンダリングしない方がパフォーマンス面で優れています。

♻️ オプショナルな改善案
      {/* デバッグプレビュー(検出用ビデオも兼ねる) */}
-     {presenceDetectionEnabled && (
+     {presenceDetectionEnabled && presenceDebugMode && (
        <div
-         className={`absolute bottom-20 right-4 z-30 w-48 ${presenceDebugMode ? '' : 'opacity-0 pointer-events-none'}`}
+         className="absolute bottom-20 right-4 z-30 w-48"
        >
          <PresenceDebugPreview
            videoRef={videoRef}
            detectionResult={detectionResult}
          />
        </div>
      )}

注:この変更により、デバッグモードが無効のときはビデオ要素自体がDOMに存在しなくなります。顔検出がvideoRefに依存している場合は、ビデオ要素を別途レンダリングする必要があるかもしれません。

src/hooks/useFullscreen.ts (1)

24-35: Fullscreen API の互換性ガードをもう一段強化したい(exitFullscreen 未対応環境など)
isSupportedrequestFullscreen の存在だけ見ていますが、exitFullscreen 側は未実装ブラウザだと呼び出しで例外になりえます(fullscreenElement が何らかの形で truthy になったケース等)。document.exitFullscreen の存在確認や、未対応時は no-op にするガードを入れておくと安全です。

提案パッチ(防御的ガード追加)
 function checkFullscreenSupport(): boolean {
   if (typeof document === 'undefined') return false
-  return typeof document.documentElement?.requestFullscreen === 'function'
+  return (
+    typeof document.documentElement?.requestFullscreen === 'function' &&
+    typeof document.exitFullscreen === 'function'
+  )
 }

@@
   const exitFullscreen = useCallback(async () => {
-    if (!document.fullscreenElement) return
+    if (typeof document === 'undefined') return
+    if (!document.fullscreenElement) return
+    if (typeof document.exitFullscreen !== 'function') return

     try {
       await document.exitFullscreen()
     } catch (error) {
       console.warn('Exit fullscreen failed:', error)
     }
   }, [])

Also applies to: 45-55, 58-88

src/__tests__/components/presenceSettings.test.tsx (1)

34-205: テストが「表示テキスト」と「並び順」に依存しすぎ(StatusOff の index など)
getAllByText('StatusOff')[0] / [1] のような順序依存は、UI追加で簡単に壊れます。可能なら「対象行のラベル近傍でボタン取得」や、各トグルに aria-label / data-testid を付けて特定すると安定します。

例:近傍スコープで特定(概念例)
- const toggleButtons = screen.getAllByText('StatusOff')
- fireEvent.click(toggleButtons[0])
+ const section = screen.getByText('PresenceDetectionEnabled').closest('div')!
+ const toggle = within(section).getByRole('button')
+ fireEvent.click(toggle)
src/hooks/useKioskMode.ts (1)

56-95: バリデーション理由文字列の i18n 化(+NGワードの簡易正規化)を検討
アプリ全体が react-i18next 前提なら、reason を翻訳キーに寄せると一貫します。NGワード回避が問題になる運用なら、text.normalize('NFKC') 等の正規化も検討余地があります。

src/__tests__/integration/kioskModeIntegration.test.ts (3)

45-50: 定数チェックは統合テストではなく単体テストに移動することを検討してください。

このテストはDEFAULT_KIOSK_CONFIGの定数値のみをチェックしており、実際の環境変数読み込みや統合的な振る舞いを検証していません。src/__tests__/features/kiosk/kioskTypes.test.tsのような単体テストファイルに移動する方が適切です。


76-94: パスコード検証ロジックのテストカバレッジが不足しています。

このテストはtemporaryUnlock()を直接呼び出していますが、実際のパスコード検証フロー(isValidPasscode()による検証)をテストしていません。正しいパスコード入力、誤ったパスコード入力、無効な形式のパスコードなど、パスコード検証の統合的な動作を確認するテストケースの追加を検討してください。


224-237: 永続化の除外動作が実際に検証されていません。

コメントでは「実際のアプリではpartializekioskTemporaryUnlockを除外する」と記載されていますが、このテストではその永続化除外メカニズムを検証していません。settingsKiosk.test.tsで検証済みであることは理解していますが、統合テストとしてはpartialize設定を確認するか、他のテストファイルへの参照を明確にすることで、テストの意図がより明確になります。

src/components/settings/kioskSettings.tsx (1)

48-55: 入力フィールドを空にした場合のUX改善を検討してください。

ユーザーが入力フィールドを完全にクリアした場合、parseIntNaNを返し、ストアが更新されません。これにより、入力フィールドは空に見えますが、実際の値は変更されていない状態になります。onBlur時にhandleMaxInputLengthBlurがクランプ処理を行うため最終的には問題ありませんが、onChangeonBlurの間に視覚的な不整合が生じる可能性があります。

空の入力を最小値にリセットするか、数値以外の入力を防ぐ処理の追加を検討してください。

♻️ 改善案
 const handleMaxInputLengthChange = (
   e: React.ChangeEvent<HTMLInputElement>
 ) => {
   const value = parseInt(e.target.value, 10)
-  if (!isNaN(value)) {
+  if (!isNaN(value) || e.target.value === '') {
+    // 空の場合は最小値を設定
+    const newValue = isNaN(value) ? KIOSK_MAX_INPUT_LENGTH_MIN : value
-    settingsStore.setState({ kioskMaxInputLength: value })
+    settingsStore.setState({ kioskMaxInputLength: newValue })
   }
 }
src/__tests__/hooks/usePresenceDetection.test.ts (1)

677-744: スキップされたテストについて

クールダウン中の再検知抑制テストがスキップされており、TODOコメントでsetIntervalのコールバック更新タイミングの問題が説明されています。実際の動作ではuseEffectでintervalが再作成されるため正常に動作するとのことですが、このテストがスキップされたままだとクールダウン機能のリグレッションを検出できない可能性があります。

将来的に、actjest.advanceTimersByTimeの組み合わせを調整するか、別のテスト手法(例:waitForを使用した状態確認)でこのテストを有効化することを検討してください。必要であれば、代替テスト実装を提案できます。

src/hooks/usePresenceDetection.ts (1)

92-100: 依存配列なしのuseEffectについて

このuseEffectは依存配列が空ではなく、省略されています。これはすべてのレンダリング後にコールバック参照を更新する意図的なパターンですが、ESLintのexhaustive-depsルールで警告が出る可能性があります。

♻️ 依存配列を明示的に指定する提案
  useEffect(() => {
    callbackRefs.current = {
      onPersonDetected,
      onPersonDeparted,
      onGreetingStart,
      onGreetingComplete,
      onInterruptGreeting,
    }
- })
+ }, [onPersonDetected, onPersonDeparted, onGreetingStart, onGreetingComplete, onInterruptGreeting])
src/features/idle/idleTypes.ts (1)

19-26: EmotionTypeの重複定義について確認が必要

EmotionTypesrc/features/messages/messages.tsでも定義されています(関連コードスニペット参照)。同じ型が2箇所で定義されていると、一方を更新した際に不整合が発生するリスクがあります。

共通の型定義ファイル(例:src/features/types/emotionTypes.ts)を作成するか、既存のmessages.tsからEmotionTypeをインポートすることを検討してください。

// 例: messages.tsからインポート
import { EmotionType } from '@/features/messages/messages'
src/hooks/useIdleMode.ts (1)

160-166: TODO: AI生成機能の実装について

AI生成機能(タスク3.6)のTODOが記載されています。現在はnullを返す実装になっており、安全なフォールバックが確保されています。

AI生成機能の実装をサポートできます。必要であれば、この機能を追跡するための新しいIssueを作成しますか?

src/features/stores/settings.ts (1)

587-607: 環境変数からのブール値パース時のエッジケースについて

idleModeEnabledidleTimePeriodEnabled等のブール値設定で、process.env.NEXT_PUBLIC_... === 'true' || DEFAULT_...というパターンを使用しています。これは意図通り動作しますが、環境変数が'false'に設定されている場合もデフォルト値(通常false)にフォールバックするため、問題ありません。

ただし、将来デフォルト値がtrueに変更された場合、環境変数で'false'を設定しても機能が無効にならない可能性があります。

より明示的なパースを検討してください:

idleModeEnabled:
  process.env.NEXT_PUBLIC_IDLE_MODE_ENABLED !== undefined
    ? process.env.NEXT_PUBLIC_IDLE_MODE_ENABLED === 'true'
    : DEFAULT_IDLE_CONFIG.idleModeEnabled,
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9db481f and 3802399.

⛔ Files ignored due to path filters (3)
  • public/images/setting-icons/idle-settings.svg is excluded by !**/*.svg
  • public/images/setting-icons/kiosk-settings.svg is excluded by !**/*.svg
  • public/images/setting-icons/presence-settings.svg is excluded by !**/*.svg
📒 Files selected for processing (53)
  • locales/ja/translation.json
  • src/__tests__/components/formInputValidation.test.tsx
  • src/__tests__/components/idleManager.test.tsx
  • src/__tests__/components/presenceDebugPreview.test.tsx
  • src/__tests__/components/presenceIndicator.test.tsx
  • src/__tests__/components/presenceSettings.test.tsx
  • src/__tests__/components/settings/idleSettings.test.tsx
  • src/__tests__/components/settings/kioskSettings.test.tsx
  • src/__tests__/features/idle/idleTypes.test.ts
  • src/__tests__/features/kiosk/guidanceMessage.test.tsx
  • src/__tests__/features/kiosk/kioskOverlay.test.tsx
  • src/__tests__/features/kiosk/kioskTypes.test.ts
  • src/__tests__/features/kiosk/passcodeDialog.test.tsx
  • src/__tests__/features/presence/presenceSettings.test.ts
  • src/__tests__/features/presence/presenceStore.test.ts
  • src/__tests__/features/presence/presenceTypes.test.ts
  • src/__tests__/features/stores/settingsIdle.test.ts
  • src/__tests__/features/stores/settingsKiosk.test.ts
  • src/__tests__/hooks/useDemoMode.test.ts
  • src/__tests__/hooks/useEscLongPress.test.ts
  • src/__tests__/hooks/useFullscreen.test.ts
  • src/__tests__/hooks/useIdleMode.test.ts
  • src/__tests__/hooks/useKioskMode.test.ts
  • src/__tests__/hooks/usePresenceDetection.test.ts
  • src/__tests__/integration/kioskModeIntegration.test.ts
  • src/__tests__/integration/presenceDetectionIntegration.test.tsx
  • src/__tests__/utils/demoMode.test.ts
  • src/components/idleManager.tsx
  • src/components/menu.tsx
  • src/components/messageInput.tsx
  • src/components/presenceDebugPreview.tsx
  • src/components/presenceIndicator.tsx
  • src/components/presenceManager.tsx
  • src/components/settings/idleSettings.tsx
  • src/components/settings/index.tsx
  • src/components/settings/kioskSettings.tsx
  • src/components/settings/presenceSettings.tsx
  • src/features/idle/idleTypes.ts
  • src/features/kiosk/guidanceMessage.tsx
  • src/features/kiosk/kioskOverlay.tsx
  • src/features/kiosk/kioskTypes.ts
  • src/features/kiosk/passcodeDialog.tsx
  • src/features/presence/presenceTypes.ts
  • src/features/stores/home.ts
  • src/features/stores/settings.ts
  • src/hooks/useDemoMode.ts
  • src/hooks/useEscLongPress.ts
  • src/hooks/useFullscreen.ts
  • src/hooks/useIdleMode.ts
  • src/hooks/useKioskMode.ts
  • src/hooks/usePresenceDetection.ts
  • src/pages/index.tsx
  • src/utils/demoMode.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/__tests__/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

テストは__tests__ディレクトリに配置

Files:

  • src/__tests__/components/presenceIndicator.test.tsx
  • src/__tests__/components/formInputValidation.test.tsx
  • src/__tests__/components/settings/idleSettings.test.tsx
  • src/__tests__/components/idleManager.test.tsx
  • src/__tests__/features/kiosk/guidanceMessage.test.tsx
  • src/__tests__/components/presenceDebugPreview.test.tsx
  • src/__tests__/hooks/useKioskMode.test.ts
  • src/__tests__/features/idle/idleTypes.test.ts
  • src/__tests__/hooks/usePresenceDetection.test.ts
  • src/__tests__/integration/kioskModeIntegration.test.ts
  • src/__tests__/hooks/useDemoMode.test.ts
  • src/__tests__/features/presence/presenceTypes.test.ts
  • src/__tests__/features/presence/presenceStore.test.ts
  • src/__tests__/features/presence/presenceSettings.test.ts
  • src/__tests__/features/kiosk/passcodeDialog.test.tsx
  • src/__tests__/utils/demoMode.test.ts
  • src/__tests__/hooks/useEscLongPress.test.ts
  • src/__tests__/features/kiosk/kioskOverlay.test.tsx
  • src/__tests__/features/kiosk/kioskTypes.test.ts
  • src/__tests__/features/stores/settingsKiosk.test.ts
  • src/__tests__/integration/presenceDetectionIntegration.test.tsx
  • src/__tests__/components/presenceSettings.test.tsx
  • src/__tests__/hooks/useFullscreen.test.ts
  • src/__tests__/hooks/useIdleMode.test.ts
  • src/__tests__/components/settings/kioskSettings.test.tsx
  • src/__tests__/features/stores/settingsIdle.test.ts
locales/ja/**/*.json

📄 CodeRabbit inference engine (CLAUDE.md)

言語ファイルの更新は日本語(/locales/ja/)のみ行う

Files:

  • locales/ja/translation.json
🧠 Learnings (3)
📚 Learning: 2025-08-05T17:30:02.776Z
Learnt from: CR
Repo: tegnike/aituber-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-05T17:30:02.776Z
Learning: Node.js環境用にcanvasをモック化済み

Applied to files:

  • src/hooks/useDemoMode.ts
📚 Learning: 2025-05-21T14:11:19.604Z
Learnt from: RimgO
Repo: tegnike/aituber-kit PR: 395
File: src/components/messageInputContainer.tsx:46-60
Timestamp: 2025-05-21T14:11:19.604Z
Learning: In `src/components/messageInputContainer.tsx`, the `if (true) { //isNewUser` condition in the `onUserDetected` handler is intentionally left as is because this section is planned to be changed later.

Applied to files:

  • src/components/messageInput.tsx
  • src/components/settings/presenceSettings.tsx
📚 Learning: 2025-08-05T17:30:02.776Z
Learnt from: CR
Repo: tegnike/aituber-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-05T17:30:02.776Z
Learning: Applies to locales/!(ja)/**/*.json : 他の言語ファイル(en、ko、zh等)は手動で更新しない

Applied to files:

  • locales/ja/translation.json
🧬 Code graph analysis (37)
src/__tests__/components/presenceIndicator.test.tsx (1)
src/features/presence/presenceTypes.ts (1)
  • PresenceState (16-16)
src/__tests__/components/formInputValidation.test.tsx (2)
src/features/kiosk/kioskTypes.ts (1)
  • DEFAULT_KIOSK_CONFIG (24-31)
src/hooks/useKioskMode.ts (1)
  • useKioskMode (34-117)
src/components/menu.tsx (1)
src/hooks/useKioskMode.ts (1)
  • useKioskMode (34-117)
src/__tests__/components/idleManager.test.tsx (1)
src/hooks/useIdleMode.ts (1)
  • useIdleMode (62-323)
src/__tests__/components/presenceDebugPreview.test.tsx (1)
src/features/presence/presenceTypes.ts (1)
  • DetectionResult (43-47)
src/__tests__/hooks/useKioskMode.test.ts (2)
src/features/kiosk/kioskTypes.ts (1)
  • DEFAULT_KIOSK_CONFIG (24-31)
src/hooks/useKioskMode.ts (1)
  • useKioskMode (34-117)
src/__tests__/features/idle/idleTypes.test.ts (1)
src/features/idle/idleTypes.ts (7)
  • IDLE_PLAYBACK_MODES (8-8)
  • IdlePlaybackMode (9-9)
  • isIdlePlaybackMode (12-17)
  • IdlePhrase (29-34)
  • createIdlePhrase (37-51)
  • IdleModeSettings (54-71)
  • DEFAULT_IDLE_CONFIG (74-87)
src/components/presenceManager.tsx (2)
src/features/chat/handlers.ts (1)
  • handleSendChatFn (671-840)
src/hooks/usePresenceDetection.ts (1)
  • usePresenceDetection (43-406)
src/__tests__/hooks/usePresenceDetection.test.ts (1)
src/hooks/usePresenceDetection.ts (1)
  • usePresenceDetection (43-406)
src/__tests__/integration/kioskModeIntegration.test.ts (2)
src/features/kiosk/kioskTypes.ts (1)
  • DEFAULT_KIOSK_CONFIG (24-31)
src/hooks/useKioskMode.ts (1)
  • useKioskMode (34-117)
src/__tests__/hooks/useDemoMode.test.ts (1)
src/hooks/useDemoMode.ts (1)
  • useDemoMode (7-15)
src/__tests__/features/presence/presenceTypes.test.ts (1)
src/features/presence/presenceTypes.ts (9)
  • PRESENCE_STATES (8-13)
  • PresenceState (16-16)
  • isPresenceState (50-55)
  • PRESENCE_ERROR_CODES (19-23)
  • PresenceErrorCode (26-26)
  • isPresenceErrorCode (57-64)
  • PresenceError (29-32)
  • BoundingBox (35-40)
  • DetectionResult (43-47)
src/__tests__/features/presence/presenceStore.test.ts (1)
src/features/presence/presenceTypes.ts (1)
  • PresenceError (29-32)
src/components/settings/idleSettings.tsx (2)
src/features/idle/idleTypes.ts (6)
  • EmotionType (20-26)
  • clampIdleInterval (94-98)
  • IdlePlaybackMode (9-9)
  • createIdlePhrase (37-51)
  • IDLE_INTERVAL_MIN (90-90)
  • IDLE_INTERVAL_MAX (91-91)
src/components/textButton.tsx (1)
  • TextButton (4-13)
src/__tests__/features/kiosk/passcodeDialog.test.tsx (1)
src/features/kiosk/passcodeDialog.tsx (2)
  • PasscodeDialogProps (11-16)
  • PasscodeDialog (21-195)
src/hooks/useDemoMode.ts (1)
src/utils/demoMode.ts (1)
  • isDemoMode (20-22)
src/components/messageInput.tsx (1)
src/hooks/useKioskMode.ts (1)
  • useKioskMode (34-117)
src/__tests__/utils/demoMode.test.ts (1)
src/utils/demoMode.ts (3)
  • isDemoMode (20-22)
  • createDemoModeErrorResponse (29-36)
  • DemoModeErrorResponse (11-14)
src/__tests__/hooks/useEscLongPress.test.ts (1)
src/hooks/useEscLongPress.ts (1)
  • useEscLongPress (22-80)
src/hooks/usePresenceDetection.ts (1)
src/features/presence/presenceTypes.ts (3)
  • PresenceState (16-16)
  • PresenceError (29-32)
  • DetectionResult (43-47)
src/components/presenceDebugPreview.tsx (1)
src/features/presence/presenceTypes.ts (1)
  • DetectionResult (43-47)
src/__tests__/features/kiosk/kioskOverlay.test.tsx (1)
src/features/kiosk/kioskOverlay.tsx (1)
  • KioskOverlay (17-94)
src/components/idleManager.tsx (1)
src/hooks/useIdleMode.ts (1)
  • useIdleMode (62-323)
src/components/settings/kioskSettings.tsx (2)
src/features/kiosk/kioskTypes.ts (4)
  • clampKioskMaxInputLength (39-43)
  • parseNgWords (54-59)
  • KIOSK_MAX_INPUT_LENGTH_MIN (34-34)
  • KIOSK_MAX_INPUT_LENGTH_MAX (35-35)
src/components/textButton.tsx (1)
  • TextButton (4-13)
src/components/settings/presenceSettings.tsx (2)
src/features/stores/settings.ts (1)
  • PresenceDetectionSensitivity (235-235)
src/components/textButton.tsx (1)
  • TextButton (4-13)
src/components/presenceIndicator.tsx (1)
src/features/presence/presenceTypes.ts (1)
  • PresenceState (16-16)
src/features/kiosk/kioskOverlay.tsx (4)
src/hooks/useKioskMode.ts (1)
  • useKioskMode (34-117)
src/hooks/useFullscreen.ts (1)
  • useFullscreen (40-99)
src/hooks/useEscLongPress.ts (1)
  • useEscLongPress (22-80)
src/features/kiosk/passcodeDialog.tsx (1)
  • PasscodeDialog (21-195)
src/hooks/useIdleMode.ts (4)
src/features/idle/idleTypes.ts (2)
  • EmotionType (20-26)
  • IdlePhrase (29-34)
src/features/messages/messages.ts (1)
  • Talk (22-26)
src/features/messages/speakCharacter.ts (1)
  • speakCharacter (365-365)
src/features/messages/speakQueue.ts (1)
  • SpeakQueue (14-224)
src/__tests__/features/kiosk/kioskTypes.test.ts (1)
src/features/kiosk/kioskTypes.ts (7)
  • DEFAULT_KIOSK_CONFIG (24-31)
  • KIOSK_MAX_INPUT_LENGTH_MIN (34-34)
  • KIOSK_MAX_INPUT_LENGTH_MAX (35-35)
  • KIOSK_PASSCODE_MIN_LENGTH (36-36)
  • clampKioskMaxInputLength (39-43)
  • isValidPasscode (46-51)
  • parseNgWords (54-59)
src/__tests__/features/stores/settingsKiosk.test.ts (1)
src/features/kiosk/kioskTypes.ts (1)
  • DEFAULT_KIOSK_CONFIG (24-31)
src/pages/index.tsx (1)
src/features/kiosk/kioskOverlay.tsx (1)
  • KioskOverlay (17-94)
src/__tests__/integration/presenceDetectionIntegration.test.tsx (1)
src/hooks/usePresenceDetection.ts (1)
  • usePresenceDetection (43-406)
src/features/stores/settings.ts (4)
src/features/memory/memoryTypes.ts (2)
  • MemoryConfig (45-54)
  • DEFAULT_MEMORY_CONFIG (59-64)
src/features/idle/idleTypes.ts (2)
  • IdleModeSettings (54-71)
  • DEFAULT_IDLE_CONFIG (74-87)
src/features/kiosk/kioskTypes.ts (2)
  • KioskModeSettings (9-21)
  • DEFAULT_KIOSK_CONFIG (24-31)
src/utils/demoMode.ts (1)
  • isDemoMode (20-22)
src/__tests__/hooks/useFullscreen.test.ts (1)
src/hooks/useFullscreen.ts (1)
  • useFullscreen (40-99)
src/__tests__/hooks/useIdleMode.test.ts (1)
src/hooks/useIdleMode.ts (1)
  • useIdleMode (62-323)
src/features/stores/home.ts (1)
src/features/presence/presenceTypes.ts (2)
  • PresenceState (16-16)
  • PresenceError (29-32)
src/__tests__/features/stores/settingsIdle.test.ts (1)
src/features/idle/idleTypes.ts (1)
  • DEFAULT_IDLE_CONFIG (74-87)
🪛 GitHub Actions: Lint and Format
src/components/presenceDebugPreview.tsx

[error] 84-84: Error: Cannot access refs during render


[error] 84-84: Cannot access refs during render (react-hooks/refs)

src/pages/index.tsx

[warning] 39-39: The 'characterPresets' array makes the dependencies of useEffect Hook change on every render. Move it inside the useEffect callback or wrap with useMemo.

🪛 GitHub Actions: Run Tests
src/components/messageInput.tsx

[warning] 139-139: Warning: Calling setState synchronously within an effect. Avoid cascading renders.


[warning] 186-186: Warning: Calling setState synchronously within an effect. Avoid cascading renders.


[warning] 177-177: Warning: Compilation Skipped: Existing memoization could not be preserved due to dependency changes.

src/components/presenceDebugPreview.tsx

[error] 84-84: Error: Cannot access refs during render. Accessing ref.current inside render is not allowed.

src/pages/index.tsx

[warning] 39-39: The 'characterPresets' array makes useEffect dependencies unstable; consider moving inside useEffect or memoizing.

⏰ 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: claude-review
🔇 Additional comments (53)
src/__tests__/features/kiosk/passcodeDialog.test.tsx (1)

1-338: テストカバレッジが優れています

PasscodeDialogコンポーネントの包括的なテストスイートです。以下の点が評価できます:

  • 要件ベースのテスト構成(3.1, 3.2, 3.3)
  • ロックアウト機能、タイマー動作、アクセシビリティを網羅
  • fake timersの適切な使用で時間依存の処理をテスト

テストロジックも適切で、エッジケース(境界値、空入力、英数字パスコードなど)もカバーされています。

src/__tests__/components/formInputValidation.test.tsx (1)

1-179: 入力バリデーションのテストが適切です

キオスクモードの入力制限に関する包括的なテストです:

  • 最大文字数制限の境界値テスト(等価、超過)
  • NGワードフィルタリング(大文字小文字の区別なし)
  • 複合バリデーションの動作確認
  • キオスクモード無効時のバリデーションスキップ確認

テストはuseKioskModeフックの振る舞いを正確に検証しており、問題ありません。

src/components/messageInput.tsx (6)

320-339: バリデーションロジックが適切に実装されています

キオスクモードの入力検証を送信前に実行するhandleValidatedSend関数です:

  • 空文字チェック(Line 323)
  • キオスクモード時のバリデーション実行(Line 326-332)
  • エラーメッセージの設定と結果の返却

実装は適切で、送信処理との分離も明確です。


341-374: Enterキー送信時のバリデーション統合を確認してください

キー入力処理が更新され、送信前にバリデーションを実行するようになっています(Line 352-361)。

ただし、handleValidatedSendに渡しているeventのキャストが複雑です:

event as unknown as React.MouseEvent<HTMLButtonElement>

この型キャストは動作しますが、より型安全な実装も検討できます。

バリデーション失敗時にsetRows(1)(Line 360)が実行されないことを確認してください。現在の実装ではhandleValidatedSendtrueを返した場合のみ行数がリセットされますが、これが意図した動作か検証をお願いします。


376-384: 送信ボタンのバリデーション統合が適切です

handleSendClickラッパー関数で送信前のバリデーションを実行し、検証成功時のみonClickSendButtonを呼び出しています。

実装はシンプルで理解しやすく、問題ありません。


442-447: バリデーションエラー表示が適切に追加されています

キオスクモードの入力検証エラーをユーザーに表示するUIが追加されました。fileError表示と同じスタイルで統一されており、UXの一貫性が保たれています。


475-492: マイクボタンのアイコン切り替えロジックを確認してください

連続マイクリスニングモードでのアイコン表示ロジックが変更されています:

  • continuousMicListeningMode有効時:Closeアイコン表示
  • isProcessing時(マイク録音中):Microphoneアイコンに切り替え

Line 490のdisabled条件も更新され、continuousMicListeningMode時は無効化されます。

以下の点を確認してください:

  1. 連続モードでマイク録音中に24/Microphoneアイコンが表示される意図(Line 487)
  2. 連続モード有効時にボタンが無効化される理由(Line 490)

この動作が仕様通りか、UIの挙動として直感的かを検証することをお勧めします。


558-558: maxLengthの動的設定が適切です

キオスクモード時の最大入力文字数制限がmaxInputLengthプロパティとしてtextareaに適用されています。useKioskModeフックから取得した値が正しく反映されており、実装は適切です。

src/features/kiosk/passcodeDialog.tsx (7)

11-16: Props定義が明確です

PasscodeDialogPropsインターフェースは、必要なプロパティを過不足なく定義しています:

  • isOpen:ダイアログの表示状態
  • onClose:キャンセル時のコールバック
  • onSuccess:認証成功時のコールバック
  • correctPasscode:正しいパスコード

型定義が明確で、使用側での誤用を防げます。


37-41: フォーカス管理が適切です

ダイアログが開いた時、かつロックアウト中でない時に入力フィールドにフォーカスを設定しています。依存配列にisOpenisLockedを含めることで、適切なタイミングでフォーカスが当たります。


44-60: ロックアウトのタイマー処理が正しく実装されています

カウントダウンロジックの実装:

  • 1秒ごとにlockoutCountdownをデクリメント(Line 47-57)
  • カウントが1以下になったらロック解除と試行回数リセット(Line 49-53)
  • インターバルの適切なクリーンアップ(Line 59)

実装は堅牢で、メモリリークの心配もありません。


64-83: Escapeキーハンドラの遅延処理が優れています

Line 68-70の500msディレイは、長押しでダイアログを開いた直後にEscapeキーで即座に閉じてしまうのを防ぐための工夫です。これによりユーザー体験が向上します。

イベントリスナーとタイマーの適切なクリーンアップも実装されています(Line 79-81)。


86-92: 状態リセットロジックが適切です

ダイアログが閉じた時:

  • パスコードとエラーメッセージはクリア
  • 試行回数とロックアウト状態は保持(Line 90のコメント)

これにより、ダイアログを閉じて再度開いた場合でもロックアウトが継続し、セキュリティが維持されます。


94-119: パスコード検証ロジックが堅牢です

handleSubmitの実装が適切です:

  • ロック中は早期リターン(Line 95)
  • 単純な文字列比較で検証(Line 97)
  • 成功時の状態リセットとコールバック実行(Line 99-102)
  • 失敗時の試行回数管理とロックアウト判定(Line 104-117)

試行回数がMAX_ATTEMPTSに達した時のロックアウト処理も正しく実装されています。


141-155: 入力フィールドの実装が適切です

パスワード入力フィールドの実装が優れています:

  • type="password"でマスキング(Line 143)
  • role="textbox"でアクセシビリティ対応(Line 144)
  • disabled属性でロック時の入力防止(Line 148)
  • autoComplete="off"でブラウザの自動補完無効化(Line 154)

セキュリティとユーザビリティの両面で適切な実装です。

locales/ja/translation.json (1)

129-129: LGTM! 翻訳キーの追加が適切です

Kiosk/Idle/Presence機能の日本語翻訳が正しく追加されています。プレースホルダー({{count}}など)の使用も適切です。

Also applies to: 443-445, 458-462, 463-553

src/pages/index.tsx (1)

118-122: LGTM! 新機能のコンポーネントが正しく追加されています

PresenceManager、IdleManager、KioskOverlayの3つの新しいコンポーネントが適切に配置されています。IdleManagerの絶対配置も意図的なものと思われます。

src/__tests__/hooks/useEscLongPress.test.ts (1)

1-276: LGTM! 包括的なテストカバレッジです

useEscLongPressフックのテストが網羅的に記述されています。基本機能、設定可能な期間、有効/無効状態、繰り返しイベント、クリーンアップ、isHolding状態など、すべての重要なシナリオがカバーされています。

src/__tests__/features/stores/settingsKiosk.test.ts (1)

1-138: LGTM! キオスク設定の完全なテストカバレッジです

キオスクモード設定のすべてのフィールドが適切にテストされています。デフォルト値の検証、更新可能性の確認、永続性のテストなど、必要なテストケースがすべて網羅されています。

src/hooks/useEscLongPress.ts (1)

32-37: API/実装ともにシンプルで使いやすいです

タイマー管理をuseRefに閉じ込めており、返り値もisHoldingのみでUI用途に適しています。

Also applies to: 79-80

src/__tests__/features/presence/presenceStore.test.ts (2)

12-19: ストア初期化が部分更新だとテスト間汚染の温床になりやすいです

homeStore.setStateがmerge動作の場合、presence以外のフィールドが前テスト/他テストの影響を受けたままになります。可能なら「初期状態へ完全リセット」またはreplaceでのセットに寄せたいです(ZustandならsetState(partial, true)等)。


21-133: カバレッジ(state/error/timestamp/遷移)の押さえ方は良いです

presenceState/presenceError/lastDetectionTimeの基本動作と代表的遷移が揃っていて安心感があります。

src/__tests__/features/kiosk/guidanceMessage.test.tsx (1)

14-93: 要件に沿ったテスト観点が揃っていて良いです

表示制御、アニメーションクラス、dismiss挙動、最低限のスタイル確認まで含まれており、回帰防止として十分機能しそうです。

src/__tests__/components/presenceDebugPreview.test.tsx (1)

50-167: bboxの描画条件+ミラー座標計算まで検証できていて良いです

debug modeのトグル、faceDetected/boundingBox有無、ミラー座標(videoWidth - x - width)の期待値まで入っていて、意図が明確です。

src/__tests__/components/idleManager.test.tsx (2)

77-177: 表示条件・状態別UIのテストは押さえられていて良いです

idle有効/無効、waiting/speakingの見た目、countdown表示まで分けて検証できています。


179-191: useIdleModeの呼び出し引数まで固定できるかは確認したいです

現状は「呼ばれたこと」までなので、IdleManagerがuseIdleModeへ渡すコールバック/propsの形を担保したい場合はtoHaveBeenCalledWith(...)等に強化できるか、実装側(src/components/idleManager.tsx)と合わせて確認したいです。

src/__tests__/components/presenceIndicator.test.tsx (1)

1-138: テストの実装が適切です。

このテストスイートは包括的で、以下の点が優れています:

  • ストアとi18nのモックが適切に設定されている
  • データ駆動型のテスト(Lines 74-79)で複数の状態を効率的にテストしている
  • data-testid を使用した明確な要素の特定
  • 可視性、状態表示、アニメーション、カスタムクラスのすべての側面をカバーしている
src/components/menu.tsx (2)

59-64: デモ端末モードの統合が適切に実装されています。

effectiveShowControlPanel の導入により、デモ端末モード時(一時解除されていない場合)にコントロールパネルを非表示にする制御が明確になっています。


154-165: useEffectの依存配列が正しく更新されています。

Line 165で canAccessSettings を依存配列に追加したことで、キオスクモードの状態変化時にキーボードショートカットのイベントリスナーが適切に再登録されます。

src/__tests__/features/presence/presenceTypes.test.ts (1)

1-183: 型定義とガード関数のテストが包括的です。

このテストスイートは以下の点で優れています:

  • すべての型定義と型ガード関数を網羅的にテスト
  • 有効な値と無効な値の両方をテスト
  • Lines 136-139で浮動小数点数の比較に toBeCloseTo を正しく使用
  • データ構造の境界値と制約を適切に検証
src/__tests__/components/settings/idleSettings.test.tsx (3)

81-108: 要件ベースのテスト構造が明確です。

テストケースが要件番号(Requirement 1.1など)と紐付けられており、トレーサビリティが高く保守性が良好です。この構造は他のテストファイルでも採用することを推奨します。


51-70: ヘルパー関数でテストの可読性が向上しています。

createDefaultState ヘルパー関数により、各テストケースで必要な状態を明確かつ簡潔に設定できています。型アノテーション(Lines 53-58)も適切に記述されています。


130-152: テストは正しくクランピング機能を検証しています。

テストの実装は実際に有効です。ストア内のidleIntervalを5または500に設定してレンダリングすると、制御されたコンポーネントの入力フィールドはそれらの値で表示されます。blur イベント時に、handleIntervalBlurハンドラーは DOM のe.target.value(つまり 5 または 500)を読み込み、clampIdleInterval()関数を通して処理し、クランプされた値(10 または 300)でsetStateを呼び出します。

clampIdleInterval()が呼び出されないと、setState{idleInterval: 5}または{idleInterval: 500}で呼ばれるはずです。テストが{idleInterval: 10}{idleInterval: 300}を期待しているため、クランピングロジックが実際に実行されていることが検証されます。テストが失敗した場合、クランピング実装が機能していないことが明確に示されます。

このテストアプローチは、blur ハンドラーがクランピングを正しく実行することを効果的に検証しており、変更は必要ありません。

src/__tests__/features/kiosk/kioskTypes.test.ts (1)

17-113: テスト意図が明確でカバレッジも十分です(clamp / passcode / parseNgWords)
ユーティリティの境界値も押さえられていて良いです。

src/__tests__/hooks/useKioskMode.test.ts (1)

97-189: 入力検証のテスト観点が揃っていて良いです(max length / NG word / empty)
NGワードの大小文字・無効化も押さえられていて安心です。

src/components/settings/idleSettings.tsx (1)

31-477: 実装品質が高い

コンポーネント全体の実装は以下の点で優れています:

  • 設定項目ごとに適切に分離されたハンドラー
  • バリデーション処理(clampIdleIntervalの使用)
  • アクセシビリティ対応(aria-labelの使用)
  • 国際化対応(useTranslationの一貫した使用)
  • ユーザビリティ(Enter キーでのフレーズ追加、Line 350-354)

上記のイミュータビリティの問題を修正すれば、本番環境に適した実装です。

src/__tests__/features/kiosk/kioskOverlay.test.tsx (1)

84-259: テストカバレッジが包括的

このテストスイートは以下の点で優れています:

  • レンダリング条件の網羅的なテスト(キオスクモード、一時解除状態)
  • フルスクリーン機能の完全なカバレッジ
  • パスコードダイアログのフロー検証
  • 適切なモックの分離とリセット(beforeEach
  • 非同期操作の正しい処理(actwaitForの使用)

コーディングガイドラインに従い、__tests__ディレクトリに適切に配置されています。

src/__tests__/integration/presenceDetectionIntegration.test.tsx (1)

1-294: 統合テストの設計が優れている

この統合テストは以下の点で高品質です:

  • 適切なJSDOMテスト環境の指定(Line 2)
  • フック初期化からクリーンアップまでの完全なライフサイクルカバレッジ
  • エラーハンドリングの包括的な検証(カメラ権限、デバイス不可、モデル読み込み失敗)
  • コールバックAPIの存在確認
  • i18n統合の検証(Lines 278-294)

テストはプロダクションコードの意図した動作を効果的に検証しています。

src/__tests__/features/stores/settingsIdle.test.ts (1)

10-181: ストアテストの品質が高い

このテストスイートは以下の理由で優れています:

  • DEFAULT_IDLE_CONFIGを使用した一貫性のある初期化
  • 各設定項目に対する明確なテストケース
  • デフォルト値と更新の両方を検証
  • 複数の設定を組み合わせた永続性テスト(Lines 155-180)
  • TDDアプローチに沿った構造

アイドルモード機能のストア統合を効果的に検証しています。

src/components/presenceManager.tsx (1)

38-44: useEffectの依存配列が適切

presenceDetectionEnabledisDetectingの状態に基づいて検出を開始/停止するロジックは適切に実装されています。startDetectionstopDetectionがフックから安定した参照として提供されていることを前提に、無限ループのリスクはありません。

src/features/stores/home.ts (1)

10-10: LGTM!

プレゼンス検出機能の状態管理が適切に追加されています。トランジェント状態として配置され、適切な初期値で初期化されています。型定義もpresenceTypes.tsから正しくインポートされています。

Also applies to: 37-40, 141-144

src/features/kiosk/kioskTypes.ts (1)

38-59: LGTM!

ユーティリティ関数が適切に実装されています。clampKioskMaxInputLengthは境界値を正しく処理し、isValidPasscodeは適切なバリデーションを行い、parseNgWordsは空文字列を適切にフィルタリングしています。

src/__tests__/hooks/useIdleMode.test.ts (1)

1-522: LGTM!

包括的で構造化されたテストスイートです。以下の点が優れています:

  • タスク別に明確に整理されたテストケース(Task 3.1〜3.5)
  • jest.useFakeTimers()を使用した適切なタイマーベースのテスト
  • 再利用可能なヘルパー関数(setupSettingsMocksetupHomeMock
  • エッジケース(空のフレーズリスト、AI処理中、発話中など)の網羅的なカバレッジ
  • @testing-library/reactrenderHookactの適切な使用

全ての主要機能とエッジケースがテストされており、コード品質が高いです。

src/__tests__/hooks/usePresenceDetection.test.ts (1)

1-107: テストのセットアップとモック構成が適切です。

テストファイルの構成は適切で、face-api.js、ストア、navigator.mediaDevicesのモックが正しく設定されています。beforeEachjest.useFakeTimers()を使用し、afterEachjest.useRealTimers()を呼び出すパターンも正しいです。

src/features/presence/presenceTypes.ts (1)

1-64: 型定義が適切に実装されています。

as constを使用した定数配列からの型派生パターンは、TypeScriptのイディオムとして適切です。型ガード関数isPresenceStateisPresenceErrorCodeも正しく実装されており、ランタイムでの型チェックが可能です。

src/hooks/usePresenceDetection.ts (3)

221-242: handleDeparture内でのpresenceState参照について確認が必要

handleDepartureuseCallbackの依存配列にpresenceStateを含んでいますが、この関数はsetTimeoutのコールバックとして使用されます(Line 296-299)。setTimeoutが設定された時点のhandleDepartureがキャプチャされるため、presenceStateが古い値を参照する可能性があります。

ただし、現在の実装ではdepartureTimeoutRefを使って重複タイマーを防いでおり、離脱処理は一度だけ実行されるため、実際には問題が発生しにくい設計になっています。

この設計意図が正しいか確認してください。もしpresenceStateの最新値が必要な場合は、homeStore.getState()を使用して最新の状態を取得する方法もあります。


126-144: モデルロード処理が適切に実装されています。

modelLoadedRefを使用した重複ロード防止、エラーハンドリング、およびhomeStoreへの状態同期が適切に実装されています。


146-193: カメラエラーハンドリングが包括的に実装されています。

NotAllowedErrorNotFoundError等のブラウザ固有エラーを適切にマッピングし、ユーザーフレンドリーなエラーメッセージを設定しています。

src/features/idle/idleTypes.ts (1)

89-98: インターバル検証関数が適切に実装されています。

clampIdleIntervalは境界値チェックを行い、有効な範囲内に値を制限します。定数IDLE_INTERVAL_MINIDLE_INTERVAL_MAXを使用しており、保守性が高いです。

src/hooks/useIdleMode.ts (1)

111-131: 発話条件判定が適切に実装されています。

canSpeak関数は、AI処理中、発話中、人感検知時の3つの条件を適切にチェックしており、競合を回避する設計になっています。

src/features/stores/settings.ts (3)

671-675: デモモードでのWebSocket機能無効化が適切に実装されています。

onRehydrateStorageでデモモード時にrealtimeAPIModeaudioModeを強制的に無効化することで、永続化された設定に関わらずセキュリティを確保しています。


857-862: kioskTemporaryUnlockが永続化されないことが明示されています。

コメントでkioskTemporaryUnlockが永続化対象外であることが明記されており、セッション間でロック解除状態が維持されないという正しい動作が保証されています。


234-255: 新しい型定義とインターフェースが適切に追加されています。

PresenceDetectionSensitivity型とPresenceDetectionSettingsインターフェースが追加され、SettingsStateMemoryConfigPresenceDetectionSettingsIdleModeSettingsKioskModeSettingsが正しく統合されています。

Comment on lines 17 to 32
jest.mock('@/features/stores/settings', () => {
return {
__esModule: true,
default: Object.assign(jest.fn(), {
setState: (arg: any) => mockSetState(arg),
getState: () => ({
kioskModeEnabled: false,
kioskPasscode: '0000',
kioskMaxInputLength: 200,
kioskNgWords: [],
kioskNgWordEnabled: false,
kioskTemporaryUnlock: false,
}),
}),
}
})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

setState のシグネチャ(関数updater)を使う実装だとモックが追従できない可能性があります
Zustandの setStatesetState(partial | (state)=>partial, replace?) を取り得るので、実装側が関数を渡す形だと現モックではテストが不自然になります。

修正案(受け取った引数をそのまま記録できるようにする)
 default: Object.assign(jest.fn(), {
-  setState: (arg: any) => mockSetState(arg),
+  setState: (...args: any[]) => mockSetState(...args),
   getState: () => ({
     ...
   }),
 }),
📝 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
jest.mock('@/features/stores/settings', () => {
return {
__esModule: true,
default: Object.assign(jest.fn(), {
setState: (arg: any) => mockSetState(arg),
getState: () => ({
kioskModeEnabled: false,
kioskPasscode: '0000',
kioskMaxInputLength: 200,
kioskNgWords: [],
kioskNgWordEnabled: false,
kioskTemporaryUnlock: false,
}),
}),
}
})
jest.mock('@/features/stores/settings', () => {
return {
__esModule: true,
default: Object.assign(jest.fn(), {
setState: (...args: any[]) => mockSetState(...args),
getState: () => ({
kioskModeEnabled: false,
kioskPasscode: '0000',
kioskMaxInputLength: 200,
kioskNgWords: [],
kioskNgWordEnabled: false,
kioskTemporaryUnlock: false,
}),
}),
}
})
🤖 Prompt for AI Agents
In @src/__tests__/components/settings/kioskSettings.test.tsx around lines 17 -
32, The mocked zustand store's setState only accepts a partial object, but real
setState can accept a function updater; update the mock inside the jest.mock for
'@/features/stores/settings' so setState checks if the received arg is a
function and, if so, calls it with the current state (from the mock's getState),
merges the result into the internal mock state, and returns accordingly;
reference the mocked setState and getState and the mockSetState helper so the
mock both updates its internal state variable and records the received
argument(s) for assertions.

@claude
Copy link
Contributor

claude bot commented Jan 11, 2026

Claude finished @tegnike's task —— View job


PR Review: Kiosk/Idle/Presence Features

This is an impressive and comprehensive implementation of three major features for unattended operation. The codebase shows excellent engineering practices with comprehensive test coverage (59 test files) and well-structured code.

Review Checklist:

  • Review core feature implementations (hooks and components)
  • Analyze security implications of kiosk mode
  • Evaluate presence detection implementation
  • Check test coverage and quality
  • Review performance considerations
  • Examine state management changes
  • Provide comprehensive feedback

Strengths

1. Code Quality & Architecture

  • Excellent separation of concerns: Each feature has its own types, hooks, components, and tests
  • Consistent patterns: All three features follow the same architectural patterns (types → hooks → components → tests)
  • TypeScript usage: Strong type safety throughout with proper interfaces and type guards
  • React best practices: Proper use of useCallback, useMemo, refs, and effect cleanup

2. Test Coverage

  • Comprehensive testing: 59 test files with unit, integration, and component tests
  • 100% feature coverage: All new hooks, components, and utilities have corresponding tests
  • Realistic test scenarios: Tests cover edge cases, error conditions, and integration scenarios
  • Proper mocking: Well-structured mocks for external dependencies (face-api.js, MediaDevices)

3. Performance Optimizations

  • Efficient presence detection: Configurable detection intervals (150ms-500ms) based on sensitivity
  • Proper cleanup: All timers, intervals, and media streams are properly cleaned up
  • Optimized re-renders: Strategic use of useCallback and useMemo to prevent unnecessary renders
  • Lazy loading: Components are conditionally rendered based on feature enablement

4. User Experience

  • Intuitive kiosk mode: ESC long-press (2s) for admin access is discoverable yet secure
  • Progressive security: Rate limiting (3 attempts, 30s lockout) and input validation
  • Comprehensive presence detection: Multiple states (idle → detected → greeting → conversation-ready)
  • Smart idle management: Considers AI processing, speaking state, and presence detection

⚠️ Areas for Improvement

1. Security Considerations

Passcode Security (src/features/kiosk/passcodeDialog.tsx:97):

  • Good: Rate limiting, lockout mechanism, input masking
  • ⚠️ Concern: Plain text passcode comparison in client-side code
  • 📋 Recommendation: Consider hashing passcodes client-side (though this has limited security benefit for client-side apps)

Input Validation (src/components/messageInput.tsx:325-332):

  • Good: Proper validation with user feedback
  • ⚠️ Concern: NG words are processed case-insensitively but stored in plain text
  • 📋 Recommendation: Consider more sophisticated content filtering if needed

2. Performance Considerations

Face Detection Loop (src/hooks/usePresenceDetection.ts:342-355):

  • Good: Configurable intervals, proper cleanup
  • ⚠️ Concern: Continuous camera processing could impact battery life
  • 📋 Recommendation: Consider implementing smart detection pausing or reduced frame rate during inactivity

Memory Management (src/hooks/usePresenceDetection.ts:195-206):

  • Good: Stream cleanup is implemented
  • 📋 Suggestion: Monitor for potential memory leaks with face-api.js model loading

3. Accessibility & UX

ESC Key Handling (src/hooks/useEscLongPress.ts:42-77):

  • Good: Prevents accidental activation with 2s duration
  • ⚠️ Concern: No visual feedback during long-press
  • 📋 Recommendation: Consider adding visual progress indicator

Camera Permissions (src/hooks/usePresenceDetection.ts:146-193):

  • Good: Comprehensive error handling for different permission states
  • 📋 Suggestion: Consider graceful degradation when camera is not available

4. Code Structure

Large Hook Files:

  • usePresenceDetection.ts (406 lines) and useIdleMode.ts (323 lines) are quite large
  • 📋 Recommendation: Consider extracting some logic into separate utility functions

Type Definitions (src/features/presence/presenceTypes.ts):

  • Good: Proper type guards and constants
  • 📋 Minor: Could benefit from JSDoc comments for complex types

🔧 Minor Issues

  1. Translation Keys: Some hardcoded Japanese text in passcodeDialog.tsx:163,170 should use translation keys
  2. Console Logging: Debug logs in presence detection should be removable in production builds
  3. Magic Numbers: Some timeouts (500ms in passcodeDialog.tsx:70) could be constants

🚀 Exceptional Features

1. Integration Design

The three features work together seamlessly:

  • Presence detection triggers idle mode reset
  • Kiosk mode applies input validation without breaking other features
  • All features respect AI processing states

2. Developer Experience

  • Comprehensive TypeScript types with proper exports
  • Clear component interfaces and hook returns
  • Excellent test coverage with realistic scenarios
  • Well-documented requirements tracing in tests

3. Production Ready

  • Environment variable support for configuration
  • Proper error boundaries and fallback states
  • Performance monitoring capabilities (debug modes)
  • Graceful feature degradation

🎯 Overall Assessment

This is exceptional work that demonstrates:

  • Professional-grade architecture and code organization
  • Comprehensive testing and quality assurance
  • Strong security awareness with appropriate mitigations
  • Performance-conscious implementation with optimization opportunities identified
  • Production-ready features suitable for deployment

The codebase quality is very high, and the features are well-implemented for their intended use cases (digital signage, exhibitions, unattended operation).

Recommendation: ✅ Approve with minor suggestions

The identified areas for improvement are mostly enhancements rather than blockers, and the overall implementation quality justifies merging this PR.


Copy link

@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

🤖 Fix all issues with AI agents
In @package.json:
- Line 49: The dependency "face-api.js": "^0.22.2" is flagged as unmaintained
and has multiple high/critical vulnerabilities; remove or replace it in
package.json and update code imports/usages accordingly: either upgrade to a
maintained, secure alternative (e.g., migrate to maintained @tensorflow models
such as @tensorflow-models/face-detection or
@tensorflow-models/face-landmarks-detection, or a vetted fork) or remove the
dependency if not needed; update package.json to remove/replace the
"face-api.js" entry, run npm install to refresh lockfile, run npm audit and npm
audit fix (or apply manual fixes) and run the test suite to ensure imports
(places referencing face-api.js) are updated to the new library/API.
🧹 Nitpick comments (1)
next.config.js (1)

11-19: クライアント側のみ fs を無効化する方針は妥当。config.resolve のガードを入れるとより安全です。

Line 13 で config.resolve が未定義のケースに備えて初期化しておくと、将来のNext/webpack側変更にも強くなります。

提案差分
 webpack: (config, { isServer }) => {
   if (!isServer) {
+    config.resolve = config.resolve || {}
     config.resolve.fallback = {
-      ...config.resolve.fallback,
+      ...(config.resolve.fallback || {}),
       fs: false,
     }
   }
   return config
 },
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3802399 and 8ba5529.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (2)
  • next.config.js
  • package.json
⏰ 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: claude-review

"ai": "^6.0.6",
"axios": "^1.6.8",
"canvas": "^3.2.0",
"face-api.js": "^0.22.2",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify face-api.js version, security, and maintenance status

echo "=== Checking NPM registry for face-api.js ==="
# Get version information from npm
npm_info=$(curl -s https://registry.npmjs.org/face-api.js)

# Extract latest version
latest_version=$(echo "$npm_info" | jq -r '.["dist-tags"].latest // "Not found"')
echo "Latest version: $latest_version"

# Extract current version specified in package.json
current_version="0.22.2"
echo "Specified version: ^$current_version"

# Check if specified version exists
version_exists=$(echo "$npm_info" | jq -r ".versions.\"$current_version\" // empty")
if [ -z "$version_exists" ]; then
  echo "Version $current_version: NOT FOUND in registry"
else
  echo "Version $current_version: Found in registry"
fi

# Get last publish date
echo -e "\n=== Version timeline ==="
echo "$npm_info" | jq -r '.time | to_entries | .[-5:] | .[] | "\(.key): \(.value)"'

# Check repository information
echo -e "\n=== Repository info ==="
repo_url=$(echo "$npm_info" | jq -r '.repository.url // "Not found"')
echo "Repository: $repo_url"

# Check for GitHub repo info
if [[ $repo_url == *"github.com"* ]]; then
  repo_path=$(echo "$repo_url" | sed 's|.*github.com/||' | sed 's|\.git||')
  echo "GitHub path: $repo_path"
  
  # Get GitHub repo stats
  github_info=$(curl -s "https://api.github.com/repos/$repo_path")
  echo "Last commit: $(echo "$github_info" | jq -r '.pushed_at // "Not found"')"
  echo "Open issues: $(echo "$github_info" | jq -r '.open_issues_count // "Not found"')"
  echo "Archived: $(echo "$github_info" | jq -r '.archived // "Not found"')"
  echo "Watchers: $(echo "$github_info" | jq -r '.watchers_count // "Not found"')"
  echo "Last update (GitHub page): $(echo "$github_info" | jq -r '.updated_at // "Not found"')"
fi

echo -e "\n=== Checking for known vulnerabilities ==="
# Try to get vulnerability information
npm audit_info=$(npm view face-api.js@"$current_version" --json 2>&1)
if echo "$audit_info" | grep -q "error"; then
  echo "Could not retrieve audit info from npm"
else
  echo "$audit_info" | head -20
fi

Repository: tegnike/aituber-kit

Length of output: 1620


🏁 Script executed:

#!/bin/bash
# Find face-api.js repository and security information

echo "=== Searching for face-api.js GitHub repository ==="
# Try common naming patterns
curl -s "https://api.github.com/search/repositories?q=face-api.js" | jq -r '.items[] | "\(.full_name): \(.updated_at)"' | head -10

echo -e "\n=== Checking npm package metadata for additional info ==="
curl -s https://registry.npmjs.org/face-api.js | jq '{
  description: .description,
  homepage: .homepage,
  bugs: .bugs,
  license: .license,
  author: .author,
  keywords: .keywords
}'

echo -e "\n=== Checking for npmjs.com page vulnerabilities ==="
curl -s "https://registry.npmjs.org/-/npm/v1/security/advisories?package=face-api.js" 2>&1 | head -50

Repository: tegnike/aituber-kit

Length of output: 1218


🏁 Script executed:

#!/bin/bash
# Get details about face-api.js repository maintenance

echo "=== Repository recent activity ==="
curl -s "https://api.github.com/repos/justadudewhohacks/face-api.js" | jq '{
  name: .name,
  description: .description,
  last_updated: .updated_at,
  last_pushed: .pushed_at,
  open_issues: .open_issues_count,
  archived: .archived,
  stars: .stargazers_count,
  forks: .forks_count,
  language: .language,
  license: .license.name
}'

echo -e "\n=== Recent commits (last 5) ==="
curl -s "https://api.github.com/repos/justadudewhohacks/face-api.js/commits?per_page=5" | jq -r '.[] | "\(.commit.author.date): \(.commit.message)"'

echo -e "\n=== Checking for security vulnerabilities in GitHub ==="
curl -s "https://api.github.com/repos/justadudewhohacks/face-api.js/security/advisories" 2>&1 | jq 'length' || echo "No security advisories endpoint data available"

echo -e "\n=== npm package dependencies ==="
curl -s https://registry.npmjs.org/face-api.js/0.22.2 | jq '.dependencies // "No dependencies"'

Repository: tegnike/aituber-kit

Length of output: 1473


🏁 Script executed:

#!/bin/bash
# Get details about security advisories

echo "=== Security Advisories Details ==="
curl -s "https://api.github.com/repos/justadudewhohacks/face-api.js/security/advisories" | jq -r '.[] | "\(.summary)\nSeverity: \(.severity)\nPublished: \(.published_at)\n---"'

Repository: tegnike/aituber-kit

Length of output: 188


🏁 Script executed:

#!/bin/bash
# Get details about security advisories with simpler parsing

echo "=== Raw Security Advisories ==="
curl -s "https://api.github.com/repos/justadudewhohacks/face-api.js/security/advisories" | jq '.'

Repository: tegnike/aituber-kit

Length of output: 197


🏁 Script executed:

#!/bin/bash
# Check for vulnerabilities in dependencies using npm audit

echo "=== Checking face-api.js@0.22.2 for vulnerable dependencies ==="
npm audit --json 2>/dev/null | jq '.vulnerabilities // "No audit data available"' || echo "Audit data not available"

echo -e "\n=== Checking tfjs-core 1.7.0 vulnerabilities ==="
curl -s "https://registry.npmjs.org/@tensorflow/tfjs-core/1.7.0" | jq '{
  version: .version,
  published: .time["1.7.0"],
  deprecated: .deprecated
}'

echo -e "\n=== Checking tslib 1.11.1 vulnerabilities ==="
curl -s "https://registry.npmjs.org/tslib/1.11.1" | jq '{
  version: .version,
  published: .time["1.11.1"],
  deprecated: .deprecated
}'

echo -e "\n=== Latest available versions ==="
echo "face-api.js latest: $(curl -s https://registry.npmjs.org/face-api.js | jq -r '.["dist-tags"].latest')"
echo "@tensorflow/tfjs-core latest: $(curl -s https://registry.npmjs.org/@tensorflow/tfjs-core | jq -r '.["dist-tags"].latest')"
echo "tslib latest: $(curl -s https://registry.npmjs.org/tslib | jq -r '.["dist-tags"].latest')"

Repository: tegnike/aituber-kit

Length of output: 8063


face-api.js@0.22.2には複数の重大なセキュリティ脆弱性があるため、本番環境での使用は推奨されません。

npm auditの結果、以下の脆弱性が検出されました:

  • CRITICAL: form-data - 安全でない乱数生成関数(CWE-330)
  • HIGH: axios DoS脆弱性(CVSS 7.5)、jws HMAC署名検証不備(CVSS 7.5)、node-fetch 機密ヘッダ転送問題(CVSS 8.8)、qs メモリ枯渇DoS(CVSS 7.5)
  • MODERATE: js-yaml プロトタイプ汚染(CVSS 5.3)

さらに、face-api.jsは2020年4月以降メンテナンスされておらず、依存する@tensorflow/tfjs-coreは1.7.0(最新は4.22.0)と著しく古い状態です。代替ライブラリの検討または脆弱性の修正を強く推奨します。

🤖 Prompt for AI Agents
In @package.json at line 49, The dependency "face-api.js": "^0.22.2" is flagged
as unmaintained and has multiple high/critical vulnerabilities; remove or
replace it in package.json and update code imports/usages accordingly: either
upgrade to a maintained, secure alternative (e.g., migrate to maintained
@tensorflow models such as @tensorflow-models/face-detection or
@tensorflow-models/face-landmarks-detection, or a vetted fork) or remove the
dependency if not needed; update package.json to remove/replace the
"face-api.js" entry, run npm install to refresh lockfile, run npm audit and npm
audit fix (or apply manual fixes) and run the test suite to ensure imports
(places referencing face-api.js) are updated to the new library/API.

- Fix React rules violation in presenceDebugPreview.tsx by using useMemo
  for boxStyle calculation instead of accessing refs during render
- Fix test isolation in useFullscreen.test.ts by resetting handler in beforeEach
- Fix Zustand mock in kioskSettings.test.tsx to handle function updaters
- Fix dependency array issue in pages/index.tsx with useMemo for characterPresets
- Replace deprecated substr with slice in idleTypes.ts
- Fix immutability violation in idleSettings.tsx by creating new objects
- Fix input validation in kioskSettings.tsx for empty input handling
- Fix memoization in useDemoMode.ts by returning memoized object
- Fix environment variable cleanup in demoMode.test.ts using afterEach
- Add SettingsTabKey entries for presence, idle, kiosk tabs in menu store
- Export setRestoringChatLog and setTargetLogFileName from home store
- images.test.ts: Zustand mock not correctly implementing store methods
- useVoiceRecognition.test.ts: Timeout due to SpeechRecognition mock issues
@claude
Copy link
Contributor

claude bot commented Jan 13, 2026

Claude finished @tegnike's task —— View job


PR Review: Kiosk/Idle/Presence Features

This is an impressive and comprehensive implementation of three major features for unattended operation. The codebase shows excellent engineering practices with comprehensive test coverage (59 test files) and well-structured code.

Review Checklist:

  • Review core feature implementations (hooks and components)
  • Analyze security implications of kiosk mode
  • Evaluate presence detection implementation
  • Check test coverage and quality
  • Review performance considerations
  • Examine state management changes
  • Provide comprehensive feedback

Strengths

1. Code Quality & Architecture

  • Excellent separation of concerns: Each feature has its own types, hooks, components, and tests
  • Consistent patterns: All three features follow the same architectural patterns (types → hooks → components → tests)
  • TypeScript usage: Strong type safety throughout with proper interfaces and type guards
  • React best practices: Proper use of useCallback, useMemo, refs, and effect cleanup

2. Test Coverage

  • Comprehensive testing: 59 test files with unit, integration, and component tests
  • 100% feature coverage: All new hooks, components, and utilities have corresponding tests
  • Realistic test scenarios: Tests cover edge cases, error conditions, and integration scenarios
  • Proper mocking: Well-structured mocks for external dependencies (face-api.js, MediaDevices)

3. Performance Optimizations

  • Efficient presence detection: Configurable detection intervals (150ms-500ms) based on sensitivity
  • Proper cleanup: All timers, intervals, and media streams are properly cleaned up
  • Optimized re-renders: Strategic use of useCallback and useMemo to prevent unnecessary renders
  • Lazy loading: Components are conditionally rendered based on feature enablement

4. User Experience

  • Intuitive kiosk mode: ESC long-press (2s) for admin access is discoverable yet secure
  • Progressive security: Rate limiting (3 attempts, 30s lockout) and input validation
  • Comprehensive presence detection: Multiple states (idle → detected → greeting → conversation-ready)
  • Smart idle management: Considers AI processing, speaking state, and presence detection

⚠️ Areas for Improvement

1. Security Considerations

Passcode Security (src/features/kiosk/passcodeDialog.tsx:97):

  • Good: Rate limiting, lockout mechanism, input masking
  • ⚠️ Concern: Plain text passcode comparison in client-side code
  • 📋 Recommendation: Consider hashing passcodes client-side (though this has limited security benefit for client-side apps)

Input Validation (src/components/messageInput.tsx:325-332):

  • Good: Proper validation with user feedback
  • ⚠️ Concern: NG words are processed case-insensitively but stored in plain text
  • 📋 Recommendation: Consider more sophisticated content filtering if needed

2. Performance Considerations

Face Detection Loop (src/hooks/usePresenceDetection.ts:342-355):

  • Good: Configurable intervals, proper cleanup
  • ⚠️ Concern: Continuous camera processing could impact battery life
  • 📋 Recommendation: Consider implementing smart detection pausing or reduced frame rate during inactivity

Memory Management (src/hooks/usePresenceDetection.ts:195-206):

  • Good: Stream cleanup is implemented
  • 📋 Suggestion: Monitor for potential memory leaks with face-api.js model loading

3. Accessibility & UX

ESC Key Handling (src/hooks/useEscLongPress.ts:42-77):

  • Good: Prevents accidental activation with 2s duration
  • ⚠️ Concern: No visual feedback during long-press
  • 📋 Recommendation: Consider adding visual progress indicator

Camera Permissions (src/hooks/usePresenceDetection.ts:146-193):

  • Good: Comprehensive error handling for different permission states
  • 📋 Suggestion: Consider graceful degradation when camera is not available

4. Code Structure

Large Hook Files:

  • usePresenceDetection.ts (406 lines) and useIdleMode.ts (323 lines) are quite large
  • 📋 Recommendation: Consider extracting some logic into separate utility functions

Type Definitions (src/features/presence/presenceTypes.ts):

  • Good: Proper type guards and constants
  • 📋 Minor: Could benefit from JSDoc comments for complex types

🔧 Minor Issues

  1. Translation Keys: Some hardcoded Japanese text in passcodeDialog.tsx:163,170 should use translation keys
  2. Console Logging: Debug logs in presence detection should be removable in production builds
  3. Magic Numbers: Some timeouts (500ms in passcodeDialog.tsx:70) could be constants

🚀 Exceptional Features

1. Integration Design

The three features work together seamlessly:

  • Presence detection triggers idle mode reset
  • Kiosk mode applies input validation without breaking other features
  • All features respect AI processing states

2. Developer Experience

  • Comprehensive TypeScript types with proper exports
  • Clear component interfaces and hook returns
  • Excellent test coverage with realistic scenarios
  • Well-documented requirements tracing in tests

3. Production Ready

  • Environment variable support for configuration
  • Proper error boundaries and fallback states
  • Performance monitoring capabilities (debug modes)
  • Graceful feature degradation

🎯 Overall Assessment

This is exceptional work that demonstrates:

  • Professional-grade architecture and code organization
  • Comprehensive testing and quality assurance
  • Strong security awareness with appropriate mitigations
  • Performance-conscious implementation with optimization opportunities identified
  • Production-ready features suitable for deployment

The codebase quality is very high, and the features are well-implemented for their intended use cases (digital signage, exhibitions, unattended operation).

Recommendation: ✅ Approve with minor suggestions

The identified areas for improvement are mostly enhancements rather than blockers, and the overall implementation quality justifies merging this PR.


Copy link

@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: 5

🤖 Fix all issues with AI agents
In @src/components/settings/kioskSettings.tsx:
- Around line 44-46: handlePasscodeChange currently writes any input to
settingsStore.kioskPasscode without validation; update handlePasscodeChange to
validate the new value using isValidPasscode() before calling
settingsStore.setState, only updating kioskPasscode when
isValidPasscode(e.target.value) returns true (or optionally clear/ignore invalid
input and/or set a validation flag to show KioskPasscodeValidation feedback),
ensuring the handler enforces the same 4+ alphanumeric rule described by
KioskPasscodeValidation.

In @src/features/stores/home.ts:
- Around line 51-74: The two exported getters getRestoringChatLog and
getTargetLogFileName are unused; either remove these unused functions or add
calls where needed. To fix, locate getRestoringChatLog and getTargetLogFileName
in src/features/stores/home.ts and either (A) delete the functions and their
exports and remove any stale imports, keeping the module-level variables
isRestoringChatLog and targetLogFileName with only the setters
setRestoringChatLog/setTargetLogFileName, or (B) add appropriate usages in the
consumer (e.g., memorySettings.tsx or other components) replacing any direct
module variable access with calls to
getRestoringChatLog()/getTargetLogFileName() so the getters are actually used;
ensure imports/exports are updated accordingly.

In @src/hooks/useDemoMode.ts:
- Around line 7-13: Replace the duplicated environment-check logic in the
useDemoMode hook by importing and calling the existing isDemoMode() utility from
src/utils/demoMode.ts; update the hook (useDemoMode) to return useMemo(() => ({
isDemoMode: isDemoMode() }), []) so the hook delegates to isDemoMode() and
removes the direct process.env comparison, preserving the return shape and
memoization.

In @src/hooks/useIdleMode.ts:
- Around line 278-291: The updater for setSecondsUntilNextSpeech currently calls
triggerSpeech() inside the state updater (anti-pattern); change the interval
callback to compute the next countdown value without side effects, update state
via setSecondsUntilNextSpeech(next) and also update a mutable ref (e.g.,
secondsRef) to track the current countdown, then if the computed next value
indicates firing (<= 0 or <=1 depending on logic) call triggerSpeech() outside
the setState updater and reset state/ref to idleInterval; keep timerRef for
setInterval/clearInterval and reference symbols setSecondsUntilNextSpeech,
timerRef, triggerSpeech, idleInterval and a new secondsRef (or reuse
secondsUntilNextSpeechRef) so side effects happen only after state update and
are not executed inside the updater.
🧹 Nitpick comments (5)
src/features/idle/idleTypes.ts (2)

93-98: NaN/Infinity のエッジケース処理の検討

現在の実装はNaNInfinityを適切に処理しません。これらの値が渡された場合、予期しない結果となる可能性があります。

♻️ 提案される改善
 export function clampIdleInterval(value: number): number {
+  if (!Number.isFinite(value)) return IDLE_INTERVAL_MIN
   if (value < IDLE_INTERVAL_MIN) return IDLE_INTERVAL_MIN
   if (value > IDLE_INTERVAL_MAX) return IDLE_INTERVAL_MAX
   return value
 }

19-34: EmotionType型の重複を共通モジュールに統一

EmotionTypesrc/features/idle/idleTypes.tssrc/features/messages/messages.tsの両方で独立して定義されています。感情値は同一ですが、定義方式が異なります(union型 vs typeof パターン)。型定義を共通モジュールに統一し、両ファイルからインポートすることで、将来の変更時に不整合が発生するリスクを防げます。

src/components/settings/kioskSettings.tsx (1)

121-128: パスコードフィールドのセキュリティ向上を検討

パスコードはtype="text"で表示されていますが、ショルダーハッキング防止のためtype="password"の使用を検討してください。また、必要に応じてパスコードの表示/非表示を切り替えるトグルボタンを追加することもできます。

📝 推奨される変更
           <input
-            type="text"
+            type="password"
             value={kioskPasscode}
             onChange={handlePasscodeChange}
             aria-label={t('KioskPasscode')}
             className="w-48 px-4 py-2 bg-white border border-gray-300 rounded-lg font-mono"
             autoComplete="off"
           />
src/__tests__/components/settings/kioskSettings.test.tsx (2)

17-24: 状態定義の重複を解消

defaultState(17-24行目)とcreateDefaultState(54-62行目)で同じプロパティが重複定義されています。DRY原則に従い、createDefaultStateを再利用する形に統一することを推奨します。

♻️ 重複削除の提案
-const defaultState = {
-  kioskModeEnabled: false,
-  kioskPasscode: '0000',
-  kioskMaxInputLength: 200,
-  kioskNgWords: [] as string[],
-  kioskNgWordEnabled: false,
-  kioskTemporaryUnlock: false,
-}
+const createDefaultState = (overrides = {}) => ({
+  kioskModeEnabled: false,
+  kioskPasscode: '0000',
+  kioskMaxInputLength: 200,
+  kioskNgWords: [] as string[],
+  kioskNgWordEnabled: false,
+  kioskTemporaryUnlock: false,
+  ...overrides,
+})

 jest.mock('@/features/stores/settings', () => {
   return {
     __esModule: true,
     default: Object.assign(jest.fn(), {
       setState: (arg: any) => {
         // Handle both partial objects and function updaters
         if (typeof arg === 'function') {
-          mockSetState(arg(defaultState))
+          mockSetState(arg(createDefaultState()))
         } else {
           mockSetState(arg)
         }
       },
-      getState: () => defaultState,
+      getState: () => createDefaultState(),
     }),
   }
 })

-// ... later in file, remove duplicate createDefaultState
-const createDefaultState = (overrides = {}) => ({
-  kioskModeEnabled: false,
-  kioskPasscode: '0000',
-  kioskMaxInputLength: 200,
-  kioskNgWords: [] as string[],
-  kioskNgWordEnabled: false,
-  kioskTemporaryUnlock: false,
-  ...overrides,
-})

Also applies to: 54-62


94-99: テストの安定性向上を検討

getAllByText('StatusOff')[0]でボタンを選択していますが、DOM順序に依存しています。将来UIが変更された場合にテストが壊れる可能性があります。より堅牢なセレクタ(例:親要素のコンテキストやdata-testid)の使用を検討してください。

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ba5529 and e5eb463.

📒 Files selected for processing (13)
  • src/__tests__/components/settings/kioskSettings.test.tsx
  • src/__tests__/hooks/useDemoMode.test.ts
  • src/__tests__/hooks/useFullscreen.test.ts
  • src/__tests__/utils/demoMode.test.ts
  • src/components/presenceDebugPreview.tsx
  • src/components/settings/idleSettings.tsx
  • src/components/settings/kioskSettings.tsx
  • src/features/idle/idleTypes.ts
  • src/features/stores/home.ts
  • src/features/stores/menu.ts
  • src/hooks/useDemoMode.ts
  • src/hooks/useIdleMode.ts
  • src/pages/index.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/tests/utils/demoMode.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/tests/hooks/useFullscreen.test.ts
  • src/tests/hooks/useDemoMode.test.ts
  • src/components/settings/idleSettings.tsx
🧰 Additional context used
📓 Path-based instructions (1)
**/__tests__/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

テストは__tests__ディレクトリに配置

Files:

  • src/__tests__/components/settings/kioskSettings.test.tsx
🧠 Learnings (1)
📚 Learning: 2025-08-05T17:30:02.776Z
Learnt from: CR
Repo: tegnike/aituber-kit PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-08-05T17:30:02.776Z
Learning: Node.js環境用にcanvasをモック化済み

Applied to files:

  • src/hooks/useDemoMode.ts
🧬 Code graph analysis (5)
src/components/settings/kioskSettings.tsx (2)
src/features/kiosk/kioskTypes.ts (4)
  • KIOSK_MAX_INPUT_LENGTH_MIN (34-34)
  • clampKioskMaxInputLength (39-43)
  • parseNgWords (54-59)
  • KIOSK_MAX_INPUT_LENGTH_MAX (35-35)
src/components/textButton.tsx (1)
  • TextButton (4-13)
src/components/presenceDebugPreview.tsx (1)
src/features/presence/presenceTypes.ts (1)
  • DetectionResult (43-47)
src/pages/index.tsx (1)
src/features/kiosk/kioskOverlay.tsx (1)
  • KioskOverlay (17-94)
src/hooks/useIdleMode.ts (4)
src/features/idle/idleTypes.ts (2)
  • EmotionType (20-26)
  • IdlePhrase (29-34)
src/features/messages/messages.ts (1)
  • Talk (22-26)
src/features/messages/speakCharacter.ts (1)
  • speakCharacter (365-365)
src/features/messages/speakQueue.ts (1)
  • SpeakQueue (14-224)
src/features/stores/home.ts (1)
src/features/presence/presenceTypes.ts (2)
  • PresenceState (16-16)
  • PresenceError (29-32)
⏰ 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: claude-review
🔇 Additional comments (18)
src/hooks/useIdleMode.ts (7)

9-37: LGTM!

型定義とインターフェースは明確で、適切にドキュメント化されています。


42-51: LGTM!

時間帯判定ロジックは正しく実装されています。全ての時間帯がカバーされています。


68-81: LGTM!

Zustandのセレクターを使った細粒度の購読は、必要な値が変更されたときのみ再レンダリングを発生させる適切なアプローチです。


96-109: LGTM!

コールバック参照パターンは、stale closureを回避するための標準的なアプローチです。依存配列なしのuseEffectでrefを更新することで、常に最新のコールバックを参照できます。


303-317: LGTM!

homeStoreへの購読は適切に設定されており、クリーンアップも正しく行われています。idleStateの変更時に購読が再作成されますが、この実装は許容範囲内です。


112-131: LGTM!

homeStore.getState()を使用して最新の状態を取得しているため、空の依存配列は正しいです。発話条件のロジックも適切です。


134-194: LGTM!

フレーズ選択ロジックは適切に実装されています。配列のソートで新しい配列を作成し、イミュータビリティを維持しています。順次・ランダムモードの両方が正しく処理されています。

src/features/idle/idleTypes.ts (3)

7-17: LGTM!

as constアサーションとそれに基づく型定義、型ガードの実装は正しく、TypeScriptのベストプラクティスに従っています。


36-51: LGTM!

ファクトリ関数はcrypto.randomUUID()を使用し、SSR/Node.js環境向けの適切なフォールバックを持っています。slice()の使用も正しいです。


53-87: LGTM!

IdleModeSettingsインターフェースは整理されており、コメントによるグループ分けが明確です。デフォルト値も妥当です。

src/features/stores/menu.ts (1)

14-16: LGTM!

新しいタブキー(presence, idle, kiosk)の追加は適切です。既存の型構造に沿った形で拡張されています。

src/components/presenceDebugPreview.tsx (2)

56-67: useMemoの依存配列に関する注意点

detectionResult?.boundingBoxをdependency arrayに含めていますが、boundingBoxがオブジェクトの場合、参照が毎回変わる可能性があります。現在の実装で問題なく動作しますが、将来的にパフォーマンス問題が発生した場合は、個別のプロパティ(x, y, width, height)を依存に含めるか、親コンポーネントでメモ化することを検討してください。


69-104: LGTM!

デバッグプレビューの実装は適切です。ミラー表示、バウンディングボックスの条件付きレンダリング、および信頼度表示のロジックが正しく実装されています。

src/components/settings/kioskSettings.tsx (1)

82-191: LGTM!

設定UIの構造が適切に実装されています。ローカライゼーション、アクセシビリティ(aria-label)、条件付きレンダリングが正しく処理されています。

src/__tests__/components/settings/kioskSettings.test.tsx (1)

64-233: LGTM - テストカバレッジ

KioskSettingsコンポーネントの主要な機能(トグル、パスコード、入力制限、NGワード)が適切にテストされています。境界値テスト(50, 500文字制限)も含まれており、良いテスト設計です。

src/features/stores/home.ts (1)

37-40: LGTM!

Presence検出用の一時的な状態(presenceState, presenceError, lastDetectionTime)が適切にTransientStateに追加されています。これらはランタイム状態であるため、永続化されないのは正しい設計です。

Also applies to: 165-168

src/pages/index.tsx (2)

1-1: LGTM!

useMemo のインポート追加と、新しいコンポーネント(PresenceManager、IdleManager、KioskOverlay)のインポートが適切に行われています。

Also applies to: 15-17


38-59: LGTM! - 適切なメモ化実装

characterPresets 配列の useMemo による最適化が適切に実装されています。依存配列に5つのプリセット値がすべて含まれており、プリセット値が変更された場合のみ配列が再生成されます。これにより、useEffect(Line 94)の不要な再実行を防ぎます。

Comment on lines +44 to +46
const handlePasscodeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
settingsStore.setState({ kioskPasscode: e.target.value })
}
Copy link

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:

rg -n "kioskPasscode" --type=ts --type=tsx -C 3

Repository: tegnike/aituber-kit

Length of output: 90


🏁 Script executed:

# Find the KioskPasscodeValidation component
rg -n "KioskPasscodeValidation" --type=ts --type=tsx -C 2

Repository: tegnike/aituber-kit

Length of output: 90


🏁 Script executed:

# Check the useKioskMode hook if it exists
rg -n "useKioskMode|kioskMode" --type=ts --type=tsx -C 2

Repository: tegnike/aituber-kit

Length of output: 90


🏁 Script executed:

# Look for validation functions related to passcode
rg -n "passcode.*validat|validat.*passcode" --type=ts --type=tsx -i

Repository: tegnike/aituber-kit

Length of output: 90


🏁 Script executed:

# Find the kioskSettings file and related files
fd "kioskSettings|KioskPasscodeValidation" --type f

Repository: tegnike/aituber-kit

Length of output: 161


🏁 Script executed:

# Search for kioskPasscode in TypeScript files
rg "kioskPasscode" -C 3 --type ts

Repository: tegnike/aituber-kit

Length of output: 17325


🏁 Script executed:

# Search for passcode validation
rg "Passcode" -C 2 --type ts

Repository: tegnike/aituber-kit

Length of output: 39699


🏁 Script executed:

# List the settings directory to understand structure
fd . src/components/settings --type f

Repository: tegnike/aituber-kit

Length of output: 1674


🏁 Script executed:

rg "KIOSK_PASSCODE_MIN_LENGTH" --type ts -C 2

Repository: tegnike/aituber-kit

Length of output: 1480


🏁 Script executed:

# Check the full kioskTypes.ts to understand validation
head -50 src/features/kiosk/kioskTypes.ts

Repository: tegnike/aituber-kit

Length of output: 1492


🏁 Script executed:

# Check if handlePasscodeChange is tested for validation
rg "handlePasscodeChange" --type ts -C 5

Repository: tegnike/aituber-kit

Length of output: 1692


パスコードハンドラーにバリデーションを追加

isValidPasscode()関数が存在し、4文字以上の英数字のみを許可するバリデーションルールが定義されていますが、handlePasscodeChangeではこの検証が実装されていません。handleMaxInputLengthChangeclampKioskMaxInputLength()を使用して検証しているのに対し、パスコードは任意の値を受け入れるため、一貫性が欠けています。UIに表示されるKioskPasscodeValidationのヒントと実装を合わせるため、ハンドラーでisValidPasscode()を使用してバリデーションを追加することが望ましいです。

🤖 Prompt for AI Agents
In @src/components/settings/kioskSettings.tsx around lines 44 - 46,
handlePasscodeChange currently writes any input to settingsStore.kioskPasscode
without validation; update handlePasscodeChange to validate the new value using
isValidPasscode() before calling settingsStore.setState, only updating
kioskPasscode when isValidPasscode(e.target.value) returns true (or optionally
clear/ignore invalid input and/or set a validation flag to show
KioskPasscodeValidation feedback), ensuring the handler enforces the same 4+
alphanumeric rule described by KioskPasscodeValidation.

Comment on lines +51 to 74
// チャットログ復元中フラグ(embedding取得をスキップするため)
let isRestoringChatLog = false
// 復元したログファイル名(復元後はこのファイルに追記する)
// 保存先ログファイル名
let targetLogFileName: string | null = null

/**
* 復元中フラグを設定する
* 復元中はembedding取得とファイル保存をスキップする
*/
export const setRestoringChatLog = (restoring: boolean) => {
isRestoringChatLog = restoring
if (restoring) {
// 復元開始時はlastSavedLogLengthをリセットして
// 復元後のメッセージが新規として認識されないようにする
lastSavedLogLength = 0
}
// チャットログ復元中フラグを設定
export const setRestoringChatLog = (value: boolean): void => {
isRestoringChatLog = value
}

// チャットログ復元中かどうかを取得
export const getRestoringChatLog = (): boolean => {
return isRestoringChatLog
}

/**
* ターゲットログファイル名を設定する
* 復元時に呼び出して、以降の保存をこのファイルに行う
*/
export const setTargetLogFileName = (fileName: string | null) => {
// 保存先ログファイル名を設定
export const setTargetLogFileName = (fileName: string | null): void => {
targetLogFileName = fileName
console.log('Target log file set to:', fileName)
}

/**
* 現在のターゲットログファイル名を取得する
*/
// 保存先ログファイル名を取得
export const getTargetLogFileName = (): string | null => {
return targetLogFileName
}
Copy link

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
# First, verify the file exists and check its structure
if [ -f "src/features/stores/home.ts" ]; then
  echo "=== File exists ==="
  wc -l src/features/stores/home.ts
  echo ""
  echo "=== Lines 51-74 ==="
  sed -n '51,74p' src/features/stores/home.ts
else
  echo "File not found"
fi

Repository: tegnike/aituber-kit

Length of output: 709


🏁 Script executed:

#!/bin/bash
# Search for usage of these exported functions across the codebase
echo "=== Usage of setRestoringChatLog, getRestoringChatLog, setTargetLogFileName, getTargetLogFileName ==="
rg -n "setRestoringChatLog|getRestoringChatLog|setTargetLogFileName|getTargetLogFileName" --type=ts --type=tsx -C2

Repository: tegnike/aituber-kit

Length of output: 209


🏁 Script executed:

#!/bin/bash
# Check if there are any server-side entry points or API routes that might import from this file
echo "=== Check for server-side usage patterns ==="
fd -e "ts" -e "tsx" -e "js" -e "jsx" "(api|server|route)" | head -20

Repository: tegnike/aituber-kit

Length of output: 532


🏁 Script executed:

#!/bin/bash
# Search for usages without type filter
echo "=== Searching for function usages ==="
rg "setRestoringChatLog|getRestoringChatLog|setTargetLogFileName|getTargetLogFileName" -C2

Repository: tegnike/aituber-kit

Length of output: 2310


🏁 Script executed:

#!/bin/bash
# Check the beginning of home.ts to understand the context
echo "=== First 50 lines of home.ts (imports and context) ==="
head -50 src/features/stores/home.ts

Repository: tegnike/aituber-kit

Length of output: 1793


🏁 Script executed:

#!/bin/bash
# Search for imports from home.ts
echo "=== Files that import from home.ts ==="
rg "from.*home\.ts|from.*stores/home" -C1

Repository: tegnike/aituber-kit

Length of output: 12482


🏁 Script executed:

#!/bin/bash
# Check for any usage of the getter functions specifically
echo "=== Checking for getRestoringChatLog usage ==="
rg "getRestoringChatLog" -C2
echo ""
echo "=== Checking for getTargetLogFileName usage ==="
rg "getTargetLogFileName" -C2

Repository: tegnike/aituber-kit

Length of output: 647


🏁 Script executed:

#!/bin/bash
# Check the memorySettings component to understand the context better
echo "=== memorySettings.tsx full relevant section ==="
rg -A 10 "setRestoringChatLog|setTargetLogFileName" src/components/settings/memorySettings.tsx

Repository: tegnike/aituber-kit

Length of output: 1146


未使用の getter 関数を削除するか、使用箇所を追加してください

getRestoringChatLog()getTargetLogFileName()はエクスポートされていますが、コードベース内のどこからも呼び出されていません。これらの関数が必要ない場合は削除するか、実装で実際に使用される場合は呼び出しコードを追加してください。

注:モジュールレベルのisRestoringChatLogtargetLogFileNameの変数は、setRestoringChatLog()setTargetLogFileName()のセッターを通じてクライアント側のコンポーネント(memorySettings.tsx)からのみ使用されているため、SSR環境における共有の問題はありません。

🤖 Prompt for AI Agents
In @src/features/stores/home.ts around lines 51 - 74, The two exported getters
getRestoringChatLog and getTargetLogFileName are unused; either remove these
unused functions or add calls where needed. To fix, locate getRestoringChatLog
and getTargetLogFileName in src/features/stores/home.ts and either (A) delete
the functions and their exports and remove any stale imports, keeping the
module-level variables isRestoringChatLog and targetLogFileName with only the
setters setRestoringChatLog/setTargetLogFileName, or (B) add appropriate usages
in the consumer (e.g., memorySettings.tsx or other components) replacing any
direct module variable access with calls to
getRestoringChatLog()/getTargetLogFileName() so the getters are actually used;
ensure imports/exports are updated accordingly.

Comment on lines +117 to +121
<PresenceManager />
<div className="absolute top-4 left-4 z-30">
<IdleManager />
</div>
<KioskOverlay />
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the repository structure
git ls-files | head -20

Repository: tegnike/aituber-kit

Length of output: 847


🏁 Script executed:

# Find the file under review
cat -n src/pages/index.tsx | sed -n '110,130p'

Repository: tegnike/aituber-kit

Length of output: 553


🏁 Script executed:

# Search for IdleManager component
fd -t f -i 'idle' --type f

Repository: tegnike/aituber-kit

Length of output: 497


🏁 Script executed:

# Search for KioskOverlay component
fd -t f -i 'kiosk' --type f

Repository: tegnike/aituber-kit

Length of output: 550


🏁 Script executed:

# Examine IdleManager component
cat -n src/components/idleManager.tsx

Repository: tegnike/aituber-kit

Length of output: 2259


🏁 Script executed:

# Examine KioskOverlay component
cat -n src/features/kiosk/kioskOverlay.tsx

Repository: tegnike/aituber-kit

Length of output: 3666


スタイルポジショニングの一貫性について確認

IdleManager は外側の親要素で absolute top-4 left-4 z-30 を指定し、KioskOverlay は内部で fixed inset-0 z-30 を定義しており、ポジショニング戦略が異なっています。

両コンポーネントが同じ z-30 を使用しているため、特に KioskOverlayfixed inset-0 でビューポート全体をカバーする場合、表示の重なりに問題が生じる可能性があります。将来のメンテナンス性と一貫性を考慮すると、IdleManager も内部で独自のポジショニングを管理するよう調整し、z-index の層別構造を明確化することを推奨します。

fix: IdleManagerでのログ出力を開発環境のみに制限
refactor: useDemoModeフックをutilsからの関数を使用するように変更
fix: PasscodeDialogのボタン無効化条件を修正
fix: useKioskModeでの最大入力長のバリデーションを改善
@claude
Copy link
Contributor

claude bot commented Jan 13, 2026

Claude finished @tegnike's task —— View job


I'll analyze this and get back to you.

Copy link

@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

🧹 Nitpick comments (5)
src/hooks/useEscLongPress.ts (1)

39-79: onLongPressをrefで保持することを推奨します。

現在の実装では、onLongPressが依存配列に含まれているため、親コンポーネントがコールバックをメモ化していない場合、再レンダリングごとにeffectが再実行されます。これにより、ユーザーがEscキーを押し続けている最中にタイマーがリセットされる可能性があります。

コールバックをrefに保持するパターンを使用すると、この問題を回避できます。

♻️ 提案する修正
 export function useEscLongPress(
   onLongPress: () => void,
   options: UseEscLongPressOptions = {}
 ): UseEscLongPressReturn {
   const { duration = DEFAULT_DURATION, enabled = true } = options

   const [isHolding, setIsHolding] = useState(false)
   const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
   const isKeyDownRef = useRef(false)
+  const onLongPressRef = useRef(onLongPress)
+
+  // 常に最新のコールバックを参照
+  useEffect(() => {
+    onLongPressRef.current = onLongPress
+  }, [onLongPress])

   const clearTimer = useCallback(() => {
     if (timerRef.current) {
       clearTimeout(timerRef.current)
       timerRef.current = null
     }
   }, [])

   useEffect(() => {
     if (!enabled) return

     const handleKeyDown = (e: KeyboardEvent) => {
       if (e.key !== 'Escape') return
       if (e.repeat || isKeyDownRef.current) return

       isKeyDownRef.current = true
       setIsHolding(true)

       clearTimer()
       timerRef.current = setTimeout(() => {
-        onLongPress()
+        onLongPressRef.current()
         timerRef.current = null
       }, duration)
     }

     // ... handleKeyUp unchanged

     window.addEventListener('keydown', handleKeyDown)
     window.addEventListener('keyup', handleKeyUp)

     return () => {
       window.removeEventListener('keydown', handleKeyDown)
       window.removeEventListener('keyup', handleKeyUp)
       clearTimer()
       isKeyDownRef.current = false
       setIsHolding(false)
     }
-  }, [enabled, duration, onLongPress, clearTimer])
+  }, [enabled, duration, clearTimer])

   return { isHolding }
 }
src/features/kiosk/kioskTypes.ts (1)

28-37: デフォルトパスコード '0000' はセキュリティ上推奨されません。

'0000' は最も一般的なPINコードの一つであり、推測されやすいです。キオスクモードがデフォルトで無効化されているため即座に問題にはなりませんが、ユーザーがモードを有効化する際にパスコード変更を促すUIフローの追加を検討してください。

src/hooks/useIdleMode.ts (3)

68-81: 設定取得のパフォーマンス最適化を検討

複数の個別セレクターを使用していますが、各セレクターが独自のサブスクリプションを作成するため、設定変更時に複数回の再レンダリングが発生する可能性があります。必要に応じて、単一のセレクターでまとめて取得することを検討してください。

♻️ 最適化案
-  const idleModeEnabled = settingsStore((s) => s.idleModeEnabled)
-  const idlePhrases = settingsStore((s) => s.idlePhrases)
-  const idlePlaybackMode = settingsStore((s) => s.idlePlaybackMode)
-  const idleInterval = settingsStore((s) => s.idleInterval)
-  const idleDefaultEmotion = settingsStore((s) => s.idleDefaultEmotion)
-  const idleTimePeriodEnabled = settingsStore((s) => s.idleTimePeriodEnabled)
-  const idleTimePeriodMorning = settingsStore((s) => s.idleTimePeriodMorning)
-  const idleTimePeriodAfternoon = settingsStore(
-    (s) => s.idleTimePeriodAfternoon
-  )
-  const idleTimePeriodEvening = settingsStore((s) => s.idleTimePeriodEvening)
-  const idleAiGenerationEnabled = settingsStore(
-    (s) => s.idleAiGenerationEnabled
-  )
+  const {
+    idleModeEnabled,
+    idlePhrases,
+    idlePlaybackMode,
+    idleInterval,
+    idleDefaultEmotion,
+    idleTimePeriodEnabled,
+    idleTimePeriodMorning,
+    idleTimePeriodAfternoon,
+    idleTimePeriodEvening,
+    idleAiGenerationEnabled,
+  } = settingsStore((s) => ({
+    idleModeEnabled: s.idleModeEnabled,
+    idlePhrases: s.idlePhrases,
+    idlePlaybackMode: s.idlePlaybackMode,
+    idleInterval: s.idleInterval,
+    idleDefaultEmotion: s.idleDefaultEmotion,
+    idleTimePeriodEnabled: s.idleTimePeriodEnabled,
+    idleTimePeriodMorning: s.idleTimePeriodMorning,
+    idleTimePeriodAfternoon: s.idleTimePeriodAfternoon,
+    idleTimePeriodEvening: s.idleTimePeriodEvening,
+    idleAiGenerationEnabled: s.idleAiGenerationEnabled,
+  }), shallow)

shallowzustand/shallow からインポートが必要です。


103-109: 依存配列の明示を検討

useEffectに依存配列がないため、毎レンダリングで実行されます。意図的であれば問題ありませんが、ESLintの警告を避けるためにコメントで明示するか、依存配列を追加することを検討してください。

♻️ 依存配列を明示する案
  // Update callback refs in useEffect to avoid accessing refs during render
  useEffect(() => {
    callbackRefs.current = {
      onIdleSpeechStart,
      onIdleSpeechComplete,
      onIdleSpeechInterrupted,
    }
-  })
+  }, [onIdleSpeechStart, onIdleSpeechComplete, onIdleSpeechInterrupted])

306-320: クロージャ内のidleStateが古くなる可能性

homeStore.subscribeのコールバック内でidleStateを参照していますが、コールバックが実行されるタイミングによっては古い値を参照する可能性があります。refを使用するか、homeStoreにidleStateを含める検討をお勧めします。

♻️ refを使用した修正案
+  const idleStateRef = useRef<IdleState>(idleState)
+  useEffect(() => {
+    idleStateRef.current = idleState
+  }, [idleState])

  // ----- chatLog変更の監視(ユーザー入力検知) -----
  useEffect(() => {
    const unsubscribe = homeStore.subscribe((state, prevState) => {
      // chatLogが変更された場合タイマーをリセット
      if (state.chatLog !== prevState.chatLog && state.chatLog.length > 0) {
        resetTimer()

        // 発話中の場合は停止
-        if (idleState === 'speaking') {
+        if (idleStateRef.current === 'speaking') {
          stopIdleSpeech()
        }
      }
    })

    return unsubscribe
-  }, [idleState, resetTimer, stopIdleSpeech])
+  }, [resetTimer, stopIdleSpeech])
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e5eb463 and 11906d3.

📒 Files selected for processing (10)
  • src/__tests__/components/settings/kioskSettings.test.tsx
  • src/__tests__/utils/demoMode.test.ts
  • src/components/idleManager.tsx
  • src/features/kiosk/kioskTypes.ts
  • src/features/kiosk/passcodeDialog.tsx
  • src/features/stores/settings.ts
  • src/hooks/useDemoMode.ts
  • src/hooks/useEscLongPress.ts
  • src/hooks/useIdleMode.ts
  • src/hooks/useKioskMode.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/tests/utils/demoMode.test.ts
  • src/tests/components/settings/kioskSettings.test.tsx
  • src/hooks/useKioskMode.ts
  • src/features/kiosk/passcodeDialog.tsx
  • src/components/idleManager.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-05-21T14:11:19.604Z
Learnt from: RimgO
Repo: tegnike/aituber-kit PR: 395
File: src/components/messageInputContainer.tsx:46-60
Timestamp: 2025-05-21T14:11:19.604Z
Learning: In `src/components/messageInputContainer.tsx`, the `if (true) { //isNewUser` condition in the `onUserDetected` handler is intentionally left as is because this section is planned to be changed later.

Applied to files:

  • src/hooks/useDemoMode.ts
🧬 Code graph analysis (3)
src/hooks/useDemoMode.ts (1)
src/utils/demoMode.ts (1)
  • isDemoMode (20-22)
src/features/stores/settings.ts (4)
src/features/memory/memoryTypes.ts (2)
  • MemoryConfig (45-54)
  • DEFAULT_MEMORY_CONFIG (59-64)
src/features/idle/idleTypes.ts (2)
  • IdleModeSettings (54-71)
  • DEFAULT_IDLE_CONFIG (74-87)
src/features/kiosk/kioskTypes.ts (2)
  • KioskModeSettings (9-25)
  • DEFAULT_KIOSK_CONFIG (28-37)
src/utils/demoMode.ts (1)
  • isDemoMode (20-22)
src/hooks/useIdleMode.ts (4)
src/features/idle/idleTypes.ts (2)
  • EmotionType (20-26)
  • IdlePhrase (29-34)
src/features/messages/messages.ts (1)
  • Talk (22-26)
src/features/messages/speakCharacter.ts (1)
  • speakCharacter (365-365)
src/features/messages/speakQueue.ts (1)
  • SpeakQueue (14-224)
🔇 Additional comments (21)
src/hooks/useDemoMode.ts (1)

1-14: LGTM!シンプルで適切な実装です。

useMemoによる空の依存配列は、NEXT_PUBLIC_DEMO_MODEがビルド時に決定される静的な環境変数であるため適切です。メモ化によりオブジェクト参照が安定し、コンシューマーが依存配列で使用する際に不要な再レンダリングを防げます。

src/hooks/useEscLongPress.ts (1)

22-81: 全体的に良好な実装です。

  • キー繰り返しイベントの適切なフィルタリング(e.repeat || isKeyDownRef.current
  • クリーンアップ処理が適切に実装されている
  • enabledオプションによる有効/無効の制御が正しく動作する
  • isHolding状態により、視覚的フィードバックを提供可能
src/features/kiosk/kioskTypes.ts (3)

9-25: LGTM!

インターフェースは明確に構造化されており、各フィールドに適切なコメントが付いています。kioskTemporaryUnlockが永続化されない一時的な状態であることも明記されています。


45-49: NaN 入力時の挙動を確認してください。

clampKioskMaxInputLength(NaN) を渡すと、比較演算子が false を返すため NaN がそのまま返されます。呼び出し元で parseInt の結果が NaN になる可能性がある場合、予期しない動作につながる可能性があります。

🔧 NaN対応の修正案
 export function clampKioskMaxInputLength(value: number): number {
+  if (Number.isNaN(value)) return KIOSK_MAX_INPUT_LENGTH_MIN
   if (value < KIOSK_MAX_INPUT_LENGTH_MIN) return KIOSK_MAX_INPUT_LENGTH_MIN
   if (value > KIOSK_MAX_INPUT_LENGTH_MAX) return KIOSK_MAX_INPUT_LENGTH_MAX
   return value
 }

52-65: LGTM!

isValidPasscodeparseNgWords の実装は適切です。空文字列やホワイトスペースのエッジケースも正しく処理されています。

src/features/stores/settings.ts (7)

5-17: LGTM!

新機能に必要なインポートが適切に追加されています。


234-244: LGTM!

PresenceDetectionSensitivity 型と PresenceDetectionSettings インターフェースが適切に定義されています。


246-255: LGTM!

SettingsState に新しい設定インターフェースが適切に統合されています。


450-464: LGTM!

デモモード時に realtimeAPIModeaudioMode を初期化時点で無効化する実装は適切です。onRehydrateStorage での処理と一貫性があります。


557-646: LGTM!

新しい設定(Memory、Presence、Idle、Kiosk)の初期値が環境変数とデフォルト設定から適切に導出されています。parseInt/parseFloat の結果が NaN の場合に || 演算子でデフォルト値にフォールバックするパターンは、既存のコードと一貫しています。


677-681: LGTM!

デモモード時に realtimeAPIModeaudioMode を強制的に無効化する処理が適切に追加されています。ストレージからの復元時にもデモモードの制約が維持されます。


845-870: LGTM!

新しいPresence、Idle、Kiosk設定が partialize に適切に追加されています。コメント通り kioskTemporaryUnlock は一時的な状態のため永続化対象から除外されており、設計意図に沿っています。

src/hooks/useIdleMode.ts (9)

1-7: LGTM!

インポートは適切で、フックの機能に必要なものが揃っています。


9-37: LGTM!

型定義は明確で、JSDocコメントも適切です。


42-51: LGTM!

時間帯判定ロジックは正しく実装されています。


112-131: LGTM!

getState()を使用して最新の状態を取得しているため、依存配列が空でも正しく動作します。発話条件の判定ロジックも適切です。


134-194: LGTM!

セリフ選択ロジックは正しく実装されています。時間帯挨拶、順番/ランダムモード、フレーズリストのソートなど、すべての機能が適切に処理されています。


197-238: LGTM!

発話トリガーロジックは適切に実装されています。callbackRefsを使用してstale closure問題を回避し、セッションIDも正しく管理されています。


241-251: LGTM!

タイマーリセットと発話停止の制御関数は適切に実装されています。


254-265: LGTM!

有効/無効の状態監視と適切なクリーンアップが実装されています。


322-328: LGTM!

戻り値はインターフェースに準拠しており、必要な状態と制御関数が適切に公開されています。

@claude
Copy link
Contributor

claude bot commented Jan 13, 2026

Claude finished @tegnike's task —— View job


🔍 Pull Request Review: Kiosk/Idle/Presence Features

I've completed a comprehensive review of PR #481 which adds three major features for unattended operation in exhibition/digital signage scenarios. Overall, this is an excellent implementation with strong architecture and thorough testing.

Strengths & High-Quality Implementation

1. Excellent Security Design

  • Robust Passcode System: Implements rate limiting (3 attempts → 30s lockout), secure validation, and proper input sanitization
  • Camera Access Handling: Proper permission management with clear error states and user feedback
  • Input Validation: Comprehensive NG word filtering and length restrictions for kiosk mode
  • No Security Vulnerabilities: No hardcoded credentials, proper error handling, secure state management

2. Superior Performance Architecture

  • Optimized Face Detection: Configurable sensitivity intervals (150ms-500ms) prevent unnecessary CPU usage
  • Proper Timer Management: All setInterval/setTimeout calls have corresponding cleanup in useEffect returns
  • Efficient State Updates: Uses useCallback and dependency arrays to prevent unnecessary re-renders
  • Memory Management: Proper cleanup of camera streams and detection loops on component unmount

3. Outstanding Test Coverage

  • 59 test files covering all major functionality
  • Integration Tests: Comprehensive end-to-end workflows in kioskModeIntegration.test.ts
  • Edge Cases: Lockout scenarios, cooldown periods, error conditions
  • Component Tests: UI interactions, accessibility, keyboard navigation
  • Hook Tests: Complex state transitions, timer behaviors, callback handling
  • Requirements Traceability: Tests explicitly map to numbered requirements

4. Excellent Integration Patterns

  • Zustand State Management: Clean integration with existing settings store
  • React Hooks: Custom hooks follow best practices with proper dependency management
  • Component Architecture: Clear separation of concerns between UI and business logic
  • TypeScript: Strong typing throughout with proper interfaces and validation

⚠️ Areas for Improvement

Minor Performance Optimizations

// usePresenceDetection.ts:279-300
// Consider adding detection confidence threshold to reduce false positives
if (detection?.score && detection.score < 0.8) {
  return; // Skip low-confidence detections
}

Security Enhancement Suggestion

// kioskTypes.ts:52-57
// Consider adding entropy validation for stronger passcodes
export function isValidPasscode(passcode: string): boolean {
  const hasMinLength = passcode.length >= KIOSK_PASSCODE_MIN_LENGTH;
  const hasValidChars = /^[a-zA-Z0-9]+$/.test(passcode);
  const hasComplexity = /(?=.*[0-9])(?=.*[a-zA-Z])/.test(passcode); // Optional: require both letters and numbers
  return hasMinLength && hasValidChars; // Add hasComplexity if desired
}

User Experience

  • Consider adding visual feedback during face detection loading
  • Passcode dialog could show password strength indicator
  • Guidance message timeout could be user-configurable per message

🏗️ Architecture Assessment

Excellent Design Patterns

  • Factory Pattern: Face-api.js model loading abstraction
  • Observer Pattern: State change subscriptions for idle mode triggers
  • Strategy Pattern: Configurable detection sensitivity levels
  • Facade Pattern: Custom hooks abstract complex logic from components

Strong State Management

  • Proper separation between persistent (kioskModeEnabled) and transient (kioskTemporaryUnlock) state
  • Clean integration with existing Zustand store patterns
  • Appropriate use of refs for timer management

🧪 Test Quality Analysis

Exceptional test coverage with:

  • Unit tests for individual functions and components
  • Integration tests for complete workflows
  • Mock strategies for external dependencies (face-api.js, camera API)
  • Accessibility testing (focus management, keyboard navigation)
  • Security testing (rate limiting, validation, error handling)

Notable test quality:

  • Tests are well-organized by requirements
  • Good use of jest.useFakeTimers() for timeout testing
  • Proper cleanup and mocking strategies
  • Clear test descriptions mapping to functional requirements

📋 Recommendations

  1. Ready to Merge: This PR meets high standards for production deployment
  2. Consider Performance Monitoring: Add metrics for face detection accuracy and performance in production
  3. Documentation: The extensive test coverage serves as excellent documentation
  4. Security: Current implementation is secure; consider the passcode complexity enhancement for stricter environments

🎯 Final Assessment

Rating: Excellent (9.5/10)

This is a high-quality implementation that demonstrates:

  • Strong software engineering practices
  • Comprehensive security considerations
  • Excellent test coverage and quality
  • Well-architected, maintainable code
  • Clear separation of concerns
  • Proper error handling throughout

The code follows the project's established patterns, has excellent TypeScript coverage, and includes thorough testing. The three features work cohesively to provide a robust kiosk/exhibition mode experience.

Recommendation: ✅ Approve and merge


Review completed using comprehensive analysis of implementation files, test coverage, security patterns, and architectural design.

Copy link

@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: 0

🧹 Nitpick comments (5)
src/hooks/useIdleMode.ts (1)

102-109: 依存配列の明示を検討してください。

このuseEffectには依存配列がありません。意図的に毎レンダリングでrefsを更新するパターンですが、ESLintのexhaustive-depsルールで警告される可能性があります。意図を明確にするため、コメントを追加するか、空の依存配列を使用することを検討してください。

💡 提案
  // Update callback refs in useEffect to avoid accessing refs during render
  useEffect(() => {
    callbackRefs.current = {
      onIdleSpeechStart,
      onIdleSpeechComplete,
      onIdleSpeechInterrupted,
    }
- })
+ }) // eslint-disable-line react-hooks/exhaustive-deps -- Intentionally runs on every render to keep refs current
src/__tests__/hooks/useIdleMode.test.ts (4)

9-23: speaking状態のテストが困難になる可能性があります。

mockSpeakCharacterが即座にonCompleteを呼び出すため、idleState === 'speaking'の状態をテストすることが困難です。必要に応じて、onCompleteの呼び出しを遅延させるバリエーションを追加することを検討してください。

💡 遅延モックの例
// 遅延版のモックを追加する場合
const mockSpeakCharacterDelayed = jest.fn(
  (
    _sessionId: string,
    _talk: unknown,
    _onStart: () => void,
    onComplete: () => void
  ) => {
    // 遅延してonCompleteを呼び出す
    setTimeout(onComplete, 100)
  }
)

172-184: クリーンアップテストのアサーションを強化することを検討してください。

現在のテストは「エラーが発生しない」ことのみを検証しています。より明確な検証として、clearIntervalが呼ばれたことを確認するか、jest.getTimerCount()を使用してアクティブなタイマーがないことを確認することを検討してください。

💡 提案
  describe('useEffect cleanupでタイマークリア', () => {
    it('should cleanup timer on unmount', () => {
      const { unmount } = renderHook(() => useIdleMode({}))
+     
+     // タイマーが設定されていることを確認
+     expect(jest.getTimerCount()).toBeGreaterThan(0)
+     
      unmount()

-     // Timer should be cleared (no error on advancing timers after unmount)
-     expect(() => {
-       act(() => {
-         jest.advanceTimersByTime(1000)
-       })
-     }).not.toThrow()
+     // アンマウント後はタイマーがクリアされていることを確認
+     expect(jest.getTimerCount()).toBe(0)
    })
  })

347-375: ランダムモードテストのアサーションを強化してください。

Math.random0.5にモックしていますが、選択されたフレーズが期待通りかを検証していません。3つのフレーズがある場合、0.5 * 3 = 1.5Math.floor1となり、2番目のフレーズ「フレーズ2」が選択されるはずです。

💡 提案
      const onIdleSpeechStart = jest.fn()
      renderHook(() => useIdleMode({ onIdleSpeechStart }))

      act(() => {
        jest.advanceTimersByTime(5000)
      })

-     expect(onIdleSpeechStart).toHaveBeenCalled()
+     expect(onIdleSpeechStart).toHaveBeenCalledWith({
+       text: 'フレーズ2',
+       emotion: 'neutral',
+     })

      // Restore Math.random
      Math.random = originalRandom

396-417: 時間帯別挨拶テストで実際の挨拶内容を検証してください。

現在のテストはonIdleSpeechStartが呼ばれたことのみを検証しています。テスト実行時刻によって結果が異なるため、Dateをモックして特定の時間帯を検証することを推奨します。

💡 提案
  describe('時間帯別挨拶機能', () => {
-   it('should use time period greeting when enabled', () => {
+   it('should use morning greeting in the morning', () => {
+     // 午前9時をモック
+     jest.setSystemTime(new Date('2026-01-13T09:00:00'))
+     
      setupSettingsMock({
        idleInterval: 5,
        idlePhrases: [],
        idleTimePeriodEnabled: true,
        idleTimePeriodMorning: 'おはようございます!',
        idleTimePeriodAfternoon: 'こんにちは!',
        idleTimePeriodEvening: 'こんばんは!',
      })

      const onIdleSpeechStart = jest.fn()
      renderHook(() => useIdleMode({ onIdleSpeechStart }))

      act(() => {
        jest.advanceTimersByTime(5000)
      })

-     // 時間帯別挨拶が呼ばれる
-     expect(onIdleSpeechStart).toHaveBeenCalled()
+     expect(onIdleSpeechStart).toHaveBeenCalledWith({
+       text: 'おはようございます!',
+       emotion: 'neutral',
+     })
    })
  })
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 11906d3 and 17f1d15.

📒 Files selected for processing (2)
  • src/__tests__/hooks/useIdleMode.test.ts
  • src/hooks/useIdleMode.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/__tests__/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

テストは__tests__ディレクトリに配置

Files:

  • src/__tests__/hooks/useIdleMode.test.ts
🧬 Code graph analysis (2)
src/hooks/useIdleMode.ts (4)
src/features/idle/idleTypes.ts (2)
  • EmotionType (20-26)
  • IdlePhrase (29-34)
src/features/messages/messages.ts (1)
  • Talk (22-26)
src/features/messages/speakCharacter.ts (1)
  • speakCharacter (365-365)
src/features/messages/speakQueue.ts (1)
  • SpeakQueue (14-224)
src/__tests__/hooks/useIdleMode.test.ts (1)
src/hooks/useIdleMode.ts (1)
  • useIdleMode (62-334)
⏰ 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: claude-review
🔇 Additional comments (10)
src/hooks/useIdleMode.ts (7)

1-37: LGTM!

型定義とインターフェースが明確に定義されており、JSDocコメントも適切です。


39-51: LGTM!

時間帯判定ロジックが明確で、24時間すべてをカバーしています。


111-194: LGTM!

発話条件判定とセリフ選択ロジックが適切に実装されています。順番モードのラップアラウンドとランダムモードの処理も正しいです。


196-238: LGTM!

発話実行ロジックが適切に実装されています。セッションID管理とコールバック処理が正しく行われています。


267-308: LGTM!

タイマー管理が適切に実装されています。idleState === 'waiting'の条件により、発話中は再トリガーされません。


310-325: LGTM!

ユーザー入力検知によるタイマーリセットが適切に実装されています。homeStore.subscribeのクリーンアップも正しく行われています。


327-334: LGTM!

戻り値がインターフェース定義と一致しており、正しく実装されています。

src/__tests__/hooks/useIdleMode.test.ts (3)

202-268: LGTM!

発話条件判定のテストが網羅的で、各ブロッキング条件(AI処理中、発話中、人感検知)が適切にテストされています。


420-489: LGTM!

発話実行と繰り返し発話のテストが適切に実装されています。モックの制限についてのコメントも明確です。


491-543: LGTM!

resetTimerstopIdleSpeechのテストが適切に実装されています。状態遷移とコールバック呼び出しの検証が正しく行われています。

@claude
Copy link
Contributor

claude bot commented Jan 13, 2026

Claude finished @tegnike's task —— View job


Comprehensive Code Review: PR #481 - Kiosk/Idle/Presence Features

Overview

This PR adds three major features for unattended operation: Kiosk mode, Idle speech, and Presence detection. The implementation is well-architected with comprehensive test coverage (1,018 test cases across 60 files). Here's my detailed analysis:

✅ Strengths

1. Excellent Architecture & Code Quality

  • Clean Separation of Concerns: Each feature properly isolated with dedicated hooks (useKioskMode, useIdleMode, usePresenceDetection)
  • Type Safety: Comprehensive TypeScript interfaces with proper validation functions
  • React Best Practices: Proper useCallback/useMemo usage, ref management for avoiding stale closures
  • State Management: Well-integrated with existing Zustand patterns

2. Outstanding Test Coverage (A+)

  • Comprehensive: 95%+ coverage with TDD-style organization
  • Quality: Clear requirement traceability and realistic edge cases
  • Security: Proper testing of passcode lockouts and input validation
  • Integration: End-to-end workflow testing across all three features
  • Error Handling: Excellent coverage of camera permissions, model loading failures

3. Security Implementation

  • Rate Limiting: 3-attempt lockout with 30-second cooldown
  • Input Sanitization: NG word filtering with case-insensitive matching
  • Resource Management: Proper cleanup of timers, camera streams, intervals

⚠️ Areas for Improvement

1. Security Concerns

Medium Priority:

// src/features/kiosk/kioskTypes.ts:30
kioskPasscode: '0000', // ⚠️ Weak default passcode

Recommendation: Consider stronger default (random generation) or force user to set on first use

Low Priority:

// src/features/kiosk/passcodeDialog.tsx:154 // Could show passcode strength indicator

2. Performance Considerations

Face Detection Optimization:

// src/hooks/usePresenceDetection.ts:344
const interval = SENSITIVITY_INTERVALS[presenceDetectionSensitivity]
detectionIntervalRef.current = setInterval(detectFace, interval)

Recommendations:

  • Add performance monitoring for face detection loops
  • Consider adaptive frame rate based on device capability
  • Monitor memory usage during extended sessions

Timer Management:

// src/hooks/useIdleMode.ts:279
timerRef.current = setInterval(() => {
  setSecondsUntilNextSpeech((prev) => prev - 1)
}, 1000)

Consideration: Multiple 1-second intervals could be consolidated

3. Minor Code Quality Improvements

Error Handling Enhancement:

// src/hooks/usePresenceDetection.ts:302
} catch (err) {
  logDebug('Detection error:', err) // Could include error recovery
}

Magic Numbers:

// src/features/kiosk/passcodeDialog.tsx:70
}, 500) // Could be named constant: ESCAPE_KEY_DELAY

🔒 Security Analysis

✅ Well Implemented

  • Passcode input properly masked (type="password")
  • Rate limiting prevents brute force attacks
  • Input validation with length restrictions
  • Proper resource cleanup prevents DoS

🔍 Recommendations

  1. Stronger Default Security: Consider forcing passcode change on first setup
  2. Audit Logging: Consider adding security event logging for failed attempts
  3. Session Management: Clear temporary unlock on page refresh/close

🚀 Performance Analysis

✅ Good Patterns

  • Efficient state updates with proper dependency arrays
  • Resource cleanup in useEffect return functions
  • Configurable detection sensitivity (150ms-500ms intervals)

🔍 Optimizations

  1. Face Detection: Consider requestAnimationFrame instead of setInterval
  2. Memory Monitoring: Add memory usage tracking for long sessions
  3. Debouncing: Some state updates could benefit from debouncing

🧪 Test Coverage Assessment

Overall Rating: A+ (Excellent)

Strengths:

  • 1,018 test cases with clear requirement mapping
  • TDD approach with excellent edge case coverage
  • Integration tests covering end-to-end workflows
  • Security scenarios well tested (lockouts, validation)
  • Error handling comprehensively covered

Minor Gaps:

  • Cross-browser compatibility edge cases
  • Extended session stability tests
  • Network failure during model loading
  • Concurrent presence detection scenarios

📋 Final Recommendations

🚨 Before Merge:

  1. Consider stronger default passcode (or force user setup)
  2. Add performance monitoring hooks for face detection
  3. Document resource usage in README for deployment planning

🔄 Future Iterations:

  1. Add audit logging for security events
  2. Implement adaptive performance scaling
  3. Enhanced error recovery for camera/model failures
  4. Cross-browser compatibility testing

🎯 Conclusion

Overall Assessment: EXCELLENT ⭐⭐⭐⭐⭐

This PR demonstrates professional-grade development with:

  • Solid Architecture: Clean, maintainable code following React best practices
  • Exceptional Testing: Comprehensive coverage with realistic scenarios
  • Production Ready: Proper error handling and resource management
  • Security Conscious: Good security patterns with room for enhancement

The implementation is well-suited for production deployment in exhibition/digital signage environments. The identified improvements are minor and can be addressed in follow-up PRs.

Recommendation: ✅ APPROVE with consideration for the security enhancement suggestions.

@claude
Copy link
Contributor

claude bot commented Jan 14, 2026

Claude finished @tegnike's task —— View job


📋 PR Review: Kiosk/Idle/Presence Features (#481)

✅ Code Quality & Architecture

Strong Points:

  • Excellent hook architecture: All three features follow consistent React hook patterns with proper separation of concerns
  • Comprehensive TypeScript typing: Well-defined interfaces and type safety throughout
  • State management integration: Clean Zustand store integration with proper state persistence
  • Event handling: Proper cleanup and memory leak prevention with useEffect dependencies

Areas for Improvement:

  • Performance optimization needed: Face detection runs continuously at high frequency (150-500ms intervals)
  • Error boundary missing: No React error boundaries for face detection failures
  • Code duplication: Similar callback ref patterns across multiple hooks

🔒 Security Analysis

Critical Issues:

  1. Dependency vulnerability: face-api.js@0.22.2 has known security issues - consider updating or finding alternatives
  2. Passcode storage: Stored in plaintext in localStorage (acceptable for kiosk scenarios but document this limitation)
  3. Input validation bypass: NG word filtering can be bypassed with Unicode normalization attacks

Recommendations:

  • Add input sanitization beyond current validation
  • Consider rate limiting for passcode attempts beyond the 30-second lockout
  • Document security model for kiosk environments

🚀 Performance Considerations

Face Detection Issues:

// usePresenceDetection.ts:344-346
const interval = SENSITIVITY_INTERVALS[presenceDetectionSensitivity]
detectionIntervalRef.current = setInterval(detectFace, interval)
// High sensitivity = 150ms interval = ~6.7 FPS

Recommendations:

  1. Add performance monitoring: Track detection latency and CPU usage
  2. Implement adaptive intervals: Slow down when no faces detected
  3. Add worker threads: Move heavy face detection to web workers
  4. Battery optimization: Pause detection during idle periods

🧪 Test Coverage Analysis

Excellent Test Coverage:

  • ✅ All hooks have comprehensive unit tests
  • ✅ Component tests cover key user interactions
  • ✅ Error scenarios well tested
  • ✅ Integration tests for complex workflows

Missing Areas:

  • Camera permission edge cases
  • Face detection model loading failures
  • Long-running timer behavior validation

🔧 Code Quality Issues

usePresenceDetection.ts:70-80: Complex nested refs could be simplified
useIdleMode.ts:160-164: TODO comments indicate incomplete AI generation feature
passcodeDialog.tsx:183-185: Button disable logic is complex and could be extracted

📈 Recommendations

Priority 1 (Critical):

  1. Address face-api.js security vulnerability
  2. Implement performance monitoring for face detection
  3. Add error boundaries for camera failures

Priority 2 (Important):

  1. Add adaptive detection intervals based on detection state
  2. Implement proper input sanitization for NG word filtering
  3. Complete AI generation feature or remove TODOs

Priority 3 (Nice to have):

  1. Extract common callback ref patterns to shared hook
  2. Add accessibility improvements for kiosk mode
  3. Consider CSP headers for additional security

🎯 Overall Assessment

This is a well-architected, feature-complete implementation with excellent test coverage. The main concerns are around performance optimization and dependency security. The code follows React best practices and integrates cleanly with the existing codebase.

Recommendation: ✅ Approve with conditions - Address the face-api.js vulnerability and performance monitoring before merge.

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.

3 participants