diff --git a/backend/docs/ANALYSIS_GUIDE.md b/backend/docs/ANALYSIS_GUIDE.md index f6d867c..3cb0e18 100644 --- a/backend/docs/ANALYSIS_GUIDE.md +++ b/backend/docs/ANALYSIS_GUIDE.md @@ -117,7 +117,7 @@ src/analysis/ "speechDurationMs": 8500, "silenceDurationMs": 500, "volumeDb": -25.3, - "transcribedText": "ログイン画面でパスワードを入力しても弾かれます" + "transcribedText": "ログインして移動したのに、ダッシュボードが真っ白で何も表示されません" }, { "turnIndex": 2, @@ -126,7 +126,7 @@ src/analysis/ "speechDurationMs": 12000, "silenceDurationMs": 300, "volumeDb": -22.1, - "transcribedText": "いや、それは違います。なぜならパスワードリセットのリンクが表示されないんです" + "transcribedText": "いや、キャッシュの問題ではないと思います。なぜなら別のブラウザで試しても画面が出ないんです" }, { "turnIndex": 3, @@ -135,7 +135,7 @@ src/analysis/ "speechDurationMs": null, "silenceDurationMs": null, "volumeDb": null, - "transcribedText": "もういいです、別の方法を試します" + "transcribedText": "もう解決しそうにないので、エンジニアの方に確認してもらえますか" } ] } diff --git a/backend/src/services/fallbackData.ts b/backend/src/services/fallbackData.ts index 339ecf8..87beabc 100644 --- a/backend/src/services/fallbackData.ts +++ b/backend/src/services/fallbackData.ts @@ -17,17 +17,7 @@ export interface FallbackEntry { export const FALLBACK_TABLE: FallbackEntry[] = [ // ===== ラリー1系: 状況説明キーワード ===== { - keywords: ['ログイン', 'パスワード', 'サインイン', 'ログアウト', '認証', 'アカウント'], - responses: [ - { response: 'パスワードは半角英数字で入力していただく必要がございます。たぶん。', emotion: 'confident' }, - { response: 'ブラウザのCookieをクリアしてみてください。それで直ると思います。', emotion: 'confused' }, - { response: 'ブラウザを変えてみるのが早いかもしれません。根拠はないですけど。', emotion: 'confused' }, - { response: 'ログイン画面を一度閉じて、もう一度開いてみてください。', emotion: 'confident' }, - { response: 'パスワードの大文字小文字は区別されますので、ご確認いただけますでしょうか。', emotion: 'neutral' }, - ], - }, - { - keywords: ['ダッシュボード', '画面', '表示されない', '見えない', '出ない', '真っ白', '白い'], + keywords: ['ダッシュボード', 'メイン画面', 'ログイン後', '表示されない', '見えない', '出ない', '真っ白', '白い', '画面移行'], responses: [ { response: 'ページを更新してみてください。F5キーを押すと更新できます。', emotion: 'confident' }, { response: 'キャッシュが原因の可能性がございます。コントロールとシフトとデリートで削除できます。', emotion: 'confused' }, @@ -35,6 +25,16 @@ export const FALLBACK_TABLE: FallbackEntry[] = [ { response: '別のタブで開き直してみるとうまくいくかもしれません。', emotion: 'neutral' }, ], }, + { + keywords: ['ログイン', 'パスワード', 'サインイン', 'ログアウト', '認証', 'アカウント', '入れない'], + responses: [ + { response: 'パスワードは半角英数字で入力していただく必要がございます。たぶん。', emotion: 'confident' }, + { response: 'ログイン画面を一度閉じて、もう一度開いてみてください。', emotion: 'confident' }, + { response: 'パスワードの大文字小文字は区別されますので、ご確認いただけますでしょうか。', emotion: 'neutral' }, + { response: 'ブラウザのCookieをクリアしてみてください。それで直ると思います。', emotion: 'confused' }, + { response: 'ブラウザを変えてみるのが早いかもしれません。根拠はないですけど。', emotion: 'confused' }, + ], + }, { keywords: ['エラー', 'エラーコード', '500', '404', '403', '不具合', 'バグ', 'おかしい'], responses: [ diff --git a/frontend/public/RealYouLogo.png b/frontend/public/images/RealYouLogo.png similarity index 100% rename from frontend/public/RealYouLogo.png rename to frontend/public/images/RealYouLogo.png diff --git a/frontend/public/StartButton.png b/frontend/public/images/StartButton.png similarity index 100% rename from frontend/public/StartButton.png rename to frontend/public/images/StartButton.png diff --git a/frontend/public/images/game2_backcground.png b/frontend/public/images/game2_backcground.png new file mode 100644 index 0000000..080b7af Binary files /dev/null and b/frontend/public/images/game2_backcground.png differ diff --git a/frontend/public/start-se.mp3 b/frontend/public/sounds/start-se.mp3 similarity index 100% rename from frontend/public/start-se.mp3 rename to frontend/public/sounds/start-se.mp3 diff --git "a/frontend/public/sounds/\351\233\273\350\251\261\343\201\214\345\210\207\343\202\214\343\202\2131.mp3" "b/frontend/public/sounds/\351\233\273\350\251\261\343\201\214\345\210\207\343\202\214\343\202\2131.mp3" new file mode 100644 index 0000000..6e0ac78 Binary files /dev/null and "b/frontend/public/sounds/\351\233\273\350\251\261\343\201\214\345\210\207\343\202\214\343\202\2131.mp3" differ diff --git "a/frontend/public/sounds/\351\233\273\350\251\261\343\201\256\345\221\274\343\201\263\345\207\272\343\201\227\351\237\263.mp3" "b/frontend/public/sounds/\351\233\273\350\251\261\343\201\256\345\221\274\343\201\263\345\207\272\343\201\227\351\237\263.mp3" new file mode 100644 index 0000000..a82da89 Binary files /dev/null and "b/frontend/public/sounds/\351\233\273\350\251\261\343\201\256\345\221\274\343\201\263\345\207\272\343\201\227\351\237\263.mp3" differ diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 5bbc151..7650a2b 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,5 +1,6 @@ 'use client'; +import Image from 'next/image'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import type { CSSProperties } from 'react'; @@ -62,7 +63,7 @@ export default function TopPage() { const handleStartClick = () => { setShowExplosion(true); - const audio = new Audio('/start-se.mp3'); + const audio = new Audio('/sounds/start-se.mp3'); audio.play().catch(() => {}); setTimeout(() => { @@ -88,15 +89,12 @@ export default function TopPage() { }} >
- Real You -本当の私じゃだめですか?-
@@ -104,15 +102,12 @@ export default function TopPage() { onClick={handleStartClick} className="transition-all duration-100 ease-out hover:scale-110 active:scale-95 active:opacity-50" > - 診断スタート diff --git a/frontend/src/features/games/helpdesk/components/HelpdeskGameFlow.tsx b/frontend/src/features/games/helpdesk/components/HelpdeskGameFlow.tsx index 6cebb5d..b1f8aad 100644 --- a/frontend/src/features/games/helpdesk/components/HelpdeskGameFlow.tsx +++ b/frontend/src/features/games/helpdesk/components/HelpdeskGameFlow.tsx @@ -1,18 +1,18 @@ 'use client'; import { useRouter } from 'next/navigation'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import type { Game2Data } from '@/features/games/types'; import { submitGame } from '@/lib/api'; import { useHelpdeskGame } from '../hooks/useHelpdeskGame'; import Spinner from '@/components/ui/Spinner'; +import { GAME_TOPIC } from '../data/supportResponses'; type SubmitStatus = 'loading' | 'success' | 'error'; export default function HelpdeskGameFlow() { const router = useRouter(); const [textInput, setTextInput] = useState(''); - const chatEndRef = useRef(null); const [submitStatus, setSubmitStatus] = useState('loading'); const pendingDataRef = useRef(null); @@ -72,11 +72,22 @@ export default function HelpdeskGameFlow() { switchToText, onKeyDown, resetTyping, - isVoiceSupported, voiceApiRetrying, retryVoiceApi, + startInstruction, + gameTopic, + hints, } = useHelpdeskGame({ onComplete: handleComplete }); + const [hintIndex, setHintIndex] = useState(0); + const [prevHints, setPrevHints] = useState(hints); + + // ヒント内容が更新されたら(AIの応答に基づき)、インデックスを0(おすすめ)にリセットする + if (hints !== prevHints) { + setPrevHints(hints); + setHintIndex(0); + } + const handleTextSubmit = useCallback(() => { if (!textInput.trim()) return; submitTextTurn(textInput); @@ -84,63 +95,38 @@ export default function HelpdeskGameFlow() { resetTyping(); }, [textInput, submitTextTurn, resetTyping]); - useEffect(() => { - chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [chatHistory]); - - // --- 完了画面 --- - if (gamePhase === 'completed') { - if (submitStatus === 'loading') { - return ( -
- -
- ); - } - if (submitStatus === 'error') { - return ( -
-

- 通信に失敗しました -

+ // --- チャット画面 (ゲームメイン画面) --- + return ( +
+ {/* 画面右上: キーボード入力 切替ボタン */} + {inputMethod === 'voice' && + gamePhase !== 'tutorial' && + gamePhase !== 'instruction' && ( -
- ); - } - return ( -
-
-

完了しました

-

- 次のゲームに移動します... -

-
-
- ); - } + )} - // --- チャット画面 --- - return ( -
{/* --- AI応答取得失敗オーバーレイ --- */} {gamePhase === 'voice-api-error' && ( -
-
-

- 通信に失敗しました -

+
+
+

通信に失敗しました

{voiceApiRetrying ? ( - +
+ +
) : ( @@ -149,137 +135,231 @@ export default function HelpdeskGameFlow() {
)} - {/* --- 指示ポップアップ(オーバーレイ / 全体タップで進む) --- */} + {/* --- ルールポップアップ (チュートリアルフェーズ) --- */} + {gamePhase === 'tutorial' && ( +
+
+ {/* 閉じるボタン */} + + +

+ ルール +

+ +
+ {instructionText} +
+
+
+ )} + + {/* --- お題提示カットイン (インストラクションフェーズ) --- */} {gamePhase === 'instruction' && ( )} -
-

- カスタマーサポートセンター -

-
- -
-
- {chatHistory.map((msg, i) => - msg.role === 'support' ? ( -
-
- 担当 -
-
- {msg.text} -
-
- ) : ( -
+ {submitStatus === 'loading' ? ( +
+ +
+ ) : submitStatus === 'error' ? ( +
+

+ 通信に失敗しました +

+
- ) + リトライ + +
+ ) : ( +

+ 終了! +

)} -
-
+ )} -
-
- {gamePhase === 'support-speaking' && ( -

- サポート担当が話しています... -

+ {/* --- 画面下部の会話エリア --- */} +
+
+ {/* AI 思考中 */} + {gamePhase === 'awaiting-api' && ( +
+
+ AI +
+

+ 少々お待ちください。 +

+
)} - {gamePhase === 'user-input' && ( -

- {inputMethod === 'voice' - ? `残り ${Math.ceil(remainingTimeMs / 1000)} 秒` - : 'テキスト入力(時間制限なし)'} -

+ + {/* AI 発話中 */} + {gamePhase === 'support-speaking' && ( +
+
+ AI +
+

+ {chatHistory.length > 0 + ? chatHistory[chatHistory.length - 1].text + : ''} +

+ {/* ▼ 進むアイコン風 */} +
+ + + +
+
)} - {inputMethod === 'voice' ? ( -
- {speech.isListening && ( - <> - 録音中... - - - )} - {!isVoiceSupported && ( -

- お使いのブラウザでは音声認識に未対応です。テキスト入力に切り替えてください。 + {/* ユーザー入力中(ストーリーモード風 2段) */} + {gamePhase === 'user-input' && ( + <> + {/* 上段:AIの直前の発言履歴 */} +

+
+ AI +
+

+ {chatHistory.length > 0 + ? chatHistory[chatHistory.length - 1].text + : ''}

- )} -
- ) : ( -
- setTextInput(e.target.value)} - onKeyDown={(e) => { - onKeyDown(); - if (e.key === 'Enter' && !e.nativeEvent.isComposing) { - handleTextSubmit(); - } - }} - placeholder="メッセージを入力..." - className="flex-1 rounded-full border border-gray-300 px-4 py-2 text-sm outline-none focus:border-blue-400" - /> - -
+
+ + {/* 下段:あなたの現在の入力 */} +
+
+ あなた +
+ + {/* --- ヒント表示領域 --- */} +
+
+
+ Advice +
+
+

+ {hints[hintIndex]} +

+ +
+
+
+

+ Mission: {gameTopic.replace('【トラブル】', '')} +

+
+
+ + {inputMethod === 'voice' ? ( +

+ {speech.interimText || ( + (お話ください...) + )} +

+ ) : ( +
+ setTextInput(e.target.value)} + onKeyDown={(e) => { + onKeyDown(); + if (e.key === 'Enter' && !e.nativeEvent.isComposing) { + handleTextSubmit(); + } + }} + placeholder="キーボードで入力..." + className="flex-1 rounded-xl border-[4px] border-black bg-white px-4 py-3 text-lg font-bold text-black outline-none focus:bg-[#f0f0f0]" + /> + +
+ )} + + {/* 音声入力時の送信ボタン */} + {inputMethod === 'voice' && speech.isListening && ( +
+
+ ● 録音中... 残り {Math.ceil(remainingTimeMs / 1000)}秒 +
+ +
+ )} +
+ )}
- - {inputMethod === 'voice' && ( - - )}
); diff --git a/frontend/src/features/games/helpdesk/data/supportResponses.ts b/frontend/src/features/games/helpdesk/data/supportResponses.ts index 32da678..ec26a64 100644 --- a/frontend/src/features/games/helpdesk/data/supportResponses.ts +++ b/frontend/src/features/games/helpdesk/data/supportResponses.ts @@ -11,6 +11,10 @@ export const INSTRUCTION_TEXT = export const INITIAL_SUPPORT_MESSAGE = 'お問い合わせありがとうございます!サポート担当の田中です。ダッシュボードが表示されないんですね...。どのような状況か、もう少し詳しく教えていただけますか?'; +/** ゲーム終了時の最後のサポート発言(固定) */ +export const FINAL_SUPPORT_MESSAGE = + 'もういいです!わかりました!こちらでもう一度確認しますので、一旦失礼します!!'; + /** * POST /api/voice/respond が失敗した場合のフォールバック応答。 * 返答内容は演出用で性格判定のスコア計算には使用しない。 @@ -20,3 +24,52 @@ export const SUPPORT_RESPONSES = [ 'なるほど〜、それは困りましたね。えーっと、とりあえずブラウザのキャッシュをクリアしてみてください!あと、Internet Explorerで開いてみるのもおすすめです!', 'うーん、それでもダメですか...。じゃあ最後の手段なんですけど、パソコンを再起動してみてもらえますか?それでもダメなら、また電話くださいね!', ]; + +/** 最初のターンに出すヒント(ミッションの再確認) */ +export const INITIAL_HINTS = [ + '「ログインしたのに、ダッシュボードが表示されないんです」', +]; + +/** ユーザーへのデフォルト発話ヒント(文脈が特定できない場合) */ +export const DEFAULT_HINTS = [ + '「キャッシュをクリアしても直りません。どうすればいいですか?」', + '「別のブラウザでも試しましたが、画面が真っ白なままです」', + '「急いでいるので、早く解決方法を教えてください」', + '「もういいです。エンジニアの担当者を出してください」', +]; + +/** AIの応答キーワードに基づくおすすめヒントのリスト */ +export const HINT_MAPPING: { keywords: string[]; hints: string[] }[] = [ + { + keywords: ['キャッシュ', 'Cookie', '履歴'], + hints: [ + '「キャッシュはもう消しましたが、状況は変わりません」', + '「具体的にどの期間のキャッシュを消せばいいですか?」', + '「キャッシュの問題ではないと思います。別のPCでも同様なので」', + ], + }, + { + keywords: ['再起動', '電源'], + hints: [ + '「PCの再起動は既に試しました。他になにかありますか?」', + '「今から再起動してみるので、少し待ってもらえますか?」', + '「再起動しても直らないから相談しているんです」', + ], + }, + { + keywords: ['ブラウザ', 'Chrome', 'Edge', 'IE'], + hints: [ + '「別のブラウザで試しても、やはりダッシュボードは出ません」', + '「推奨ブラウザは何ですか?」', + '「ブラウザのバージョンは最新のはずです」', + ], + }, + { + keywords: ['エンジニア', '担当', '確認'], + hints: [ + '「話が進まないので、わかる方に代わってください」', + '「いつまでに確認が終わるか、目処を教えてください」', + '「これ以上待てません。至急エンジニアに確認してください」', + ], + }, +]; diff --git a/frontend/src/features/games/helpdesk/hooks/useHelpdeskGame.ts b/frontend/src/features/games/helpdesk/hooks/useHelpdeskGame.ts index f8ed11d..3d39e47 100644 --- a/frontend/src/features/games/helpdesk/hooks/useHelpdeskGame.ts +++ b/frontend/src/features/games/helpdesk/hooks/useHelpdeskGame.ts @@ -10,7 +10,11 @@ import { postVoiceRespond } from '@/lib/api'; import { GAME_TOPIC, INITIAL_SUPPORT_MESSAGE, + FINAL_SUPPORT_MESSAGE, INSTRUCTION_TEXT, + INITIAL_HINTS, + DEFAULT_HINTS, + HINT_MAPPING, MAX_TURNS, TURN_TIME_LIMIT_MS, } from '../data/supportResponses'; @@ -31,9 +35,11 @@ import { useTypingMetrics } from './useTypingMetrics'; * error → その他エラー */ export type GamePhase = + | 'tutorial' | 'instruction' | 'support-speaking' | 'user-input' + | 'awaiting-api' | 'voice-api-error' | 'submitting' | 'completed' @@ -65,9 +71,37 @@ export function useHelpdeskGame(options: { const [currentTurn, setCurrentTurn] = useState(0); // 0〜2(3ラリー) const [chatHistory, setChatHistory] = useState([]); // 表示用メッセージ履歴 const [turns, setTurns] = useState([]); // 収集データ用ターンログ - const [gamePhase, setGamePhase] = useState('instruction'); + const [gamePhase, setGamePhase] = useState('tutorial'); const [remainingTimeMs, setRemainingTimeMs] = useState(TURN_TIME_LIMIT_MS); const [voiceApiRetrying, setVoiceApiRetrying] = useState(false); + const [currentHints, setCurrentHints] = useState(INITIAL_HINTS); + + // --- 環境チェック & SE 準備 --- + const isSpeechSupported = + typeof window !== 'undefined' && + !!( + (window as Window & { SpeechRecognition?: unknown }).SpeechRecognition || + (window as Window & { webkitSpeechRecognition?: unknown }) + .webkitSpeechRecognition + ) && + !!window.speechSynthesis; + + // 呼び出し音 & 切断音 + const callingAudioRef = useRef(null); + const hangupAudioRef = useRef(null); + + useEffect(() => { + if (typeof window !== 'undefined') { + callingAudioRef.current = new Audio('/sounds/電話の呼び出し音.mp3'); + callingAudioRef.current.loop = true; + hangupAudioRef.current = new Audio('/sounds/電話が切れる1.mp3'); + + // 非対応ブラウザならテキストモードへ強制 + if (!isSpeechSupported) { + setInputMethod('text'); + } + } + }, [isSpeechSupported]); // --- 再レンダリング不要なデータを ref で管理 --- const pendingVoiceRequestRef = useRef<{ @@ -82,6 +116,7 @@ export function useHelpdeskGame(options: { const typingVariancesRef = useRef([]); // テキスト入力ターンごとの分散を蓄積 const skipNextVoiceResultRef = useRef(false); // テキスト切替時に onResult を無視するフラグ const submittedRef = useRef(false); // buildAndSubmit の二重実行防止 + const usedHintsRef = useRef>(new Set()); // --- 子フック --- const { startRecording, stopRecording } = useAudioMetrics(); @@ -104,7 +139,7 @@ export function useHelpdeskGame(options: { currentTurnRef.current = nextTurn; setCurrentTurn(nextTurn); - if (nextTurn >= MAX_TURNS) { + if (nextTurn > MAX_TURNS) { setGamePhase('submitting'); } else { setGamePhase('support-speaking'); @@ -207,6 +242,11 @@ export function useHelpdeskGame(options: { }; onComplete(game2Data); setGamePhase('completed'); + + // 電話終了音 + if (hangupAudioRef.current) { + hangupAudioRef.current.play().catch(() => {}); + } }, [onComplete]); /** gamePhase が submitting になったら一度だけ buildAndSubmit を実行 */ @@ -244,15 +284,52 @@ export function useHelpdeskGame(options: { const addSupportResponseAndSpeak = useCallback((supportText: string) => { setChatHistory((prev) => [...prev, { role: 'support', text: supportText }]); + // 最初の発言(サポートが喋り始めた)タイミングで呼び出し音を止める + if (callingAudioRef.current) { + callingAudioRef.current.pause(); + callingAudioRef.current.currentTime = 0; + } + + // AIの応答内容からキーワードを検索し、ヒントを更新する + // ただし初回挨拶(Turn 0)の場合は INITIAL_HINTS を継続する + if (currentTurnRef.current === 0) { + setCurrentHints(INITIAL_HINTS); + } else { + const matched = HINT_MAPPING.find((m) => + m.keywords.some((kw) => supportText.includes(kw)) + ); + + let nextHints = matched ? [...matched.hints] : [...DEFAULT_HINTS]; + + // 既に使用したヒントを除外 + nextHints = nextHints.filter((h) => !usedHintsRef.current.has(h)); + + // もし全て使用済みならデフォルトに戻す(あるいは空にしないための配慮) + if (nextHints.length === 0) { + nextHints = DEFAULT_HINTS.filter((h) => !usedHintsRef.current.has(h)); + } + + if (nextHints.length > 0) { + setCurrentHints(nextHints); + // 今回表示するヒント(先頭1つなど)を使用済みとしてマークする場合: + // ここではUI側でインデックスがリセットされるため、先頭のヒントを使用済みに追加 + usedHintsRef.current.add(nextHints[0]); + } + } + const transitionToInput = () => { - setGamePhase('user-input'); - setRemainingTimeMs(TURN_TIME_LIMIT_MS); + if (currentTurnRef.current >= MAX_TURNS) { + setGamePhase('submitting'); + } else { + setGamePhase('user-input'); + setRemainingTimeMs(TURN_TIME_LIMIT_MS); + } }; if (typeof window !== 'undefined' && window.speechSynthesis) { const utterance = new SpeechSynthesisUtterance(supportText); utterance.lang = 'ja-JP'; - utterance.rate = 1.1; + utterance.rate = 1.3; utterance.onend = transitionToInput; utterance.onerror = transitionToInput; window.speechSynthesis.speak(utterance); @@ -272,6 +349,8 @@ export function useHelpdeskGame(options: { if (currentTurn === 0) { supportText = INITIAL_SUPPORT_MESSAGE; + } else if (currentTurn === MAX_TURNS) { + supportText = FINAL_SUPPORT_MESSAGE; } else { const history = chatHistoryRef.current; const lastUserMsg = [...history] @@ -311,6 +390,13 @@ export function useHelpdeskGame(options: { if (cancelled) return; + // 初回なら呼び出し音を3秒聞かせる + if (currentTurn === 0) { + await new Promise((resolve) => setTimeout(resolve, 3000)); + } + + if (cancelled) return; + addSupportResponseAndSpeak(supportText); }; @@ -456,10 +542,44 @@ export function useHelpdeskGame(options: { [getVarianceAndReset, advanceAfterUserTurn] ); + // ========================================================= + // ゲーム開始 + // ========================================================= + + /** モーダルからお題オーバレイへ進むとき */ + const startInstruction = useCallback(async () => { + if (gamePhase !== 'tutorial') return; + setGamePhase('instruction'); + + // iOS 等での音声(TTS/SE)ロック解除のためのハック + if (typeof window !== 'undefined') { + const silentAudio = new Audio(); + silentAudio.src = + 'data:audio/wav;base64,UklGRigAAABXQVZFRm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA='; + silentAudio.play().catch(() => {}); + + // マイクの事前許可を求める + try { + const stream = await navigator.mediaDevices.getUserMedia({ + audio: true, + }); + // 許可が得られたらすぐに閉じる(本番の録音は useAudioMetrics が行う) + stream.getTracks().forEach((track) => track.stop()); + } catch (err) { + console.warn('Microphone permission denied or not available:', err); + } + } + }, [gamePhase]); + /** 指示ポップアップから「相談を始める」を押したとき */ const startGame = useCallback(() => { if (gamePhase !== 'instruction') return; setGamePhase('support-speaking'); + + // 電話呼び出し音を開始 + if (callingAudioRef.current) { + callingAudioRef.current.play().catch(() => {}); + } }, [gamePhase]); /** @@ -498,6 +618,7 @@ export function useHelpdeskGame(options: { maxTurns: MAX_TURNS, speech, startGame, + startInstruction, endVoiceTurnManually, submitTextTurn, switchToText, @@ -506,5 +627,6 @@ export function useHelpdeskGame(options: { isVoiceSupported: speech.isSupported, voiceApiRetrying, retryVoiceApi, + hints: currentHints, }; } diff --git a/frontend/src/features/games/terms/components/PopupTerms.tsx b/frontend/src/features/games/terms/components/PopupTerms.tsx index 87a4982..c1d0493 100644 --- a/frontend/src/features/games/terms/components/PopupTerms.tsx +++ b/frontend/src/features/games/terms/components/PopupTerms.tsx @@ -4,7 +4,6 @@ import TermsContent from './TermsContent'; import type { FC } from 'react'; interface PopupTermsProps { - onClose: () => void; onCheckboxChange: ( key: 'readConfirm' | 'mailMagazine' | 'thirdPartyShare', checked: boolean @@ -22,7 +21,6 @@ interface PopupTermsProps { } const PopupTerms: FC = ({ - onClose, onCheckboxChange, onHiddenInputChange, checkboxStates, diff --git a/frontend/src/features/games/terms/components/TermsGameFlow.tsx b/frontend/src/features/games/terms/components/TermsGameFlow.tsx index 5a2cc5d..4a029bc 100644 --- a/frontend/src/features/games/terms/components/TermsGameFlow.tsx +++ b/frontend/src/features/games/terms/components/TermsGameFlow.tsx @@ -5,7 +5,6 @@ import { useSetAtom } from 'jotai'; import { useRouter } from 'next/navigation'; import type { Game1Data, ScrollEvent } from '@/features/games/types'; import { game1DataAtom } from '@/stores/games'; -import TermsContent from './TermsContent'; import PopupAd from './PopupAd'; import PopupTerms from './PopupTerms'; // TODO: バックエンド接続時にコメントアウトを解除する @@ -32,7 +31,7 @@ export default function TermsGameFlow() { // ポップアップ広告の表示状態 const [showPopup, setShowPopup] = useState(false); // 利用規約モーダル表示状態(初期で表示) - const [showTermsModal, setShowTermsModal] = useState(true); + const [showTermsModal] = useState(true); // ゲーム完了フラグ(trueで完了画面を表示→次のゲームへ遷移) const [isCompleted, setIsCompleted] = useState(false); @@ -206,10 +205,6 @@ export default function TermsGameFlow() { {showTermsModal && ( { - setShowTermsModal(false); - scrollContainerRef.current = null; - }} onCheckboxChange={handleCheckboxChange} onHiddenInputChange={handleHiddenInputChange} checkboxStates={checkboxStates}