| |
| |
| "use strict"; |
|
|
| |
| let learningData = null; |
| let currentItemIndex = 0; |
| let currentMode = 'quiz'; |
| let correctEffectTimeout; |
| let correctEffect = null; |
| let sideMenu = null; |
| let menuOverlay = null; |
|
|
| |
|
|
| |
| |
| |
| |
| function navigateTo(url) { |
| closeMenu(); |
| setTimeout(() => { |
| window.location.href = url; |
| }, 100); |
| } |
|
|
| |
| |
| |
| function openMenu() { |
| console.log("Opening menu..."); |
| if (sideMenu && menuOverlay) { |
| sideMenu.classList.add('open'); |
| menuOverlay.classList.add('open'); |
| document.body.classList.add('menu-open'); |
| } else { |
| console.error("Side menu or overlay element not found. Cannot open menu."); |
| } |
| } |
|
|
| |
| |
| |
| function closeMenu() { |
| if (sideMenu && menuOverlay) { |
| sideMenu.classList.remove('open'); |
| menuOverlay.classList.remove('open'); |
| document.body.classList.remove('menu-open'); |
| } |
| } |
|
|
|
|
| |
| |
| |
| |
| |
| function toggleLoading(show, buttonId = 'generate-button') { |
| const targetButton = document.getElementById(buttonId); |
|
|
| |
| if (targetButton && buttonId === 'generate-button') { |
| const spinner = targetButton.querySelector('.loading-spinner'); |
| const buttonText = targetButton.querySelector('.button-text'); |
| if (show) { |
| targetButton.disabled = true; |
| if (spinner) spinner.style.display = 'inline-block'; |
| if (buttonText) buttonText.textContent = '生成中...'; |
| } else { |
| targetButton.disabled = false; |
| if (spinner) spinner.style.display = 'none'; |
| if (buttonText) buttonText.textContent = '生成する'; |
| } |
| } |
|
|
| |
| const loadingIndicator = document.getElementById('mode-indicator'); |
| const cardElement = document.getElementById('learning-card'); |
| const paginationElement = document.querySelector('.pagination'); |
| const optionsArea = document.getElementById('options-area'); |
| const tapToShowElement = document.getElementById('tap-to-show'); |
|
|
| const currentPathname = window.location.pathname; |
| if (currentPathname.endsWith('/learning') || currentPathname.endsWith('/learning.html')) { |
| if (show) { |
| |
| if (loadingIndicator) { |
| loadingIndicator.textContent = '読み込み中...'; |
| loadingIndicator.classList.add('loading'); |
| } |
| if (cardElement) cardElement.style.opacity = '0.5'; |
| if (paginationElement) paginationElement.style.display = 'none'; |
| if (optionsArea) optionsArea.style.display = 'none'; |
| if (tapToShowElement) tapToShowElement.style.display = 'none'; |
| } else { |
| |
| if (loadingIndicator) { |
| loadingIndicator.classList.remove('loading'); |
| |
| } |
| if (cardElement) cardElement.style.opacity = '1'; |
| |
| if (paginationElement) paginationElement.style.display = 'flex'; |
| |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| function displayErrorMessage(message, elementId = 'error-message') { |
| const errorElement = document.getElementById(elementId); |
| if (errorElement) { |
| errorElement.textContent = message; |
| errorElement.style.display = message ? 'block' : 'none'; |
| } else { |
| if (message) { |
| console.error(`Error element with ID "${elementId}" not found. Cannot display message: ${message}`); |
| } |
| } |
| } |
|
|
| |
| function goToInput() { |
| navigateTo('/input'); |
| } |
|
|
| function goToHistory() { |
| navigateTo('/history'); |
| } |
|
|
| function goToSettings() { |
| navigateTo('/settings'); |
| } |
|
|
| function goToLearning(contentId) { |
| if (contentId) { |
| navigateTo(`/learning?id=${encodeURIComponent(contentId)}`); |
| } else { |
| console.error('goToLearning requires a content ID.'); |
| alert('学習コンテンツのIDが見つかりません。'); |
| } |
| } |
|
|
|
|
| |
|
|
| |
| |
| |
| async function handleGenerateSubmit() { |
| const urlInput = document.getElementById('youtube-url'); |
| const youtubeUrl = urlInput.value.trim(); |
| const errorMsgElementId = 'error-message'; |
| displayErrorMessage('', errorMsgElementId); |
|
|
| if (!youtubeUrl) { |
| displayErrorMessage('YouTubeリンクを入力してください。', errorMsgElementId); |
| return false; |
| } |
|
|
| try { |
| const urlObj = new URL(youtubeUrl); |
| const validHostnames = ['www.youtube.com', 'youtube.com', 'youtu.be']; |
| if (!validHostnames.includes(urlObj.hostname)) throw new Error('Invalid hostname'); |
| if (urlObj.hostname === 'youtu.be' && urlObj.pathname.length <= 1) throw new Error('Missing video ID for youtu.be'); |
| if (urlObj.hostname.includes('youtube.com')) { |
| if (urlObj.pathname === '/watch' && !urlObj.searchParams.has('v')) throw new Error('Missing video ID parameter (v=...) for youtube.com/watch'); |
| if (urlObj.pathname.startsWith('/shorts/') && urlObj.pathname.length <= 8) throw new Error('Missing video ID for youtube.com/shorts/'); |
| } |
| } catch (e) { |
| console.warn("Invalid URL format:", e.message); |
| displayErrorMessage('有効なYouTube動画のリンクを入力してください。(例: https://www.youtube.com/watch?v=...)', errorMsgElementId); |
| return false; |
| } |
|
|
| toggleLoading(true, 'generate-button'); |
|
|
| try { |
| const response = await fetch('/api/generate', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, |
| body: JSON.stringify({ url: youtubeUrl }), |
| }); |
|
|
| let result; |
| try { |
| result = await response.json(); |
| } catch (jsonError) { |
| console.error('Failed to parse JSON response:', jsonError); |
| throw new Error(response.ok ? 'サーバーからの応答形式が不正です。' : `サーバーエラー (${response.status})`); |
| } |
|
|
| |
| if (response.ok && result && typeof result === 'object' && result.success && result.data && result.data.id) { |
| console.log("Generation successful, navigating to learning page with ID:", result.data.id); |
| goToLearning(result.data.id); |
| } else { |
| console.error('Generation API call failed or returned unexpected structure:', result); |
| const serverMessage = (result && typeof result === 'object' && result.message) || |
| (result && typeof result === 'object' && result.error && result.error.message) || |
| (response.ok ? '生成に失敗しました (不明な応答形式)。' : `サーバーエラー (${response.status})`); |
| displayErrorMessage(serverMessage, errorMsgElementId); |
| } |
|
|
| } catch (error) { |
| console.error('Error during generation request:', error); |
| const userMessage = error.message.includes('Failed to fetch') ? 'サーバーに接続できませんでした。' : `エラー: ${error.message}`; |
| displayErrorMessage(userMessage, errorMsgElementId); |
| } finally { |
| toggleLoading(false, 'generate-button'); |
| } |
| return false; |
| } |
|
|
|
|
| |
|
|
| |
| |
| |
| async function initializeLearningScreen() { |
| console.log('Initializing Learning Screen...'); |
| const params = new URLSearchParams(window.location.search); |
| const contentId = params.get('id'); |
|
|
| if (!contentId) { |
| displayLearningError('学習コンテンツのIDが見つかりません。'); |
| return; |
| } |
| console.log('Content ID:', contentId); |
| toggleLoading(true); |
|
|
| try { |
| const response = await fetch(`/api/learning/${contentId}`); |
| if (!response.ok) { |
| let errorMessage = `サーバーからのデータ取得に失敗 (${response.status})`; |
| try { |
| const errorData = await response.json(); |
| errorMessage = errorData.message || errorMessage; |
| } catch (e) { console.warn('Failed to parse error response as JSON.'); } |
| throw new Error(errorMessage); |
| } |
|
|
| |
| const result = await response.json(); |
| console.log('Fetched data object:', result); |
|
|
| |
| if (!result || typeof result !== 'object' || !result.success || !result.data || !Array.isArray(result.data.items)) { |
| console.error('Invalid data structure received from server:', result); |
| throw new Error('サーバーから受け取ったデータの形式が正しくありません。'); |
| } |
|
|
| |
| learningData = { |
| title: result.data.title || `学習セット (${contentId})`, |
| items: result.data.items |
| }; |
|
|
| |
| if (learningData.items.length === 0) { |
| throw new Error('学習データが見つかりませんでした (アイテムが0件)。'); |
| } |
|
|
| |
| const titleElement = document.getElementById('learning-title'); |
| if (titleElement) { |
| titleElement.textContent = learningData.title; |
| } else { |
| console.warn("Title element ('learning-title') not found."); |
| } |
|
|
| |
| currentItemIndex = 0; |
| displayCurrentItem(); |
|
|
| } catch (error) { |
| console.error('Error initializing learning screen:', error); |
| const message = (error instanceof SyntaxError) ? `サーバー応答の解析エラー: ${error.message}` : `読み込みエラー: ${error.message}`; |
| displayLearningError(message); |
| } finally { |
| |
| |
| toggleLoading(false); |
| } |
| } |
|
|
| |
| |
| |
| function displayCurrentItem() { |
| hideCorrectEffect(); |
| clearTimeout(correctEffectTimeout); |
|
|
| const cardElement = document.getElementById('learning-card'); |
| const cardTextElement = document.getElementById('card-text'); |
| const answerTextElement = document.getElementById('answer-text'); |
| const tapToShowElement = document.getElementById('tap-to-show'); |
| const optionsArea = document.getElementById('options-area'); |
| const modeIndicator = document.getElementById('mode-indicator'); |
|
|
| if (!cardElement || !cardTextElement || !answerTextElement || !tapToShowElement || !optionsArea || !modeIndicator) { |
| console.error("One or more required learning elements are missing."); |
| displayLearningError("画面表示に必要な要素が見つかりません。"); |
| return; |
| } |
| if (!learningData || !learningData.items || currentItemIndex < 0 || currentItemIndex >= learningData.items.length) { |
| console.error('Invalid learning data or index:', learningData, currentItemIndex); |
| displayLearningError('表示する学習データが見つかりません。'); |
| return; |
| } |
|
|
| const item = learningData.items[currentItemIndex]; |
|
|
| |
| cardTextElement.innerHTML = ''; |
| answerTextElement.style.display = 'none'; |
| answerTextElement.textContent = ''; |
| tapToShowElement.style.display = 'none'; |
| optionsArea.innerHTML = ''; |
| optionsArea.style.display = 'none'; |
| modeIndicator.classList.remove('loading'); |
|
|
| |
| if (item.type === 'question' && item.text && item.answer) { |
| currentMode = 'quiz'; |
| modeIndicator.textContent = 'クイズモード'; |
| cardTextElement.textContent = item.text; |
| answerTextElement.textContent = `答え: ${item.answer}`; |
|
|
| if (item.options && Array.isArray(item.options) && item.options.length > 0) { |
| optionsArea.style.display = 'block'; |
| item.options.forEach(option => { |
| const button = document.createElement('button'); |
| button.classList.add('option-button'); |
| button.textContent = option; |
| button.onclick = () => handleOptionClick(option); |
| optionsArea.appendChild(button); |
| }); |
| tapToShowElement.style.display = 'block'; |
| } else { |
| console.warn(`Quiz item ${currentItemIndex} has no options.`); |
| tapToShowElement.style.display = 'block'; |
| } |
| cardElement.onclick = () => revealAnswer(); |
| tapToShowElement.onclick = () => revealAnswer(); |
|
|
| } else if (item.type === 'summary' && item.text) { |
| currentMode = 'summary'; |
| modeIndicator.textContent = '要約モード'; |
| cardTextElement.innerHTML = item.text.replace(/\n/g, '<br>'); |
| cardElement.onclick = null; |
| tapToShowElement.style.display = 'none'; |
| optionsArea.style.display = 'none'; |
|
|
| } else { |
| console.warn('Unknown or invalid item type/data:', item); |
| currentMode = 'unknown'; |
| modeIndicator.textContent = 'データエラー'; |
| cardTextElement.textContent = `[不正なデータ形式] ${item.text || 'この項目を表示できません。'}`; |
| cardElement.onclick = null; |
| tapToShowElement.style.display = 'none'; |
| optionsArea.style.display = 'none'; |
| } |
|
|
| updatePagination(); |
| } |
|
|
| |
| |
| |
| |
| function handleOptionClick(selectedOption) { |
| if (currentMode !== 'quiz' || !learningData || !learningData.items || !learningData.items[currentItemIndex]) return; |
|
|
| const answerTextElement = document.getElementById('answer-text'); |
| if (answerTextElement && answerTextElement.style.display === 'block') return; |
|
|
| const currentItem = learningData.items[currentItemIndex]; |
| const correctAnswer = currentItem.answer; |
| const isCorrect = selectedOption === correctAnswer; |
|
|
| if (isCorrect) { |
| console.log("Correct!"); |
| showCorrectEffect(); |
| } else { |
| console.log("Incorrect..."); |
| } |
| revealAnswer(selectedOption); |
| } |
|
|
|
|
| |
| |
| |
| |
| function revealAnswer(selectedOption = null) { |
| if (currentMode !== 'quiz' || !learningData || !learningData.items || !learningData.items[currentItemIndex]) return; |
|
|
| const answerTextElement = document.getElementById('answer-text'); |
| const tapToShowElement = document.getElementById('tap-to-show'); |
| const optionsArea = document.getElementById('options-area'); |
| const cardElement = document.getElementById('learning-card'); |
|
|
| if (answerTextElement && answerTextElement.style.display === 'block') return; |
|
|
| if (answerTextElement) answerTextElement.style.display = 'block'; |
| if (tapToShowElement) tapToShowElement.style.display = 'none'; |
| if (cardElement) cardElement.onclick = null; |
|
|
| if (optionsArea) { |
| const correctAnswer = learningData.items[currentItemIndex].answer; |
| const buttons = optionsArea.querySelectorAll('.option-button'); |
| buttons.forEach(button => { |
| button.disabled = true; |
| button.onclick = null; |
| const buttonText = button.textContent; |
| if (buttonText === correctAnswer) button.classList.add('correct'); |
| else if (buttonText === selectedOption) button.classList.add('incorrect'); |
| else button.classList.add('other-disabled'); |
| }); |
| } |
| } |
|
|
|
|
| |
| |
| |
| function goToNext() { |
| if (learningData && learningData.items && currentItemIndex < learningData.items.length - 1) { |
| currentItemIndex++; |
| displayCurrentItem(); |
| } else { |
| console.log("Already at the last item or no data."); |
| if (learningData && learningData.items && currentItemIndex === learningData.items.length - 1) { |
| alert("学習セットが完了しました!"); |
| } |
| } |
| } |
|
|
| |
| |
| |
| function goToPrev() { |
| if (learningData && learningData.items && currentItemIndex > 0) { |
| currentItemIndex--; |
| displayCurrentItem(); |
| } else { |
| console.log("Already at the first item or no data."); |
| } |
| } |
|
|
| |
| |
| |
| function updatePagination() { |
| const pageInfo = document.getElementById('page-info'); |
| const prevButton = document.getElementById('prev-button'); |
| const nextButton = document.getElementById('next-button'); |
|
|
| if (!pageInfo || !prevButton || !nextButton) { |
| console.warn("Pagination elements not found."); |
| return; |
| } |
| |
| |
| if (learningData && learningData.items && learningData.items.length > 0) { |
| const totalItems = learningData.items.length; |
| pageInfo.textContent = `${currentItemIndex + 1} / ${totalItems}`; |
| prevButton.disabled = currentItemIndex === 0; |
| nextButton.disabled = currentItemIndex === totalItems - 1; |
| } else { |
| pageInfo.textContent = '0 / 0'; |
| prevButton.disabled = true; |
| nextButton.disabled = true; |
| } |
| } |
|
|
| |
| |
| |
| |
| function displayLearningError(message) { |
| const cardElement = document.getElementById('learning-card'); |
| const titleElement = document.getElementById('learning-title'); |
| const paginationElement = document.querySelector('.pagination'); |
| const optionsArea = document.getElementById('options-area'); |
| const modeIndicator = document.getElementById('mode-indicator'); |
| const tapToShow = document.getElementById('tap-to-show'); |
|
|
| if (titleElement) titleElement.textContent = 'エラー'; |
| if (modeIndicator) modeIndicator.textContent = 'エラー発生'; |
| if (cardElement) { |
| cardElement.innerHTML = `<p class="main-text error-text">${message}</p>`; |
| cardElement.onclick = null; |
| } |
| |
| if (paginationElement) paginationElement.style.display = 'none'; |
| if (optionsArea) { |
| optionsArea.innerHTML = ''; |
| optionsArea.style.display = 'none'; |
| } |
| if (tapToShow) tapToShow.style.display = 'none'; |
| |
| toggleLoading(false); |
| } |
|
|
| |
| |
| |
| function showCorrectEffect() { |
| if (correctEffect) { |
| clearTimeout(correctEffectTimeout); |
| correctEffect.classList.add('show'); |
| correctEffectTimeout = setTimeout(() => { hideCorrectEffect(); }, 1000); |
| } else { console.warn("Correct effect element not found."); } |
| } |
|
|
| |
| |
| |
| function hideCorrectEffect() { |
| if (correctEffect && correctEffect.classList.contains('show')) { |
| correctEffect.classList.remove('show'); |
| } |
| clearTimeout(correctEffectTimeout); |
| } |
|
|
|
|
| |
|
|
| |
| |
| |
| |
| |
| function handleToggleChange(checkbox, type) { |
| const isChecked = checkbox.checked; |
| console.log(`Toggle changed for ${type}: ${isChecked}`); |
| if (type === 'dark') { |
| document.body.classList.toggle('dark-mode', isChecked); |
| try { localStorage.setItem('darkModeEnabled', isChecked); } |
| catch (e) { console.warn('Could not save dark mode preference:', e); } |
| } else if (type === 'notification') { |
| console.log("Notification setting toggled:", isChecked); |
| alert(`通知設定未実装 (設定: ${isChecked ? 'ON' : 'OFF'})`); |
| } |
| } |
|
|
| |
| |
| |
| function handleLogout() { |
| console.log("Logout button clicked"); |
| |
| alert("ログアウトしました。(開発中)"); |
| goToInput(); |
| } |
|
|
| |
| |
| |
| function applyDarkModePreference() { |
| try { |
| const darkModeEnabled = localStorage.getItem('darkModeEnabled') === 'true'; |
| document.body.classList.toggle('dark-mode', darkModeEnabled); |
| |
| } catch (e) { console.warn('Could not load/apply dark mode preference:', e); } |
| } |
|
|
|
|
| |
| const pathname = window.location.pathname; |
| applyDarkModePreference(); |
|
|
| document.addEventListener('DOMContentLoaded', () => { |
| console.log('DOM fully loaded. Path:', pathname); |
|
|
| sideMenu = document.getElementById('side-menu'); |
| menuOverlay = document.getElementById('menu-overlay'); |
| if (!sideMenu || !menuOverlay) console.warn("Menu elements not found."); |
|
|
| |
| if (pathname.endsWith('/learning') || pathname.endsWith('/learning.html')) { |
| correctEffect = document.getElementById('correct-effect'); |
| if (!correctEffect) console.warn("Correct effect element not found."); |
| initializeLearningScreen(); |
| } |
|
|
| |
| if (pathname === '/' || pathname.endsWith('/input') || pathname.endsWith('/input.html')) { |
| console.log("Initializing Input page..."); |
| const form = document.getElementById('generate-form'); |
| if (form) form.addEventListener('submit', (e) => { e.preventDefault(); handleGenerateSubmit(); }); |
| else console.warn("Generate form not found."); |
| const urlParams = new URLSearchParams(window.location.search); |
| const initialUrl = urlParams.get('url'); |
| if (initialUrl) { |
| const urlInput = document.getElementById('youtube-url'); |
| if (urlInput) urlInput.value = initialUrl; |
| } |
| } |
| |
| else if (pathname.endsWith('/history') || pathname.endsWith('/history.html')) { |
| console.log("Initializing History page..."); |
| |
| } |
| |
| else if (pathname.endsWith('/settings') || pathname.endsWith('/settings.html')) { |
| console.log("Initializing Settings page..."); |
| try { |
| const darkModeEnabled = localStorage.getItem('darkModeEnabled') === 'true'; |
| const toggle = document.querySelector('input[type="checkbox"][onchange*="dark"]'); |
| if (toggle) toggle.checked = darkModeEnabled; |
| else console.warn("Dark mode toggle not found."); |
| } catch (e) { console.warn('Could not set dark mode toggle state:', e); } |
| } |
|
|
| updateFooterNavActiveState(pathname); |
| closeMenu(); |
| }); |
|
|
|
|
| |
| |
| |
| function updateFooterNavActiveState(currentPath) { |
| const footerNav = document.querySelector('.footer-nav'); |
| if (!footerNav) return; |
| const buttons = footerNav.querySelectorAll('button'); |
| buttons.forEach(button => { |
| button.classList.remove('active'); |
| const onclickAttr = button.getAttribute('onclick'); |
| if (onclickAttr) { |
| if ((currentPath === '/' || currentPath.endsWith('/input') || currentPath.endsWith('/input.html')) && onclickAttr.includes('goToInput')) button.classList.add('active'); |
| else if ((currentPath.endsWith('/history') || currentPath.endsWith('/history.html')) && onclickAttr.includes('goToHistory')) button.classList.add('active'); |
| else if ((currentPath.endsWith('/settings') || currentPath.endsWith('/settings.html')) && onclickAttr.includes('goToSettings')) button.classList.add('active'); |
| } |
| }); |
| } |
|
|
|
|
| |
|
|
| |