// 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');
});
}
});