// static/script.js // Timestamp generator for downloaded files: YYYYMMDD_HHMM_XXXXXX function getTimestampedFileName(baseName) { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); return `${year}${month}${day}_${hours}${minutes}_${baseName}`; } document.addEventListener('DOMContentLoaded', () => { // 常用單字速查快卡資料庫 const PHRASES = [ { zh: "你好", desc: "日常招呼" }, { zh: "謝謝你", desc: "表達感恩" }, { zh: "再見", desc: "告別祝願" }, { zh: "加油", desc: "鼓舞打氣" }, { zh: "早安", desc: "晨間問候" }, { zh: "晚安", desc: "夜間問候" }, { zh: "吃飽了嗎?", desc: "關心問候" }, { zh: "很高興認識你", desc: "禮貌交友" }, { zh: "請問這個多少錢?", desc: "購物問價" }, { zh: "祝你幸運成功", desc: "祝福" } ]; // 全域變數 let currentTribe = "太魯閣"; let currentMode = "zh_to_native"; // 預設:華 ➔ 族 let lastSourceText = ""; let lastTargetText = ""; let lastNativeText = ""; let lastChineseText = ""; let lastDirection = "zh_to_native"; let currentAudio = null; let mediaRecorder = null; let audioChunks = []; // --- DOM 元件獲取 --- const tribeSelect = document.getElementById('tribeSelect'); const textInput = document.getElementById('textInput'); const textOutput = document.getElementById('textOutput'); const charCount = document.getElementById('charCount'); const speedTimer = document.getElementById('speedTimer'); const translateBtn = document.getElementById('translateBtn'); const ttsBtn = document.getElementById('ttsBtn'); const deepTranslateBtn = document.getElementById('deepTranslateBtn'); const copyBtn = document.getElementById('copyBtn'); const confidenceBadge = document.getElementById('confidenceBadge'); const confidenceLevel = document.getElementById('confidenceLevel'); const confidenceDesc = document.getElementById('confidenceDesc'); const phrasesGrid = document.getElementById('phrasesGrid'); const historyList = document.getElementById('historyList'); // 1. 初始化 10 句速查快卡 function initPhrasebook() { phrasesGrid.innerHTML = ''; PHRASES.forEach(p => { const card = document.createElement('div'); card.className = 'phrase-card'; card.innerHTML = `
${p.zh}
${p.desc}
`; card.addEventListener('click', () => { textInput.value = p.zh; updateCharCount(); updateDirBtnsUI('zh_to_native'); handleTranslate(); }); phrasesGrid.appendChild(card); }); } // 2. Tab 切換邏輯 const tabBtns = document.querySelectorAll('.tab-btn'); const tabContents = document.querySelectorAll('.tab-content'); tabBtns.forEach(btn => { btn.addEventListener('click', () => { tabBtns.forEach(b => b.classList.remove('active')); tabContents.forEach(c => c.classList.remove('active')); btn.classList.add('active'); const targetTab = btn.getAttribute('data-tab'); document.getElementById(targetTab).classList.add('active'); }); }); // 3. 翻譯方向模式按鈕處理 const dirBtns = document.querySelectorAll('.dir-btn'); function updateDirBtnsUI(mode) { currentMode = mode; dirBtns.forEach(btn => { if (btn.getAttribute('data-mode') === mode) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); const inputBoxTitle = document.getElementById('inputBoxTitle'); if (inputBoxTitle) { if (mode === 'zh_to_native') { inputBoxTitle.innerHTML = ' 原文輸入 (華)'; textInput.placeholder = `請在此輸入華,將自動翻譯為【${currentTribe}】...`; } else if (mode === 'native_to_zh') { inputBoxTitle.innerHTML = ` 原文輸入 (${currentTribe})`; textInput.placeholder = `請在此輸入【${currentTribe}】,將自動翻譯為華...`; } else { inputBoxTitle.innerHTML = ' 原文輸入 (自動判斷)'; textInput.placeholder = `請在此輸入句子,系統將自動判斷方向...`; } } } dirBtns.forEach(btn => { btn.addEventListener('click', () => { const mode = btn.getAttribute('data-mode'); updateDirBtnsUI(mode); if (textInput.value.trim().length > 0) { handleTranslate(); } }); }); // 4. 族切換同步 tribeSelect.addEventListener('change', (e) => { currentTribe = e.target.value; document.querySelectorAll('.current-tribe-label').forEach(el => el.textContent = currentTribe); updateDirBtnsUI(currentMode); if (textInput.value.trim().length > 0) { if (currentMode === 'zh_to_native') { // 華翻族:切換族時自動將原華翻譯為新族 handleTranslate(); } else { // 族翻華:切換族時,原族已不符新選族,直接清空避免誤會 textInput.value = ''; textOutput.textContent = ''; textOutput.classList.add('empty'); lastTargetText = ''; updateCharCount(); // 隱藏信心值與功能按鈕 confidenceBadge.style.display = 'none'; ttsBtn.disabled = true; deepTranslateBtn.disabled = true; copyBtn.disabled = true; } } }); // 5. 字數統計與上限限制 function updateCharCount() { const len = textInput.value.length; charCount.textContent = `${len} 字`; charCount.style.color = 'var(--text-muted)'; } textInput.addEventListener('input', updateCharCount); // 6. 執行文字翻譯 (POST /api/translate) async function handleTranslate() { const text = textInput.value.trim(); if (!text) return; if (text.length > 300) { alert('您的翻譯字數超過300字,會需要一些翻譯等待時間,敬請稍後。'); } translateBtn.disabled = true; translateBtn.innerHTML = ' 翻譯中...'; textOutput.classList.remove('empty'); textOutput.textContent = 'AI 正向遠端 API 請求翻譯中...'; const startTime = performance.now(); try { const res = await fetch('/api/translate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: text, tribe: currentTribe, mode: currentMode }) }); const data = await res.json(); const endTime = performance.now(); const durationSec = ((endTime - startTime) / 1000).toFixed(1); speedTimer.innerHTML = ` 耗時: ${durationSec}s`; if (data.error) { textOutput.textContent = `❌ ${data.error}`; return; } lastSourceText = text; lastTargetText = data.target; lastDirection = data.direction || 'zh_to_native'; lastNativeText = (data.direction === 'native_to_zh') ? text : data.target; lastChineseText = (data.direction === 'native_to_zh') ? data.target : text; textOutput.textContent = data.target; // 啟用按鈕 ttsBtn.disabled = false; deepTranslateBtn.disabled = false; copyBtn.disabled = false; // 顯示信心值與翻譯方向標籤 const dirLabel = data.direction === 'native_to_zh' ? `${currentTribe} ➔ 華` : `華 ➔ ${currentTribe}`; confidenceBadge.style.display = 'inline-flex'; confidenceBadge.className = `badge-confidence ${data.confidence_level || 'high'}`; confidenceLevel.textContent = data.confidence_level === 'low' ? '低' : (data.confidence_level === 'medium' ? '中' : '高'); confidenceDesc.textContent = `【${dirLabel}】 ${data.confidence_desc || '一般生活句'}`; // 記錄至本地歷史紀錄 saveToHistory(text, data.target, currentTribe); } catch (err) { textOutput.textContent = `❌ 網路請求失敗: ${err.message}`; } finally { translateBtn.disabled = false; translateBtn.innerHTML = ' 開始極速翻譯'; } } translateBtn.addEventListener('click', handleTranslate); // 雙向對調按鈕事件 const swapDirectionBtn = document.getElementById('swapDirectionBtn'); if (swapDirectionBtn) { swapDirectionBtn.addEventListener('click', () => { if (currentMode === 'zh_to_native') { updateDirBtnsUI('native_to_zh'); } else if (currentMode === 'native_to_zh') { updateDirBtnsUI('zh_to_native'); } if (lastTargetText) { textInput.value = lastTargetText; updateCharCount(); handleTranslate(); } else if (textInput.value.trim()) { handleTranslate(); } }); } // Enter 鍵按快捷翻譯 (Shift+Enter 換行) textInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleTranslate(); } }); // 6. 🔊 聽發音 (POST /api/tts - 傳送族文字) ttsBtn.addEventListener('click', async () => { const nativeTextToSpeak = lastNativeText || (lastDirection === 'native_to_zh' ? lastSourceText : lastTargetText); if (!nativeTextToSpeak) return; ttsBtn.disabled = true; ttsBtn.innerHTML = ' 合成中...'; try { const res = await fetch('/api/tts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: nativeTextToSpeak, tribe: currentTribe }) }); const data = await res.json(); if (data.audio_url) { if (currentAudio) currentAudio.pause(); currentAudio = new Audio(data.audio_url); currentAudio.play(); } else { alert('⚠️ 音合成失敗:' + (data.error || '未知錯誤')); } } catch (err) { alert('❌ 音服務請求失敗'); } finally { ttsBtn.disabled = false; ttsBtn.innerHTML = ' 🔊 聽發音'; } }); // 8. 🧠 深度文化潤飾與 API Key 管理 const apiKeyModal = document.getElementById('apiKeyModal'); const closeApiKeyModalBtn = document.getElementById('closeApiKeyModalBtn'); const cancelApiKeyBtn = document.getElementById('cancelApiKeyBtn'); const confirmApiKeyBtn = document.getElementById('confirmApiKeyBtn'); const geminiApiKeyInput = document.getElementById('geminiApiKeyInput'); const rememberApiKeyCheckbox = document.getElementById('rememberApiKeyCheckbox'); const changeApiKeyBtn = document.getElementById('changeApiKeyBtn'); let pendingDeepTranslateResolve = null; function promptForApiKey() { return new Promise((resolve) => { pendingDeepTranslateResolve = resolve; const savedKey = localStorage.getItem('gemini_api_key') || ''; if (geminiApiKeyInput) { geminiApiKeyInput.value = savedKey; setTimeout(() => geminiApiKeyInput.focus(), 100); } if (apiKeyModal) apiKeyModal.style.display = 'flex'; }); } function closeApiKeyModal(apiKey = null) { if (apiKeyModal) apiKeyModal.style.display = 'none'; if (pendingDeepTranslateResolve) { pendingDeepTranslateResolve(apiKey); pendingDeepTranslateResolve = null; } } if (closeApiKeyModalBtn) closeApiKeyModalBtn.addEventListener('click', () => closeApiKeyModal(null)); if (cancelApiKeyBtn) cancelApiKeyBtn.addEventListener('click', () => closeApiKeyModal(null)); function handleApiKeySubmit() { const key = geminiApiKeyInput.value.trim(); if (!key) { alert('⚠️ 請輸入有效的 API Key'); return; } if (rememberApiKeyCheckbox && rememberApiKeyCheckbox.checked) { localStorage.setItem('gemini_api_key', key); } else { localStorage.removeItem('gemini_api_key'); } closeApiKeyModal(key); } if (confirmApiKeyBtn) { confirmApiKeyBtn.addEventListener('click', handleApiKeySubmit); } if (geminiApiKeyInput) { geminiApiKeyInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); handleApiKeySubmit(); } }); } if (changeApiKeyBtn) { changeApiKeyBtn.addEventListener('click', async () => { const deepModal = document.getElementById('deepTranslateModal'); if (deepModal) deepModal.style.display = 'none'; const newKey = await promptForApiKey(); if (newKey && lastSourceText && lastTargetText) { await executeDeepTranslate(newKey); } }); } async function executeDeepTranslate(apiKeyToUse) { deepTranslateBtn.disabled = true; deepTranslateBtn.innerHTML = ' 潤飾中...'; try { const res = await fetch('/api/deep_translate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ source_text: lastSourceText, target_text: lastTargetText, tribe: currentTribe, api_key: apiKeyToUse }) }); const data = await res.json(); if (data.result) { const bodyElem = document.getElementById('deepTranslateResultBody'); const modal = document.getElementById('deepTranslateModal'); // 🎯 使用後端 is_api_key_error 旗標(精準、不依賴字串比對) if (data.is_api_key_error === true) { localStorage.removeItem('gemini_api_key'); // 直接寫入 innerHTML,用 onclick 屬性綁定事件(最穩定的方式,不依賴 addEventListener) bodyElem.innerHTML = [ '
', '
⚠️
', '
【API Key 驗證失敗】
', '
', '您的 Google Gemini API Key 無效或已過期。
請點擊下方按鈕重新輸入有效金鑰。', '
', '', '
' ].join(''); } else { bodyElem.textContent = data.result; } modal.style.display = 'flex'; } else { alert('⚠️ 深度翻譯服務未回傳結果'); } } catch (err) { alert('❌ 深度翻譯請求失敗'); } finally { deepTranslateBtn.disabled = false; deepTranslateBtn.innerHTML = ' 🧠 深度文化潤飾'; } } // 🌐 全域函式:讓 onclick 屬性能呼叫到 promptForApiKey window.__changeApiKey__ = async function() { document.getElementById('deepTranslateModal').style.display = 'none'; const newKey = await promptForApiKey(); if (newKey && lastSourceText && lastTargetText) { await executeDeepTranslate(newKey); } }; deepTranslateBtn.addEventListener('click', async () => { if (!lastSourceText || !lastTargetText) return; // 🎯 點擊「深度文化潤飾」時強制跳出 API Key 彈窗 (若有預存金鑰會自動帶入) const apiKey = await promptForApiKey(); if (!apiKey) return; // 使用者按下取消 await executeDeepTranslate(apiKey); }); // 9. 複製結果 copyBtn.addEventListener('click', () => { if (!lastTargetText) return; navigator.clipboard.writeText(lastTargetText).then(() => { const originalText = copyBtn.innerHTML; copyBtn.innerHTML = ' 已複製!'; setTimeout(() => copyBtn.innerHTML = originalText, 2000); }); }); // 10. 🎙️ 音錄音與 ASR (POST /api/asr) const startRecordBtn = document.getElementById('startRecordBtn'); const stopRecordBtn = document.getElementById('stopRecordBtn'); const recordingStatus = document.getElementById('recordingStatus'); const audioDropzone = document.getElementById('audioDropzone'); const audioFileInput = document.getElementById('audioFileInput'); const audioResult = document.getElementById('audioResult'); const audioOutput = document.getElementById('audioOutput'); startRecordBtn.addEventListener('click', async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); mediaRecorder = new MediaRecorder(stream); audioChunks = []; mediaRecorder.ondataavailable = event => audioChunks.push(event.data); mediaRecorder.onstop = async () => { const audioBlob = new Blob(audioChunks, { type: 'audio/wav' }); await sendAudioFile(audioBlob, 'recorded_audio.wav'); }; mediaRecorder.start(); recordingStatus.style.display = 'block'; startRecordBtn.disabled = true; stopRecordBtn.disabled = false; } catch (err) { alert('⚠️ 無法存取麥克風設備: ' + err.message); } }); stopRecordBtn.addEventListener('click', () => { if (mediaRecorder && mediaRecorder.state !== 'inactive') { mediaRecorder.stop(); recordingStatus.style.display = 'none'; startRecordBtn.disabled = false; stopRecordBtn.disabled = true; } }); audioDropzone.addEventListener('click', () => audioFileInput.click()); audioFileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) { sendAudioFile(e.target.files[0], e.target.files[0].name); } }); async function sendAudioFile(blobOrFile, filename) { audioResult.style.display = 'block'; audioOutput.textContent = '音音軌分析中,請稍候...'; const formData = new FormData(); formData.append('file', blobOrFile, filename); formData.append('tribe', currentTribe); try { const res = await fetch('/api/asr', { method: 'POST', body: formData }); const data = await res.json(); if (data.error) { audioOutput.textContent = `❌ ${data.error}`; } else { audioOutput.innerHTML = `
辨識文字: ${data.source || '無文本'}
【${currentTribe}翻譯】: ${data.target || '無翻譯'}
`; } } catch (err) { audioOutput.textContent = `❌ 音分析傳送失敗: ${err.message}`; } } // 11. 🖼️ 圖片 OCR 翻譯 (POST /api/ocr) const imageDropzone = document.getElementById('imageDropzone'); const imageFileInput = document.getElementById('imageFileInput'); const imageResult = document.getElementById('imageResult'); const imageOutput = document.getElementById('imageOutput'); imageDropzone.addEventListener('click', () => imageFileInput.click()); imageFileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) { sendImageFile(e.target.files[0]); } }); async function sendImageFile(file) { imageResult.style.display = 'block'; imageOutput.textContent = 'Gemini 視覺模型解析圖片中...'; // 顯示圖片預覽 const imagePreviewContainer = document.getElementById('imagePreviewContainer'); const imagePreview = document.getElementById('imagePreview'); const reader = new FileReader(); reader.onload = function(e) { imagePreview.src = e.target.result; imagePreviewContainer.style.display = 'block'; }; reader.readAsDataURL(file); let currentOcrItems = []; const formData = new FormData(); formData.append('file', file); formData.append('tribe', currentTribe); try { const res = await fetch('/api/ocr', { method: 'POST', body: formData }); const data = await res.json(); if (data.items && data.items.length > 0) { currentOcrItems = data.items; imageOutput.innerHTML = ''; data.items.forEach(item => { const card = document.createElement('div'); card.style.cssText = 'background: rgba(0,0,0,0.4); padding: 12px; border-radius: 8px; border-left: 3px solid var(--primary-cyan);'; card.innerHTML = `
原圖文字: ${item.original}
譯文: ${item.translated}
`; imageOutput.appendChild(card); }); } else { imageOutput.textContent = '⚠️ 未能在圖片中辨識出有效文字,或 OCR 回傳空值。'; } } catch (err) { imageOutput.textContent = `❌ 圖片上傳解析失敗: ${err.message}`; } } // 複製 OCR 結果 const copyOcrBtn = document.getElementById('copyOcrBtn'); if(copyOcrBtn) { copyOcrBtn.addEventListener('click', () => { const outputText = Array.from(imageOutput.children) .filter(card => card.tagName.toLowerCase() === 'div') .map(card => { const orig = card.querySelector('div:nth-child(1)').textContent.replace('原圖文字: ', ''); const trans = card.querySelector('div:nth-child(2)').textContent.replace('譯文: ', ''); return `【原圖文字】: ${orig}\n【太魯閣翻譯】: ${trans}`; }).join('\n\n'); if (!outputText) return; navigator.clipboard.writeText(outputText); const originalText = copyOcrBtn.innerHTML; copyOcrBtn.innerHTML = ' 已複製'; setTimeout(() => copyOcrBtn.innerHTML = originalText, 2000); }); } // 下載 OCR WORD const downloadOcrWordBtn = document.getElementById('downloadOcrWordBtn'); if(downloadOcrWordBtn) { downloadOcrWordBtn.addEventListener('click', () => { const outputText = Array.from(imageOutput.children) .filter(card => card.tagName.toLowerCase() === 'div') .map(card => { const orig = card.querySelector('div:nth-child(1)').textContent.replace('原圖文字: ', ''); const trans = card.querySelector('div:nth-child(2)').textContent.replace('譯文: ', ''); return `

【原圖文字】: ${orig}

【太魯閣翻譯】: ${trans}

`; }).join(''); if (!outputText) return; const htmlContent = ` OCR 翻譯結果

圖片 OCR 翻譯結果


${outputText} `; const blob = new Blob(['\ufeff', htmlContent], { type: 'application/msword' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = getTimestampedFileName('圖片OCR翻譯結果.doc'); document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }); } // 12. 🎬 影片音軌翻譯 (POST /api/video) const videoDropzone = document.getElementById('videoDropzone'); const videoFileInput = document.getElementById('videoFileInput'); const videoResult = document.getElementById('videoResult'); const videoOutput = document.getElementById('videoOutput'); videoDropzone.addEventListener('click', () => videoFileInput.click()); videoFileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) { sendVideoFile(e.target.files[0]); } }); let currentVideoData = null; // 格式化秒數為 SRT 時間軸 (HH:MM:SS,MMM) function formatSRTTime(seconds) { const date = new Date(0); date.setSeconds(Math.floor(seconds)); const hh = String(date.getUTCHours()).padStart(2, '0'); const mm = String(date.getUTCMinutes()).padStart(2, '0'); const ss = String(date.getUTCSeconds()).padStart(2, '0'); const mmm = String(Math.floor((seconds % 1) * 1000)).padStart(3, '0'); return `${hh}:${mm}:${ss},${mmm}`; } async function sendVideoFile(file) { videoResult.style.display = 'block'; videoOutput.textContent = 'MoviePy 提取 16kHz 音軌與 ASR 意識別中 (影片較大時需數秒時間)...'; // 顯示影片預覽 const videoPreviewContainer = document.getElementById('videoPreviewContainer'); const videoPreview = document.getElementById('videoPreview'); const fileUrl = URL.createObjectURL(file); videoPreview.src = fileUrl; videoPreviewContainer.style.display = 'block'; currentVideoData = null; // 重置資料 const formData = new FormData(); formData.append('file', file); formData.append('tribe', currentTribe); try { const res = await fetch('/api/video', { method: 'POST', body: formData }); const data = await res.json(); if (data.error) { videoOutput.textContent = `❌ ${data.error}`; } else { currentVideoData = data; // 保存資料供按鈕使用 // 渲染各個片段 videoOutput.innerHTML = ''; if (data.segments && data.segments.length > 0) { data.segments.sort((a, b) => a.start - b.start).forEach((seg, idx) => { const card = document.createElement('div'); card.style.cssText = 'background: rgba(0,0,0,0.4); padding: 12px; border-radius: 8px; border-left: 3px solid var(--primary-cyan); margin-bottom: 10px;'; const timeRange = `[${formatSRTTime(seg.start).split(',')[0]} - ${formatSRTTime(seg.end).split(',')[0]}]`; card.innerHTML = `
區段 ${idx + 1}   ${timeRange}
提取音逐字稿: ${seg.source}
【${currentTribe}對譯】: ${seg.target}
`; videoOutput.appendChild(card); }); } else { videoOutput.innerHTML = `
無法辨識出任何有效語音區段。
`; } } } catch (err) { videoOutput.textContent = `❌ 影片處理失敗: ${err.message}`; } } // 影片模組按鈕綁定 const downloadAudioBtn = document.getElementById('downloadAudioBtn'); const downloadSrtBtn = document.getElementById('downloadSrtBtn'); const copyVideoBtn = document.getElementById('copyVideoBtn'); const downloadVideoWordBtn = document.getElementById('downloadVideoWordBtn'); if(downloadAudioBtn) { downloadAudioBtn.addEventListener('click', () => { if (!currentVideoData || !currentVideoData.audio_url) { alert('無可用的音軌檔案。'); return; } const link = document.createElement('a'); link.href = currentVideoData.audio_url; link.download = getTimestampedFileName('影片提取音軌.wav'); document.body.appendChild(link); link.click(); document.body.removeChild(link); }); } if(downloadSrtBtn) { downloadSrtBtn.addEventListener('click', () => { if (!currentVideoData || !currentVideoData.segments) { alert('請先等待影片辨識完成或確認有辨識出段落。'); return; } let srtContent = ''; currentVideoData.segments.sort((a, b) => a.start - b.start).forEach((seg, idx) => { const startTime = formatSRTTime(seg.start); const endTime = formatSRTTime(seg.end); srtContent += `${idx + 1}\n${startTime} --> ${endTime}\n${seg.source}\n${seg.target}\n\n`; }); const blob = new Blob([srtContent], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = getTimestampedFileName('影片翻譯字幕.srt'); document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }); } if(copyVideoBtn) { copyVideoBtn.addEventListener('click', () => { if (!currentVideoData || !currentVideoData.segments) return; const outputText = currentVideoData.segments.sort((a, b) => a.start - b.start).map((seg, idx) => { return `[區段 ${idx + 1}]\n【原音逐字稿】: ${seg.source}\n【${currentVideoData.tribe}翻譯】: ${seg.target}`; }).join('\n\n'); navigator.clipboard.writeText(outputText); const originalText = copyVideoBtn.innerHTML; copyVideoBtn.innerHTML = ' 已複製'; setTimeout(() => copyVideoBtn.innerHTML = originalText, 2000); }); } if(downloadVideoWordBtn) { downloadVideoWordBtn.addEventListener('click', () => { if (!currentVideoData || !currentVideoData.segments) return; const outputText = currentVideoData.segments.sort((a, b) => a.start - b.start).map((seg, idx) => { return `

[區段 ${idx + 1}]

【原音逐字稿】: ${seg.source}

【${currentVideoData.tribe}翻譯】: ${seg.target}

`; }).join(''); const htmlContent = ` 影片翻譯結果

影片翻譯結果


${outputText} `; const blob = new Blob(['\ufeff', htmlContent], { type: 'application/msword' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = getTimestampedFileName('影片翻譯結果.doc'); document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }); } // 13. 💬 送出報錯與建議 (POST /api/feedback) const sendFeedbackBtn = document.getElementById('sendFeedbackBtn'); sendFeedbackBtn.addEventListener('click', async () => { const nickname = document.getElementById('fbNickname').value.trim() || '熱心使用者'; const comment = document.getElementById('fbComment').value.trim(); if (!comment) { alert('請填寫回饋與建議內容!'); return; } sendFeedbackBtn.disabled = true; sendFeedbackBtn.innerHTML = ' 發送中...'; try { const res = await fetch('/api/feedback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_nickname: nickname, tribe: currentTribe, comment: comment }) }); const data = await res.json(); alert('✅ 感謝您的寶貴回饋!意見已成功寄送給料維護團隊。'); document.getElementById('fbComment').value = ''; } catch (err) { alert('❌ 寄送失敗,請稍後再試。'); } finally { sendFeedbackBtn.disabled = false; sendFeedbackBtn.innerHTML = ' 送出意見回饋'; } }); // 14. 歷史紀錄儲存 function saveToHistory(source, target, tribe) { let history = JSON.parse(localStorage.getItem('ilrdf_history') || '[]'); history.unshift({ source, target, tribe, time: new Date().toLocaleTimeString() }); history = history.slice(0, 8); // 保留最新 8 條 localStorage.setItem('ilrdf_history', JSON.stringify(history)); renderHistory(); } function renderHistory() { let history = JSON.parse(localStorage.getItem('ilrdf_history') || '[]'); if (history.length === 0) { historyList.innerHTML = '
尚未有翻譯紀錄。
'; return; } historyList.innerHTML = ''; history.forEach(h => { const item = document.createElement('div'); item.className = 'history-item'; item.innerHTML = `
${h.source}${h.target}
${h.tribe} | ${h.time}
`; item.querySelector('button').addEventListener('click', () => { textInput.value = h.source; tribeSelect.value = h.tribe; currentTribe = h.tribe; updateCharCount(); handleTranslate(); }); historyList.appendChild(item); }); } // 初始化速查句卡與歷史紀錄 initPhrasebook(); renderHistory(); // 清除歷史紀錄 (綁定為全域函式,讓 HTML onclick 可以呼叫) window.clearHistory = function() { if (!confirm('確定要清除全部翻譯歷史紀錄嗎?此操作無法復原。')) return; localStorage.removeItem('ilrdf_history'); renderHistory(); // 給按鈕一個短暫的視覺回饋 const btn = document.getElementById('clearHistoryBtn'); if (btn) { const orig = btn.innerHTML; btn.innerHTML = ' 已清除'; btn.style.color = '#4ade80'; btn.style.borderColor = 'rgba(74,222,128,0.4)'; setTimeout(() => { btn.innerHTML = orig; btn.style.color = '#ff6b6b'; btn.style.borderColor = 'rgba(255,80,80,0.4)'; }, 1500); } }; // 15. 深度文化潤飾 Modal 事件綁定 const deepTranslateModal = document.getElementById('deepTranslateModal'); const closeModalBtn = document.getElementById('closeModalBtn'); const copyDeepResultBtn = document.getElementById('copyDeepResultBtn'); const downloadWordBtn = document.getElementById('downloadWordBtn'); closeModalBtn.addEventListener('click', () => { deepTranslateModal.style.display = 'none'; }); // 點擊背景關閉 deepTranslateModal.addEventListener('click', (e) => { if (e.target === deepTranslateModal) { deepTranslateModal.style.display = 'none'; } }); if (apiKeyModal) { apiKeyModal.addEventListener('click', (e) => { if (e.target === apiKeyModal) { closeApiKeyModal(null); } }); } // 複製結果 copyDeepResultBtn.addEventListener('click', () => { const content = document.getElementById('deepTranslateResultBody').textContent; navigator.clipboard.writeText(content).then(() => { const originalText = copyDeepResultBtn.innerHTML; copyDeepResultBtn.innerHTML = ' 已複製'; setTimeout(() => copyDeepResultBtn.innerHTML = originalText, 2000); }); }); // 下載 WORD downloadWordBtn.addEventListener('click', () => { const content = document.getElementById('deepTranslateResultBody').textContent; // 簡單生成相容 Word 的 HTML 格式 doc const htmlContent = ` 深度文化潤飾結果
${content}
`; const blob = new Blob(['\ufeff', htmlContent], { type: 'application/msword' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = getTimestampedFileName('深度文化潤飾結果.doc'); document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }); // 16. 長句測試區事件綁定 const longPhraseZh = document.getElementById('longPhraseZh'); const longPhraseNative = document.getElementById('longPhraseNative'); if (longPhraseZh) { longPhraseZh.addEventListener('click', () => { textInput.value = '很久以前,天地間只有黑暗沒有白天,人類的生活很辛苦而且沒有發展。就這樣以松樹的木片點火當作燈來做家事及到田間工作。'; updateCharCount(); updateDirBtnsUI('zh_to_native'); }); } if (longPhraseNative) { longPhraseNative.addEventListener('click', () => { textInput.value = 'Mqaras ku bi qmita sunan, Lituk ka hangan mu, ima ka hangan su? Emptgsa ku ka yaku, ga ku tmgsa kari Truku. Gaga su qmpah qpahun manu?'; updateCharCount(); updateDirBtnsUI('native_to_zh'); }); } });