syurein commited on
Commit
ba82e98
·
1 Parent(s): a09ca12
Files changed (1) hide show
  1. static/script.js +149 -368
static/script.js CHANGED
@@ -3,11 +3,11 @@
3
  "use strict"; // より厳格なエラーチェック
4
 
5
  // --- グローバル変数 ---
6
- let learningData = null; // 学習データ (learning.html用) - { title: '...', items: [...] } の形式を想定
7
- let currentItemIndex = 0; // 現在表示中のアイテムインデックス (learning.html用)
8
- let currentMode = 'quiz'; // 現在のモード 'quiz' or 'summary' (learning.html用)
9
- let correctEffectTimeout; // 正解エフェクト非表示用のタイマーID (learning.html用)
10
- let correctEffect = null; // 正解エフェクト要素 (learning.html用)
11
  let sideMenu = null; // サイドメニュー要素
12
  let menuOverlay = null; // メニューオーバーレイ要素
13
 
@@ -19,10 +19,9 @@ let menuOverlay = null; // メニューオーバーレイ要素
19
  */
20
  function navigateTo(url) {
21
  closeMenu(); // 遷移前にメニューを閉じる
22
- // 少し遅延させてから遷移する(メニューが閉じるアニメーションを見せるため、任意)
23
  setTimeout(() => {
24
  window.location.href = url;
25
- }, 100); // 100ミリ秒後に遷移
26
  }
27
 
28
  /**
@@ -33,7 +32,7 @@ function openMenu() {
33
  if (sideMenu && menuOverlay) {
34
  sideMenu.classList.add('open');
35
  menuOverlay.classList.add('open');
36
- document.body.classList.add('menu-open'); // 背景スクロール禁止
37
  } else {
38
  console.error("Side menu or overlay element not found. Cannot open menu.");
39
  }
@@ -43,13 +42,11 @@ function openMenu() {
43
  * サイドメニューを閉じます。
44
  */
45
  function closeMenu() {
46
- // console.log("Closing menu..."); // デバッグ時以外は不要かも
47
  if (sideMenu && menuOverlay) {
48
  sideMenu.classList.remove('open');
49
  menuOverlay.classList.remove('open');
50
- document.body.classList.remove('menu-open'); // 背景スクロール許可
51
  }
52
- // メニュー要素が見つからない場合のエラーログはここでは不要
53
  }
54
 
55
 
@@ -65,7 +62,6 @@ function toggleLoading(show, buttonId = 'generate-button') {
65
  if (targetButton && buttonId === 'generate-button') {
66
  const spinner = targetButton.querySelector('.loading-spinner');
67
  const buttonText = targetButton.querySelector('.button-text');
68
-
69
  if (show) {
70
  targetButton.disabled = true;
71
  if (spinner) spinner.style.display = 'inline-block';
@@ -73,18 +69,17 @@ function toggleLoading(show, buttonId = 'generate-button') {
73
  } else {
74
  targetButton.disabled = false;
75
  if (spinner) spinner.style.display = 'none';
76
- if (buttonText) buttonText.textContent = '生成する'; // 元のテキストに戻す
77
  }
78
  }
79
 
80
- // learning.html 用の汎用ローディング表示 (mode-indicatorなどを利用)
81
  const loadingIndicator = document.getElementById('mode-indicator');
82
  const cardElement = document.getElementById('learning-card');
83
  const paginationElement = document.querySelector('.pagination');
84
  const optionsArea = document.getElementById('options-area');
85
- const tapToShowElement = document.getElementById('tap-to-show'); // 追加
86
 
87
- // 現在のパスがlearningページか確認
88
  const currentPathname = window.location.pathname;
89
  if (currentPathname.endsWith('/learning') || currentPathname.endsWith('/learning.html')) {
90
  if (show) {
@@ -95,16 +90,13 @@ function toggleLoading(show, buttonId = 'generate-button') {
95
  if (cardElement) cardElement.style.opacity = '0.5';
96
  if (paginationElement) paginationElement.style.display = 'none';
97
  if (optionsArea) optionsArea.style.display = 'none';
98
- if (tapToShowElement) tapToShowElement.style.display = 'none'; // ローディング中は非表示
99
  } else {
100
- // ローディング終了時の表示は displayCurrentItem で制御されるため、
101
- // ここではローディングインジケータのテキストクリア程度で良い
102
  if (loadingIndicator) {
103
- // loadingIndicator.textContent = ''; // displayCurrentItemでモードが表示される
104
  loadingIndicator.classList.remove('loading');
105
  }
106
  if (cardElement) cardElement.style.opacity = '1';
107
- // pagination, optionsArea, tapToShowElement の表示は displayCurrentItem に任せる
108
  }
109
  }
110
  }
@@ -118,11 +110,9 @@ function displayErrorMessage(message, elementId = 'error-message') {
118
  const errorElement = document.getElementById(elementId);
119
  if (errorElement) {
120
  errorElement.textContent = message;
121
- // メッセージがあれば表示、なければ非表示 (display: block/none)
122
  errorElement.style.display = message ? 'block' : 'none';
123
  } else {
124
- // エラー表示要素自体が見つからない場合
125
- if (message) { // メッセージがある場合のみコンソールにエラー表示
126
  console.error(`Error element with ID "${elementId}" not found. Cannot display message: ${message}`);
127
  }
128
  }
@@ -130,7 +120,7 @@ function displayErrorMessage(message, elementId = 'error-message') {
130
 
131
  // --- 画面遷移用関数 ---
132
  function goToInput() {
133
- navigateTo('/'); // input.html はルートパスを想定
134
  }
135
 
136
  function goToHistory() {
@@ -143,12 +133,10 @@ function goToSettings() {
143
 
144
  function goToLearning(contentId) {
145
  if (contentId) {
146
- // navigateTo内でメニューは閉じられる
147
  navigateTo(`/learning?id=${encodeURIComponent(contentId)}`);
148
  } else {
149
  console.error('goToLearning requires a content ID.');
150
- // ユーザーへのフィードバック
151
- alert('学習コンテンツのIDが見つかりません。履歴画面から再度お試しください。');
152
  }
153
  }
154
 
@@ -161,101 +149,67 @@ function goToLearning(contentId) {
161
  async function handleGenerateSubmit() {
162
  const urlInput = document.getElementById('youtube-url');
163
  const youtubeUrl = urlInput.value.trim();
164
- const errorMsgElementId = 'error-message'; // エラーメッセージ表示用ID
165
- displayErrorMessage('', errorMsgElementId); // 前のエラーメッセージをクリア
166
 
167
  if (!youtubeUrl) {
168
  displayErrorMessage('YouTubeリンクを入力してください。', errorMsgElementId);
169
- return false; // 送信中断
170
  }
171
 
172
- // 簡単なURL形式チェックと YouTube ドメイン検証
173
  try {
174
- const urlObj = new URL(youtubeUrl); // URLオブジェクト生成試行
175
  const validHostnames = ['www.youtube.com', 'youtube.com', 'youtu.be'];
176
- if (!validHostnames.includes(urlObj.hostname)) {
177
- throw new Error('Invalid hostname'); // YouTube以外のドメイン
178
- }
179
- // さらに詳細なパスチェック(例)
180
- if (urlObj.hostname === 'youtu.be' && urlObj.pathname.length <= 1) {
181
- throw new Error('Missing video ID for youtu.be'); // youtu.be/ の後にIDがない
182
- }
183
  if (urlObj.hostname.includes('youtube.com')) {
184
- if (urlObj.pathname === '/watch' && !urlObj.searchParams.has('v')) {
185
- throw new Error('Missing video ID parameter (v=...) for youtube.com/watch'); // vパラメータがない
186
- }
187
- if (urlObj.pathname.startsWith('/shorts/') && urlObj.pathname.length <= 8) {
188
- throw new Error('Missing video ID for youtube.com/shorts/'); // shorts/ の後にIDがない
189
- }
190
- // 他の有効な形式があれば追加 (例: /live/ など)
191
  }
192
  } catch (e) {
193
- // URLパースエラーまたは上記バリデーションエラー
194
  console.warn("Invalid URL format:", e.message);
195
  displayErrorMessage('有効なYouTube動画のリンクを入力してください。(例: https://www.youtube.com/watch?v=...)', errorMsgElementId);
196
- return false; // 送信中断
197
  }
198
 
199
-
200
- toggleLoading(true, 'generate-button'); // ローディング開始
201
 
202
  try {
203
- // APIエンドポイント '/api/generate' へPOSTリクエスト
204
  const response = await fetch('/api/generate', {
205
  method: 'POST',
206
- headers: {
207
- 'Content-Type': 'application/json',
208
- 'Accept': 'application/json', // サーバーからのJSON応答を期待
209
- },
210
- body: JSON.stringify({ url: youtubeUrl }), // URLをJSON形式で送信
211
  });
212
 
213
- // レスポンスボディをJSONとしてパース試行
214
  let result;
215
  try {
216
  result = await response.json();
217
  } catch (jsonError) {
218
- // JSONパース失敗
219
- console.error('Failed to parse JSON response:', jsonError);
220
- // サーバーエラーの可能性が高いが、レスポンスステータスも確認
221
- if (!response.ok) {
222
- throw new Error(`サーバーエラー (${response.status})。応答形式が不正です。`);
223
- } else {
224
- // 成功ステータスなのにJSONでない場合(通常はありえない)
225
- throw new Error('サーバーからの応答形式が予期せぬ形式です。');
226
- }
227
  }
228
 
229
-
230
- // ★★★ サーバーが返すJSON構造に合わせて修正 ★★★
231
- // サーバーが { success: true, data: { id: '...' } } のような形式を返す想定
232
  if (response.ok && result && typeof result === 'object' && result.success && result.data && result.data.id) {
233
- // 生成成功: 取得したIDを使って学習画面へ遷移
234
  console.log("Generation successful, navigating to learning page with ID:", result.data.id);
235
  goToLearning(result.data.id);
236
  } else {
237
- // APIがエラーを返した場合、または期待する構造でなかった場合
238
  console.error('Generation API call failed or returned unexpected structure:', result);
239
- const serverMessage = (result && typeof result === 'object' && result.message) || // resultがオブジェクトでmessageがあればそれを表示
240
- (result && typeof result === 'object' && result.error && result.error.message) || // error.messageがあればそれを表示
241
- (response.ok ? '生成に失敗しました (不明な応答形式)。' : `サーバーエラー (${response.status})`); // それ以外の場合
242
  displayErrorMessage(serverMessage, errorMsgElementId);
243
  }
244
 
245
  } catch (error) {
246
- // fetch自体が失敗した場合(ネットワークエラーなど)または上記のthrow new Error
247
  console.error('Error during generation request:', error);
248
- // ユーザーフレンドリーなメッセージ表示
249
- if (error.message.includes('Failed to fetch')) {
250
- displayErrorMessage('サーバーに接続できませんでした。ネットワーク接続を確認してください。', errorMsgElementId);
251
- } else {
252
- displayErrorMessage(`エラーが発生しました: ${error.message}`, errorMsgElementId);
253
- }
254
  } finally {
255
- toggleLoading(false, 'generate-button'); // ローディング終了(成功・失敗問わず)
256
  }
257
-
258
- return false; // formのデフォルト送信を常にキャンセル
259
  }
260
 
261
 
@@ -269,52 +223,49 @@ async function initializeLearningScreen() {
269
  const params = new URLSearchParams(window.location.search);
270
  const contentId = params.get('id');
271
 
272
- // 正解エフェクト要素はDOMContentLoadedで取得済みのはず
273
-
274
  if (!contentId) {
275
  displayLearningError('学習コンテンツのIDが見つかりません。');
276
  return;
277
  }
278
  console.log('Content ID:', contentId);
279
-
280
- toggleLoading(true); // ローディング表示開始
281
 
282
  try {
283
  const response = await fetch(`/api/learning/${contentId}`);
284
  if (!response.ok) {
285
- // エラーレスポンスがJSON形式でない場合も考慮
286
  let errorMessage = `サーバーからのデータ取得に失敗 (${response.status})`;
287
  try {
288
  const errorData = await response.json();
289
  errorMessage = errorData.message || errorMessage;
290
- } catch (e) {
291
- console.warn('Failed to parse error response as JSON.');
292
- }
293
  throw new Error(errorMessage);
294
  }
295
 
296
- // ★★★ サーバーからの応答が配列であると想定して処理 ★★★
297
- const itemsArray = await response.json();
298
- console.log('Fetched data (items array):', itemsArray);
299
 
300
- // 配列であるか、空でないかをチェック
301
- if (!Array.isArray(itemsArray)) {
302
- throw new Error('サーバーからのデータ形式が不正です (配列ではありません)。');
303
- }
304
- if (itemsArray.length === 0) {
305
- throw new Error('学習データが見つかりませんでした (空の配列)。');
306
  }
307
 
308
- // learningData オブジェクトを構築
309
  learningData = {
310
- title: `学習セット (${contentId})`, // タイトルは固定文字列またはcontentIdから生成
311
- items: itemsArray // 取得した配列を items プロパティに設定
312
  };
313
 
314
- // タイトルを設定 (learningData.title を使用)
 
 
 
 
 
315
  const titleElement = document.getElementById('learning-title');
316
  if (titleElement) {
317
- titleElement.textContent = learningData.title || '学習セット'; // デフォルトタイトル
318
  } else {
319
  console.warn("Title element ('learning-title') not found.");
320
  }
@@ -324,16 +275,11 @@ async function initializeLearningScreen() {
324
  displayCurrentItem();
325
 
326
  } catch (error) {
327
- // fetchエラーまたは上記のthrow new Error
328
  console.error('Error initializing learning screen:', error);
329
- // JSONパースエラーの場合のメッセージを改善
330
- if (error instanceof SyntaxError) {
331
- displayLearningError(`サーバー応答の解析エラー: ${error.message}`);
332
- } else {
333
- displayLearningError(`読み込みエラー: ${error.message}`);
334
- }
335
  } finally {
336
- toggleLoading(false); // ローディング表示終了
337
  }
338
  }
339
 
@@ -341,10 +287,9 @@ async function initializeLearningScreen() {
341
  * 現在の学習アイテムをカードに表示 (クイズ or 要約)
342
  */
343
  function displayCurrentItem() {
344
- hideCorrectEffect(); // 前のエフェクトを隠す
345
- clearTimeout(correctEffectTimeout); // タイマーもクリア
346
 
347
- // 要素取得 (毎回取得する方が確実)
348
  const cardElement = document.getElementById('learning-card');
349
  const cardTextElement = document.getElementById('card-text');
350
  const answerTextElement = document.getElementById('answer-text');
@@ -352,88 +297,71 @@ function displayCurrentItem() {
352
  const optionsArea = document.getElementById('options-area');
353
  const modeIndicator = document.getElementById('mode-indicator');
354
 
355
- // 要素が存在しない場合はエラーを出して終了
356
  if (!cardElement || !cardTextElement || !answerTextElement || !tapToShowElement || !optionsArea || !modeIndicator) {
357
- console.error("One or more required learning elements are missing from the DOM.");
358
- displayLearningError("画面の表示に必要な要素が見つかりません。");
359
  return;
360
  }
361
-
362
- // データ存在チェック (learningData と learningData.items をチェック)
363
  if (!learningData || !learningData.items || currentItemIndex < 0 || currentItemIndex >= learningData.items.length) {
364
  console.error('Invalid learning data or index:', learningData, currentItemIndex);
365
  displayLearningError('表示する学習データが見つかりません。');
366
  return;
367
  }
368
 
369
- // ★★★ learningData.items から現在のアイテムを取得 ★★★
370
  const item = learningData.items[currentItemIndex];
371
 
372
- // 表示内容リセット
373
- cardTextElement.innerHTML = ''; // textContentではなくinnerHTMLでリセット
374
  answerTextElement.style.display = 'none';
375
- answerTextElement.textContent = ''; // 内容もクリア
376
  tapToShowElement.style.display = 'none';
377
- optionsArea.innerHTML = ''; // 選択肢エリアクリア
378
- optionsArea.style.display = 'none'; // 一旦非表示
379
- modeIndicator.classList.remove('loading'); // ローディングクラス除去
380
 
381
- // itemタイプに基づいて表示を分岐
382
- // ★★★ 問題文のキーを 'text' に修正 ★★★
383
- if (item.type === 'question' && item.text && item.answer) { // `item.question` -> `item.text` に変更
384
  currentMode = 'quiz';
385
  modeIndicator.textContent = 'クイズモード';
386
- // ★★★ 問題文には item.text を使用 ★★★
387
- cardTextElement.textContent = item.text; // 問題文はtextContentで安全に設定
388
- answerTextElement.textContent = `答え: ${item.answer}`; // 答えは事前に設定(非表示)
389
 
390
- // 選択肢ボタンを生成
391
  if (item.options && Array.isArray(item.options) && item.options.length > 0) {
392
- optionsArea.style.display = 'block'; // 選択肢があれば表示
393
- const optionsToDisplay = item.options; // シャッフルしない場合
394
-
395
- optionsToDisplay.forEach(option => {
396
  const button = document.createElement('button');
397
  button.classList.add('option-button');
398
  button.textContent = option;
399
- // クリックしたら handleOptionClick を呼ぶ
400
  button.onclick = () => handleOptionClick(option);
401
  optionsArea.appendChild(button);
402
  });
403
- tapToShowElement.style.display = 'block'; // 選択肢がある場合もタップ表示は有効
404
  } else {
405
- // 選択肢がないクイズの場合
406
  console.warn(`Quiz item ${currentItemIndex} has no options.`);
407
- tapToShowElement.style.display = 'block'; // 答え表示は可能にする
408
  }
409
- // カード自体 or タップ表示テキストのクリックで解答表示
410
- cardElement.onclick = () => revealAnswer(); // 引数なしで呼び出し
411
- tapToShowElement.onclick = () => revealAnswer(); // 引数なしで呼び出し
412
 
413
  } else if (item.type === 'summary' && item.text) {
414
  currentMode = 'summary';
415
  modeIndicator.textContent = '要約モード';
416
- // 改行(\n)を<br>に置換して表示 (innerHTMLを使うので注意)
417
  cardTextElement.innerHTML = item.text.replace(/\n/g, '<br>');
418
-
419
- // 要約モードではクリックイベント不要
420
  cardElement.onclick = null;
421
  tapToShowElement.style.display = 'none';
422
- optionsArea.style.display = 'none'; // 念のため
423
 
424
  } else {
425
- // 不明なタイプまたは必要なデータ (text や answer) が欠けている場合
426
  console.warn('Unknown or invalid item type/data:', item);
427
  currentMode = 'unknown';
428
  modeIndicator.textContent = 'データエラー';
429
- // ★★★ `item.text` を表示試行するように変更 ★★★
430
- cardTextElement.textContent = `[不正なデータ形式] ${item.text || 'この項目を表示できません。'}`;
431
  cardElement.onclick = null;
432
  tapToShowElement.style.display = 'none';
433
  optionsArea.style.display = 'none';
434
  }
435
 
436
- updatePagination(); // ページネーション表示を更新
437
  }
438
 
439
  /**
@@ -441,30 +369,21 @@ function displayCurrentItem() {
441
  * @param {string} selectedOption - ユーザーが選択した選択肢のテキスト
442
  */
443
  function handleOptionClick(selectedOption) {
444
- // ★★★ learningData.items の存在もチェック ★★★
445
  if (currentMode !== 'quiz' || !learningData || !learningData.items || !learningData.items[currentItemIndex]) return;
446
 
447
- // 解答表示がすでに行われていないかチェック(二重処理防止)
448
  const answerTextElement = document.getElementById('answer-text');
449
- if (answerTextElement && answerTextElement.style.display === 'block') {
450
- console.log('Answer already revealed, ignoring option click.');
451
- return;
452
- }
453
 
454
- // ★★★ learningData.items から現在のアイテムを取得 ★★★
455
  const currentItem = learningData.items[currentItemIndex];
456
  const correctAnswer = currentItem.answer;
457
  const isCorrect = selectedOption === correctAnswer;
458
 
459
  if (isCorrect) {
460
  console.log("Correct!");
461
- showCorrectEffect(); // ★★★ 正解エフェクト表示 ★★★
462
  } else {
463
  console.log("Incorrect...");
464
- // 不正解時のフィードバック(例: ボタンを赤くするなど)はrevealAnswerで行う
465
  }
466
-
467
- // 選択された選択肢を引数に渡して、解答表示とボタンの状態更新を行う
468
  revealAnswer(selectedOption);
469
  }
470
 
@@ -474,53 +393,29 @@ function handleOptionClick(selectedOption) {
474
  * @param {string|null} [selectedOption=null] - ユーザーが選択した選択肢。nullの場合はカードタップ等による表示。
475
  */
476
  function revealAnswer(selectedOption = null) {
477
- // ★★★ learningData.items の存在もチェック ★★★
478
  if (currentMode !== 'quiz' || !learningData || !learningData.items || !learningData.items[currentItemIndex]) return;
479
 
480
  const answerTextElement = document.getElementById('answer-text');
481
  const tapToShowElement = document.getElementById('tap-to-show');
482
  const optionsArea = document.getElementById('options-area');
483
- const cardElement = document.getElementById('learning-card'); // カード要素も取得
484
 
485
- // 既に表示済みなら何もしない
486
- if (answerTextElement && answerTextElement.style.display === 'block') {
487
- return;
488
- }
489
 
490
- // 解答テキストを表示
491
- if (answerTextElement) {
492
- answerTextElement.style.display = 'block';
493
- }
494
- // 「タップして表示」を隠す
495
- if (tapToShowElement) {
496
- tapToShowElement.style.display = 'none';
497
- }
498
- // カード自体のクリックイベントを無効化(解答表示後は不要)
499
- if (cardElement) {
500
- cardElement.onclick = null;
501
- }
502
 
503
- // 選択肢ボタンがあれば状態を更新
504
  if (optionsArea) {
505
- // ★★★ learningData.items から現在のアイテムを取得 ★★★
506
  const correctAnswer = learningData.items[currentItemIndex].answer;
507
- const buttons = optionsArea.querySelectorAll('.option-button'); // querySelectorAll推奨
508
-
509
  buttons.forEach(button => {
510
- button.disabled = true; // 全てのボタンを無効化
511
- button.onclick = null; // クリックイベント解除
512
-
513
  const buttonText = button.textContent;
514
-
515
- if (buttonText === correctAnswer) {
516
- button.classList.add('correct'); // 正解ボタンにクラス付与
517
- } else if (buttonText === selectedOption) {
518
- // 不正解で、かつユーザーが選択したボタン
519
- button.classList.add('incorrect');
520
- } else {
521
- // 正解でもなく、ユーザーが選択したものでもないボタン
522
- button.classList.add('other-disabled'); // 他の選択肢用のスタイル
523
- }
524
  });
525
  }
526
  }
@@ -530,13 +425,11 @@ function revealAnswer(selectedOption = null) {
530
  * 次の学習アイテムへ移動
531
  */
532
  function goToNext() {
533
- // ★★★ learningData.items の存在もチェック ★★★
534
  if (learningData && learningData.items && currentItemIndex < learningData.items.length - 1) {
535
  currentItemIndex++;
536
- displayCurrentItem(); // 次のアイテムを表示
537
  } else {
538
  console.log("Already at the last item or no data.");
539
- // 最後のアイテムの場合、完了メッセージなどを表示しても良い
540
  if (learningData && learningData.items && currentItemIndex === learningData.items.length - 1) {
541
  alert("学習セットが完了しました!");
542
  }
@@ -547,10 +440,9 @@ function goToNext() {
547
  * 前の学習アイテムへ移動
548
  */
549
  function goToPrev() {
550
- // ★★★ learningData.items の存在もチェック ★★★
551
  if (learningData && learningData.items && currentItemIndex > 0) {
552
  currentItemIndex--;
553
- displayCurrentItem(); // 前のアイテムを表示
554
  } else {
555
  console.log("Already at the first item or no data.");
556
  }
@@ -564,21 +456,16 @@ function updatePagination() {
564
  const prevButton = document.getElementById('prev-button');
565
  const nextButton = document.getElementById('next-button');
566
 
567
- // 要素の存在チェック
568
  if (!pageInfo || !prevButton || !nextButton) {
569
  console.warn("Pagination elements not found.");
570
  return;
571
  }
572
-
573
- // ★★★ learningData.items の存在をチェック ★★★
574
  if (learningData && learningData.items && learningData.items.length > 0) {
575
  const totalItems = learningData.items.length;
576
  pageInfo.textContent = `${currentItemIndex + 1} / ${totalItems}`;
577
- // ボタンの有効/無効を設定
578
  prevButton.disabled = currentItemIndex === 0;
579
  nextButton.disabled = currentItemIndex === totalItems - 1;
580
  } else {
581
- // データがない場合や空の場合
582
  pageInfo.textContent = '0 / 0';
583
  prevButton.disabled = true;
584
  nextButton.disabled = true;
@@ -597,23 +484,19 @@ function displayLearningError(message) {
597
  const modeIndicator = document.getElementById('mode-indicator');
598
  const tapToShow = document.getElementById('tap-to-show');
599
 
600
- // 各要素が存在すればエラー表示に切り替える
601
  if (titleElement) titleElement.textContent = 'エラー';
602
  if (modeIndicator) modeIndicator.textContent = 'エラー発生';
603
  if (cardElement) {
604
- // カードの内容をエラーメッセージで置き換え
605
- cardElement.innerHTML = `<p class="main-text" style="color: red; text-align: center; padding: 20px;">${message}</p>`;
606
- cardElement.onclick = null; // クリックイベント解除
607
  }
608
- if (paginationElement) paginationElement.style.display = 'none'; // ページネーション非表示
609
  if (optionsArea) {
610
- optionsArea.innerHTML = ''; // 選択肢クリア
611
  optionsArea.style.display = 'none';
612
  }
613
- if (tapToShow) tapToShow.style.display = 'none'; // タップ表示も隠す
614
-
615
- // エラー時はローディング表示も確実に解除
616
- toggleLoading(false);
617
  }
618
 
619
  /**
@@ -621,16 +504,10 @@ function displayLearningError(message) {
621
  */
622
  function showCorrectEffect() {
623
  if (correctEffect) {
624
- clearTimeout(correctEffectTimeout); // 既存のタイマーをクリア
625
- correctEffect.classList.add('show'); // 表示クラスを追加
626
-
627
- // 指定時間後 (例: 1秒) に非表示処理を開始
628
- correctEffectTimeout = setTimeout(() => {
629
- hideCorrectEffect();
630
- }, 1000); // 表示時間 (ミリ秒)
631
- } else {
632
- console.warn("Correct effect element not found.");
633
- }
634
  }
635
 
636
  /**
@@ -638,9 +515,8 @@ function showCorrectEffect() {
638
  */
639
  function hideCorrectEffect() {
640
  if (correctEffect && correctEffect.classList.contains('show')) {
641
- correctEffect.classList.remove('show'); // 表示クラスを削除
642
  }
643
- // タイマーもクリアしておく(手動で非表示にする場合など)
644
  clearTimeout(correctEffectTimeout);
645
  }
646
 
@@ -655,26 +531,14 @@ function hideCorrectEffect() {
655
  function handleToggleChange(checkbox, type) {
656
  const isChecked = checkbox.checked;
657
  console.log(`Toggle changed for ${type}: ${isChecked}`);
658
-
659
  if (type === 'dark') {
660
- // ダークモードの切り替え
661
  document.body.classList.toggle('dark-mode', isChecked);
662
- // 設定をlocalStorageに保存 (エラーハンドリング付き)
663
- try {
664
- localStorage.setItem('darkModeEnabled', isChecked);
665
- console.log(`Dark mode preference saved: ${isChecked}`);
666
- } catch (e) {
667
- console.warn('Could not save dark mode preference to localStorage:', e);
668
- // ユーザーに通知する (任意)
669
- // alert('ダークモード設定の保存に失敗しました。');
670
- }
671
  } else if (type === 'notification') {
672
- // 通知設定の切り替え (未実装)
673
  console.log("Notification setting toggled:", isChecked);
674
- alert(`通知設定は現在未実装です。(設定: ${isChecked ? 'ON' : 'OFF'})`);
675
- // ここに通知設定のAPI呼び出しなどを実装
676
  }
677
- // 他のトグルスイッチの処理もここに追加
678
  }
679
 
680
  /**
@@ -682,14 +546,8 @@ function handleToggleChange(checkbox, type) {
682
  */
683
  function handleLogout() {
684
  console.log("Logout button clicked");
685
- // TODO: 実際のログアウト処理を実装
686
- // - サーバーAPIへのログアウト要求
687
- // - ローカルストレージやセッションストレージの認証情報クリア
688
- // - など
689
-
690
- // 仮の動作: アラート表示と入力画面への遷移
691
- alert("ログアウトしました。(この機能は現在開発中です)");
692
- // navigateTo 内でメニューが閉じる
693
  goToInput();
694
  }
695
 
@@ -698,97 +556,62 @@ function handleLogout() {
698
  */
699
  function applyDarkModePreference() {
700
  try {
701
- // localStorageから 'darkModeEnabled' の値を取得し、'true' かどうかで判定
702
  const darkModeEnabled = localStorage.getItem('darkModeEnabled') === 'true';
703
  document.body.classList.toggle('dark-mode', darkModeEnabled);
704
- console.log(`Applied dark mode preference from localStorage: ${darkModeEnabled}`);
705
- } catch (e) {
706
- console.warn('Could not load or apply dark mode preference from localStorage:', e);
707
- // ここでエラーが発生しても処理は続行する
708
- }
709
  }
710
 
711
 
712
  // --- ページの初期化処理 ---
713
- const pathname = window.location.pathname; // 現在のパスをグローバルスコープで取得
 
714
 
715
- // ★★★ 最初にダークモード設定を適用 (FOUC: Flash of Unstyled Content 対策) ★★★
716
- applyDarkModePreference();
717
-
718
- // ★★★ DOMContentLoaded イベントリスナー: DOM構築完了後に実行 ★★★
719
  document.addEventListener('DOMContentLoaded', () => {
720
- console.log('DOM fully loaded and parsed. Path:', pathname);
721
 
722
- // メニュー要素を取得してグローバル変数に格納
723
  sideMenu = document.getElementById('side-menu');
724
  menuOverlay = document.getElementById('menu-overlay');
725
- if (!sideMenu || !menuOverlay) {
726
- console.warn("Side menu or overlay element not found on this page.");
727
- }
728
 
729
- // learning.html の場合のみ正解エフェクト要素を取得し、初期化関数を呼ぶ
730
  if (pathname.endsWith('/learning') || pathname.endsWith('/learning.html')) {
731
  correctEffect = document.getElementById('correct-effect');
732
- if (!correctEffect) {
733
- console.warn("Correct effect element ('correct-effect') not found on learning page.");
734
- }
735
- initializeLearningScreen(); // learningページ専用の初期化
736
  }
737
 
738
- // input.html の初期化処理
739
  if (pathname === '/' || pathname.endsWith('/input') || pathname.endsWith('/input.html')) {
740
  console.log("Initializing Input page...");
741
  const form = document.getElementById('generate-form');
742
- if (form) {
743
- // フォーム送信イベントにリスナーを設定
744
- form.addEventListener('submit', (event) => {
745
- event.preventDefault(); // デフォルトの送信をキャンセル
746
- handleGenerateSubmit(); // カスタム送信処理を呼び出し
747
- });
748
- } else {
749
- console.warn("Generate form ('generate-form') not found.");
750
  }
751
- // URLパラメータから初期値を設定する例 (任意)
752
- const urlParams = new URLSearchParams(window.location.search);
753
- const initialUrl = urlParams.get('url');
754
- if (initialUrl) {
755
- const urlInput = document.getElementById('youtube-url');
756
- if (urlInput) {
757
- urlInput.value = initialUrl;
758
- console.log("Set initial URL from query parameter:", initialUrl);
759
- }
760
- }
761
  }
762
- // history.html の初期化処理 (必要なら)
763
  else if (pathname.endsWith('/history') || pathname.endsWith('/history.html')) {
764
  console.log("Initializing History page...");
765
- // 例: loadHistoryData(); // 履歴データを読み込んで表示する関数
766
  }
767
- // settings.html の初期化処理
768
  else if (pathname.endsWith('/settings') || pathname.endsWith('/settings.html')) {
769
  console.log("Initializing Settings page...");
770
- // ダークモードトグルの状態をlocalStorageに合わせて更新
771
- try {
772
  const darkModeEnabled = localStorage.getItem('darkModeEnabled') === 'true';
773
- // type='dark' を持つトグルスイッチを探す
774
  const toggle = document.querySelector('input[type="checkbox"][onchange*="dark"]');
775
- if (toggle) {
776
- toggle.checked = darkModeEnabled;
777
- console.log("Set dark mode toggle state to:", darkModeEnabled);
778
- } else {
779
- console.warn("Dark mode toggle switch not found on settings page.");
780
- }
781
- } catch (e) {
782
- console.warn('Could not set dark mode toggle state on settings page:', e);
783
- }
784
  }
785
 
786
- // フッターナビゲーションのアクティブ状態を更新
787
- updateFooterNavActiveState(pathname);
788
-
789
- // ★★★ ページ読み込み時にメニューが意図せず開いた状態なら閉じる ★★★
790
- // (特にブラウザの履歴操作などで発生することがあるため)
791
- closeMenu();
792
  });
793
 
794
 
@@ -797,62 +620,20 @@ document.addEventListener('DOMContentLoaded', () => {
797
  */
798
  function updateFooterNavActiveState(currentPath) {
799
  const footerNav = document.querySelector('.footer-nav');
800
- if (!footerNav) {
801
- // フッターがないページなら何もしない
802
- return;
803
- }
804
-
805
  const buttons = footerNav.querySelectorAll('button');
806
- let foundActive = false; // アクティブなボタンが見つかったか
807
-
808
  buttons.forEach(button => {
809
- button.classList.remove('active'); // まず全て非アクティブに
810
  const onclickAttr = button.getAttribute('onclick');
811
  if (onclickAttr) {
812
- // onclick属性の値から遷移先を判定
813
- if ((currentPath === '/' || currentPath.endsWith('/input') || currentPath.endsWith('/input.html')) && onclickAttr.includes('goToInput')) {
814
- button.classList.add('active');
815
- foundActive = true;
816
- } else if ((currentPath.endsWith('/history') || currentPath.endsWith('/history.html')) && onclickAttr.includes('goToHistory')) {
817
- button.classList.add('active');
818
- foundActive = true;
819
- } else if ((currentPath.endsWith('/settings') || currentPath.endsWith('/settings.html')) && onclickAttr.includes('goToSettings')) {
820
- button.classList.add('active');
821
- foundActive = true;
822
- }
823
- // learning ページは通常フッターをアクティブにしない想定
824
  }
825
  });
826
-
827
- // デバッグ用ログ
828
- // console.log(`Footer nav active state updated for path "${currentPath}". Active button found: ${foundActive}`);
829
  }
830
 
831
 
832
- // --- デバッグ用: 一部関数/変数をグローバルスコープに公開 ---
833
- // 注意: 本番環境では削除またはコメントアウトすることを強く推奨します
834
- window.debug = {
835
- navigateTo,
836
- goToInput,
837
- goToHistory,
838
- goToSettings,
839
- goToLearning,
840
- openMenu,
841
- closeMenu,
842
- handleGenerateSubmit,
843
- initializeLearningScreen,
844
- handleOptionClick,
845
- revealAnswer,
846
- goToNext,
847
- goToPrev,
848
- handleToggleChange,
849
- handleLogout,
850
- showCorrectEffect,
851
- hideCorrectEffect,
852
- learningData, // 現在の学習データ確認用
853
- currentItemIndex, // 現在のインデックス確認用
854
- sideMenu, // メニュー要素確認用
855
- menuOverlay // オーバーレイ要素確認用
856
- };
857
-
858
- // --- END OF FILE script.js ---
 
3
  "use strict"; // より厳格なエラーチェック
4
 
5
  // --- グローバル変数 ---
6
+ let learningData = null; // 学習データ - { title: '...', items: [...] } の形式を想定
7
+ let currentItemIndex = 0; // 現在表示中のアイテムインデックス
8
+ let currentMode = 'quiz'; // 現在のモード 'quiz' or 'summary'
9
+ let correctEffectTimeout; // 正解エフェクト非表示用のタイマーID
10
+ let correctEffect = null; // 正解エフェクト要素
11
  let sideMenu = null; // サイドメニュー要素
12
  let menuOverlay = null; // メニューオーバーレイ要素
13
 
 
19
  */
20
  function navigateTo(url) {
21
  closeMenu(); // 遷移前にメニューを閉じる
 
22
  setTimeout(() => {
23
  window.location.href = url;
24
+ }, 100); // 少し遅延させて遷移
25
  }
26
 
27
  /**
 
32
  if (sideMenu && menuOverlay) {
33
  sideMenu.classList.add('open');
34
  menuOverlay.classList.add('open');
35
+ document.body.classList.add('menu-open');
36
  } else {
37
  console.error("Side menu or overlay element not found. Cannot open menu.");
38
  }
 
42
  * サイドメニューを閉じます。
43
  */
44
  function closeMenu() {
 
45
  if (sideMenu && menuOverlay) {
46
  sideMenu.classList.remove('open');
47
  menuOverlay.classList.remove('open');
48
+ document.body.classList.remove('menu-open');
49
  }
 
50
  }
51
 
52
 
 
62
  if (targetButton && buttonId === 'generate-button') {
63
  const spinner = targetButton.querySelector('.loading-spinner');
64
  const buttonText = targetButton.querySelector('.button-text');
 
65
  if (show) {
66
  targetButton.disabled = true;
67
  if (spinner) spinner.style.display = 'inline-block';
 
69
  } else {
70
  targetButton.disabled = false;
71
  if (spinner) spinner.style.display = 'none';
72
+ if (buttonText) buttonText.textContent = '生成する';
73
  }
74
  }
75
 
76
+ // learning.html 用の汎用ローディング表示
77
  const loadingIndicator = document.getElementById('mode-indicator');
78
  const cardElement = document.getElementById('learning-card');
79
  const paginationElement = document.querySelector('.pagination');
80
  const optionsArea = document.getElementById('options-area');
81
+ const tapToShowElement = document.getElementById('tap-to-show');
82
 
 
83
  const currentPathname = window.location.pathname;
84
  if (currentPathname.endsWith('/learning') || currentPathname.endsWith('/learning.html')) {
85
  if (show) {
 
90
  if (cardElement) cardElement.style.opacity = '0.5';
91
  if (paginationElement) paginationElement.style.display = 'none';
92
  if (optionsArea) optionsArea.style.display = 'none';
93
+ if (tapToShowElement) tapToShowElement.style.display = 'none';
94
  } else {
 
 
95
  if (loadingIndicator) {
 
96
  loadingIndicator.classList.remove('loading');
97
  }
98
  if (cardElement) cardElement.style.opacity = '1';
99
+ // 他の要素の表示は displayCurrentItem に任せる
100
  }
101
  }
102
  }
 
110
  const errorElement = document.getElementById(elementId);
111
  if (errorElement) {
112
  errorElement.textContent = message;
 
113
  errorElement.style.display = message ? 'block' : 'none';
114
  } else {
115
+ if (message) {
 
116
  console.error(`Error element with ID "${elementId}" not found. Cannot display message: ${message}`);
117
  }
118
  }
 
120
 
121
  // --- 画面遷移用関数 ---
122
  function goToInput() {
123
+ navigateTo('/');
124
  }
125
 
126
  function goToHistory() {
 
133
 
134
  function goToLearning(contentId) {
135
  if (contentId) {
 
136
  navigateTo(`/learning?id=${encodeURIComponent(contentId)}`);
137
  } else {
138
  console.error('goToLearning requires a content ID.');
139
+ alert('学習コンテンツのIDが見つかりません。');
 
140
  }
141
  }
142
 
 
149
  async function handleGenerateSubmit() {
150
  const urlInput = document.getElementById('youtube-url');
151
  const youtubeUrl = urlInput.value.trim();
152
+ const errorMsgElementId = 'error-message';
153
+ displayErrorMessage('', errorMsgElementId);
154
 
155
  if (!youtubeUrl) {
156
  displayErrorMessage('YouTubeリンクを入力してください。', errorMsgElementId);
157
+ return false;
158
  }
159
 
 
160
  try {
161
+ const urlObj = new URL(youtubeUrl);
162
  const validHostnames = ['www.youtube.com', 'youtube.com', 'youtu.be'];
163
+ if (!validHostnames.includes(urlObj.hostname)) throw new Error('Invalid hostname');
164
+ if (urlObj.hostname === 'youtu.be' && urlObj.pathname.length <= 1) throw new Error('Missing video ID for youtu.be');
 
 
 
 
 
165
  if (urlObj.hostname.includes('youtube.com')) {
166
+ if (urlObj.pathname === '/watch' && !urlObj.searchParams.has('v')) throw new Error('Missing video ID parameter (v=...) for youtube.com/watch');
167
+ if (urlObj.pathname.startsWith('/shorts/') && urlObj.pathname.length <= 8) throw new Error('Missing video ID for youtube.com/shorts/');
 
 
 
 
 
168
  }
169
  } catch (e) {
 
170
  console.warn("Invalid URL format:", e.message);
171
  displayErrorMessage('有効なYouTube動画のリンクを入力してください。(例: https://www.youtube.com/watch?v=...)', errorMsgElementId);
172
+ return false;
173
  }
174
 
175
+ toggleLoading(true, 'generate-button');
 
176
 
177
  try {
 
178
  const response = await fetch('/api/generate', {
179
  method: 'POST',
180
+ headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
181
+ body: JSON.stringify({ url: youtubeUrl }),
 
 
 
182
  });
183
 
 
184
  let result;
185
  try {
186
  result = await response.json();
187
  } catch (jsonError) {
188
+ console.error('Failed to parse JSON response:', jsonError);
189
+ throw new Error(response.ok ? 'サーバーからの応答形式が不正です。' : `サーバーエラー (${response.status})`);
 
 
 
 
 
 
 
190
  }
191
 
192
+ // ★★★ サーバーが返すJSON構造に合わせてチェック ★★★
193
+ // { success: true, data: { id: '...' } } を期待
 
194
  if (response.ok && result && typeof result === 'object' && result.success && result.data && result.data.id) {
 
195
  console.log("Generation successful, navigating to learning page with ID:", result.data.id);
196
  goToLearning(result.data.id);
197
  } else {
 
198
  console.error('Generation API call failed or returned unexpected structure:', result);
199
+ const serverMessage = (result && typeof result === 'object' && result.message) ||
200
+ (result && typeof result === 'object' && result.error && result.error.message) ||
201
+ (response.ok ? '生成に失敗しました (不明な応答形式)。' : `サーバーエラー (${response.status})`);
202
  displayErrorMessage(serverMessage, errorMsgElementId);
203
  }
204
 
205
  } catch (error) {
 
206
  console.error('Error during generation request:', error);
207
+ const userMessage = error.message.includes('Failed to fetch') ? 'サーバーに接続できませんでした。' : `エラー: ${error.message}`;
208
+ displayErrorMessage(userMessage, errorMsgElementId);
 
 
 
 
209
  } finally {
210
+ toggleLoading(false, 'generate-button');
211
  }
212
+ return false;
 
213
  }
214
 
215
 
 
223
  const params = new URLSearchParams(window.location.search);
224
  const contentId = params.get('id');
225
 
 
 
226
  if (!contentId) {
227
  displayLearningError('学習コンテンツのIDが見つかりません。');
228
  return;
229
  }
230
  console.log('Content ID:', contentId);
231
+ toggleLoading(true);
 
232
 
233
  try {
234
  const response = await fetch(`/api/learning/${contentId}`);
235
  if (!response.ok) {
 
236
  let errorMessage = `サーバーからのデータ取得に失敗 (${response.status})`;
237
  try {
238
  const errorData = await response.json();
239
  errorMessage = errorData.message || errorMessage;
240
+ } catch (e) { console.warn('Failed to parse error response as JSON.'); }
 
 
241
  throw new Error(errorMessage);
242
  }
243
 
244
+ // ★★★ サーバーからの応答がオブジェクト {success: ..., data: {...}} であると想定 ★★★
245
+ const result = await response.json();
246
+ console.log('Fetched data object:', result);
247
 
248
+ // 応答の形式をチェック
249
+ if (!result || typeof result !== 'object' || !result.success || !result.data || !Array.isArray(result.data.items)) {
250
+ console.error('Invalid data structure received from server:', result);
251
+ throw new Error('サーバーから受け取ったデータの形式が正しくありません。');
 
 
252
  }
253
 
254
+ // ★★★ learningData を構築 (title と items を data から取得) ★★★
255
  learningData = {
256
+ title: result.data.title || `学習セット (${contentId})`, // data.title があればそれを使う
257
+ items: result.data.items // data.items を使う
258
  };
259
 
260
+ // アイテム配列が空でないかチェック
261
+ if (learningData.items.length === 0) {
262
+ throw new Error('学習データが見つかりませんでした (アイテムが0件)。');
263
+ }
264
+
265
+ // タイトルを設定
266
  const titleElement = document.getElementById('learning-title');
267
  if (titleElement) {
268
+ titleElement.textContent = learningData.title;
269
  } else {
270
  console.warn("Title element ('learning-title') not found.");
271
  }
 
275
  displayCurrentItem();
276
 
277
  } catch (error) {
 
278
  console.error('Error initializing learning screen:', error);
279
+ const message = (error instanceof SyntaxError) ? `サーバー応答の解析エラー: ${error.message}` : `読み込みエラー: ${error.message}`;
280
+ displayLearningError(message);
 
 
 
 
281
  } finally {
282
+ toggleLoading(false);
283
  }
284
  }
285
 
 
287
  * 現在の学習アイテムをカードに表示 (クイズ or 要約)
288
  */
289
  function displayCurrentItem() {
290
+ hideCorrectEffect();
291
+ clearTimeout(correctEffectTimeout);
292
 
 
293
  const cardElement = document.getElementById('learning-card');
294
  const cardTextElement = document.getElementById('card-text');
295
  const answerTextElement = document.getElementById('answer-text');
 
297
  const optionsArea = document.getElementById('options-area');
298
  const modeIndicator = document.getElementById('mode-indicator');
299
 
 
300
  if (!cardElement || !cardTextElement || !answerTextElement || !tapToShowElement || !optionsArea || !modeIndicator) {
301
+ console.error("One or more required learning elements are missing.");
302
+ displayLearningError("画面表示に必要な要素が見つかりません。");
303
  return;
304
  }
 
 
305
  if (!learningData || !learningData.items || currentItemIndex < 0 || currentItemIndex >= learningData.items.length) {
306
  console.error('Invalid learning data or index:', learningData, currentItemIndex);
307
  displayLearningError('表示する学習データが見つかりません。');
308
  return;
309
  }
310
 
 
311
  const item = learningData.items[currentItemIndex];
312
 
313
+ // リセット
314
+ cardTextElement.innerHTML = '';
315
  answerTextElement.style.display = 'none';
316
+ answerTextElement.textContent = '';
317
  tapToShowElement.style.display = 'none';
318
+ optionsArea.innerHTML = '';
319
+ optionsArea.style.display = 'none';
320
+ modeIndicator.classList.remove('loading');
321
 
322
+ // ★★★ サーバーレスポンスのキーに合わせて item.text を使用 (変更なし) ★★★
323
+ if (item.type === 'question' && item.text && item.answer) { // questionではなくtextキーで問題文を取得
 
324
  currentMode = 'quiz';
325
  modeIndicator.textContent = 'クイズモード';
326
+ cardTextElement.textContent = item.text; // 問題文
327
+ answerTextElement.textContent = `答え: ${item.answer}`;
 
328
 
 
329
  if (item.options && Array.isArray(item.options) && item.options.length > 0) {
330
+ optionsArea.style.display = 'block';
331
+ item.options.forEach(option => {
 
 
332
  const button = document.createElement('button');
333
  button.classList.add('option-button');
334
  button.textContent = option;
 
335
  button.onclick = () => handleOptionClick(option);
336
  optionsArea.appendChild(button);
337
  });
338
+ tapToShowElement.style.display = 'block';
339
  } else {
 
340
  console.warn(`Quiz item ${currentItemIndex} has no options.`);
341
+ tapToShowElement.style.display = 'block'; // 選択肢なくても解答表示は可能
342
  }
343
+ cardElement.onclick = () => revealAnswer();
344
+ tapToShowElement.onclick = () => revealAnswer();
 
345
 
346
  } else if (item.type === 'summary' && item.text) {
347
  currentMode = 'summary';
348
  modeIndicator.textContent = '要約モード';
 
349
  cardTextElement.innerHTML = item.text.replace(/\n/g, '<br>');
 
 
350
  cardElement.onclick = null;
351
  tapToShowElement.style.display = 'none';
352
+ optionsArea.style.display = 'none';
353
 
354
  } else {
 
355
  console.warn('Unknown or invalid item type/data:', item);
356
  currentMode = 'unknown';
357
  modeIndicator.textContent = 'データエラー';
358
+ cardTextElement.textContent = `[不正なデータ形式] ${item.text || 'この項目を表示できません。'}`; // item.textを表示試行
 
359
  cardElement.onclick = null;
360
  tapToShowElement.style.display = 'none';
361
  optionsArea.style.display = 'none';
362
  }
363
 
364
+ updatePagination();
365
  }
366
 
367
  /**
 
369
  * @param {string} selectedOption - ユーザーが選択した選択肢のテキスト
370
  */
371
  function handleOptionClick(selectedOption) {
 
372
  if (currentMode !== 'quiz' || !learningData || !learningData.items || !learningData.items[currentItemIndex]) return;
373
 
 
374
  const answerTextElement = document.getElementById('answer-text');
375
+ if (answerTextElement && answerTextElement.style.display === 'block') return; // 解答表示済み
 
 
 
376
 
 
377
  const currentItem = learningData.items[currentItemIndex];
378
  const correctAnswer = currentItem.answer;
379
  const isCorrect = selectedOption === correctAnswer;
380
 
381
  if (isCorrect) {
382
  console.log("Correct!");
383
+ showCorrectEffect();
384
  } else {
385
  console.log("Incorrect...");
 
386
  }
 
 
387
  revealAnswer(selectedOption);
388
  }
389
 
 
393
  * @param {string|null} [selectedOption=null] - ユーザーが選択した選択肢。nullの場合はカードタップ等による表示。
394
  */
395
  function revealAnswer(selectedOption = null) {
 
396
  if (currentMode !== 'quiz' || !learningData || !learningData.items || !learningData.items[currentItemIndex]) return;
397
 
398
  const answerTextElement = document.getElementById('answer-text');
399
  const tapToShowElement = document.getElementById('tap-to-show');
400
  const optionsArea = document.getElementById('options-area');
401
+ const cardElement = document.getElementById('learning-card');
402
 
403
+ if (answerTextElement && answerTextElement.style.display === 'block') return; // 表示済み
 
 
 
404
 
405
+ if (answerTextElement) answerTextElement.style.display = 'block';
406
+ if (tapToShowElement) tapToShowElement.style.display = 'none';
407
+ if (cardElement) cardElement.onclick = null;
 
 
 
 
 
 
 
 
 
408
 
 
409
  if (optionsArea) {
 
410
  const correctAnswer = learningData.items[currentItemIndex].answer;
411
+ const buttons = optionsArea.querySelectorAll('.option-button');
 
412
  buttons.forEach(button => {
413
+ button.disabled = true;
414
+ button.onclick = null;
 
415
  const buttonText = button.textContent;
416
+ if (buttonText === correctAnswer) button.classList.add('correct');
417
+ else if (buttonText === selectedOption) button.classList.add('incorrect');
418
+ else button.classList.add('other-disabled');
 
 
 
 
 
 
 
419
  });
420
  }
421
  }
 
425
  * 次の学習アイテムへ移動
426
  */
427
  function goToNext() {
 
428
  if (learningData && learningData.items && currentItemIndex < learningData.items.length - 1) {
429
  currentItemIndex++;
430
+ displayCurrentItem();
431
  } else {
432
  console.log("Already at the last item or no data.");
 
433
  if (learningData && learningData.items && currentItemIndex === learningData.items.length - 1) {
434
  alert("学習セットが完了しました!");
435
  }
 
440
  * 前の学習アイテムへ移動
441
  */
442
  function goToPrev() {
 
443
  if (learningData && learningData.items && currentItemIndex > 0) {
444
  currentItemIndex--;
445
+ displayCurrentItem();
446
  } else {
447
  console.log("Already at the first item or no data.");
448
  }
 
456
  const prevButton = document.getElementById('prev-button');
457
  const nextButton = document.getElementById('next-button');
458
 
 
459
  if (!pageInfo || !prevButton || !nextButton) {
460
  console.warn("Pagination elements not found.");
461
  return;
462
  }
 
 
463
  if (learningData && learningData.items && learningData.items.length > 0) {
464
  const totalItems = learningData.items.length;
465
  pageInfo.textContent = `${currentItemIndex + 1} / ${totalItems}`;
 
466
  prevButton.disabled = currentItemIndex === 0;
467
  nextButton.disabled = currentItemIndex === totalItems - 1;
468
  } else {
 
469
  pageInfo.textContent = '0 / 0';
470
  prevButton.disabled = true;
471
  nextButton.disabled = true;
 
484
  const modeIndicator = document.getElementById('mode-indicator');
485
  const tapToShow = document.getElementById('tap-to-show');
486
 
 
487
  if (titleElement) titleElement.textContent = 'エラー';
488
  if (modeIndicator) modeIndicator.textContent = 'エラー発生';
489
  if (cardElement) {
490
+ cardElement.innerHTML = `<p class="main-text error-text">${message}</p>`; // CSSでスタイル調整用クラス追加
491
+ cardElement.onclick = null;
 
492
  }
493
+ if (paginationElement) paginationElement.style.display = 'none';
494
  if (optionsArea) {
495
+ optionsArea.innerHTML = '';
496
  optionsArea.style.display = 'none';
497
  }
498
+ if (tapToShow) tapToShow.style.display = 'none';
499
+ toggleLoading(false); // エラー時はローディング解除
 
 
500
  }
501
 
502
  /**
 
504
  */
505
  function showCorrectEffect() {
506
  if (correctEffect) {
507
+ clearTimeout(correctEffectTimeout);
508
+ correctEffect.classList.add('show');
509
+ correctEffectTimeout = setTimeout(() => { hideCorrectEffect(); }, 1000);
510
+ } else { console.warn("Correct effect element not found."); }
 
 
 
 
 
 
511
  }
512
 
513
  /**
 
515
  */
516
  function hideCorrectEffect() {
517
  if (correctEffect && correctEffect.classList.contains('show')) {
518
+ correctEffect.classList.remove('show');
519
  }
 
520
  clearTimeout(correctEffectTimeout);
521
  }
522
 
 
531
  function handleToggleChange(checkbox, type) {
532
  const isChecked = checkbox.checked;
533
  console.log(`Toggle changed for ${type}: ${isChecked}`);
 
534
  if (type === 'dark') {
 
535
  document.body.classList.toggle('dark-mode', isChecked);
536
+ try { localStorage.setItem('darkModeEnabled', isChecked); }
537
+ catch (e) { console.warn('Could not save dark mode preference:', e); }
 
 
 
 
 
 
 
538
  } else if (type === 'notification') {
 
539
  console.log("Notification setting toggled:", isChecked);
540
+ alert(`通知設定未実装 (設定: ${isChecked ? 'ON' : 'OFF'})`);
 
541
  }
 
542
  }
543
 
544
  /**
 
546
  */
547
  function handleLogout() {
548
  console.log("Logout button clicked");
549
+ // TODO: 実際のログアウト処理
550
+ alert("ログアウトしました。(開発中)");
 
 
 
 
 
 
551
  goToInput();
552
  }
553
 
 
556
  */
557
  function applyDarkModePreference() {
558
  try {
 
559
  const darkModeEnabled = localStorage.getItem('darkModeEnabled') === 'true';
560
  document.body.classList.toggle('dark-mode', darkModeEnabled);
561
+ // console.log(`Applied dark mode preference: ${darkModeEnabled}`);
562
+ } catch (e) { console.warn('Could not load/apply dark mode preference:', e); }
 
 
 
563
  }
564
 
565
 
566
  // --- ページの初期化処理 ---
567
+ const pathname = window.location.pathname;
568
+ applyDarkModePreference(); // 最初に適用
569
 
 
 
 
 
570
  document.addEventListener('DOMContentLoaded', () => {
571
+ console.log('DOM fully loaded. Path:', pathname);
572
 
 
573
  sideMenu = document.getElementById('side-menu');
574
  menuOverlay = document.getElementById('menu-overlay');
575
+ if (!sideMenu || !menuOverlay) console.warn("Menu elements not found.");
 
 
576
 
577
+ // learningページ固有の初期化
578
  if (pathname.endsWith('/learning') || pathname.endsWith('/learning.html')) {
579
  correctEffect = document.getElementById('correct-effect');
580
+ if (!correctEffect) console.warn("Correct effect element not found.");
581
+ initializeLearningScreen();
 
 
582
  }
583
 
584
+ // inputページ固有の初期化
585
  if (pathname === '/' || pathname.endsWith('/input') || pathname.endsWith('/input.html')) {
586
  console.log("Initializing Input page...");
587
  const form = document.getElementById('generate-form');
588
+ if (form) form.addEventListener('submit', (e) => { e.preventDefault(); handleGenerateSubmit(); });
589
+ else console.warn("Generate form not found.");
590
+ const urlParams = new URLSearchParams(window.location.search);
591
+ const initialUrl = urlParams.get('url');
592
+ if (initialUrl) {
593
+ const urlInput = document.getElementById('youtube-url');
594
+ if (urlInput) urlInput.value = initialUrl;
 
595
  }
 
 
 
 
 
 
 
 
 
 
596
  }
597
+ // historyページ固有の初期化
598
  else if (pathname.endsWith('/history') || pathname.endsWith('/history.html')) {
599
  console.log("Initializing History page...");
600
+ // loadHistoryData();
601
  }
602
+ // settingsページ固有の初期化
603
  else if (pathname.endsWith('/settings') || pathname.endsWith('/settings.html')) {
604
  console.log("Initializing Settings page...");
605
+ try { // ダークモードトグル状態設定
 
606
  const darkModeEnabled = localStorage.getItem('darkModeEnabled') === 'true';
 
607
  const toggle = document.querySelector('input[type="checkbox"][onchange*="dark"]');
608
+ if (toggle) toggle.checked = darkModeEnabled;
609
+ else console.warn("Dark mode toggle not found.");
610
+ } catch (e) { console.warn('Could not set dark mode toggle state:', e); }
 
 
 
 
 
 
611
  }
612
 
613
+ updateFooterNavActiveState(pathname); // フッターアクティブ状態更新
614
+ closeMenu(); // 念のためメニューを閉じる
 
 
 
 
615
  });
616
 
617
 
 
620
  */
621
  function updateFooterNavActiveState(currentPath) {
622
  const footerNav = document.querySelector('.footer-nav');
623
+ if (!footerNav) return;
 
 
 
 
624
  const buttons = footerNav.querySelectorAll('button');
 
 
625
  buttons.forEach(button => {
626
+ button.classList.remove('active');
627
  const onclickAttr = button.getAttribute('onclick');
628
  if (onclickAttr) {
629
+ if ((currentPath === '/' || currentPath.endsWith('/input') || currentPath.endsWith('/input.html')) && onclickAttr.includes('goToInput')) button.classList.add('active');
630
+ else if ((currentPath.endsWith('/history') || currentPath.endsWith('/history.html')) && onclickAttr.includes('goToHistory')) button.classList.add('active');
631
+ else if ((currentPath.endsWith('/settings') || currentPath.endsWith('/settings.html')) && onclickAttr.includes('goToSettings')) button.classList.add('active');
 
 
 
 
 
 
 
 
 
632
  }
633
  });
 
 
 
634
  }
635
 
636
 
637
+ // --- デバッグ用グローバル公開 ---
638
+ // 本番環境では削除推奨
639
+