Spaces:
Running
Running
Update index.html
Browse files- index.html +332 -164
index.html
CHANGED
|
@@ -135,8 +135,17 @@
|
|
| 135 |
}
|
| 136 |
|
| 137 |
/* 觸摸手寫板時的視覺回饋類別 */
|
| 138 |
-
.canvas-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
border-color: #6366f1 !important; /* Indigo-500 */
|
|
|
|
|
|
|
|
|
|
| 140 |
background-color: #f0fdf4 !important; /* Green-50 */
|
| 141 |
}
|
| 142 |
|
|
@@ -416,7 +425,8 @@
|
|
| 416 |
|
| 417 |
<div id="handwriting-container" class="hidden flex-col gap-2">
|
| 418 |
<!-- 使用 wrapper 包裹 canvas,並強制設定固定高度,防止高度塌陷 -->
|
| 419 |
-
|
|
|
|
| 420 |
<!-- Canvas 將會由 JS 動態生成插入 -->
|
| 421 |
</div>
|
| 422 |
|
|
@@ -663,12 +673,12 @@
|
|
| 663 |
// [第四步] JavaScript 邏輯 (Script)
|
| 664 |
let effectiveVersion = 'pc'; // 預設為 PC 版
|
| 665 |
|
| 666 |
-
// [修改]
|
| 667 |
let isDrawing = false;
|
| 668 |
-
|
| 669 |
-
let
|
| 670 |
-
let
|
| 671 |
-
|
| 672 |
// [新增] TrOCR 相關變數
|
| 673 |
let ocrPipeline = null; // 存放載入好的模型
|
| 674 |
let isModelLoading = false;
|
|
@@ -851,6 +861,8 @@
|
|
| 851 |
let currentSpeedQuestionType = '';
|
| 852 |
let quizStartTime = 0;
|
| 853 |
let parsedWordsFromAI = [];
|
|
|
|
|
|
|
| 854 |
|
| 855 |
|
| 856 |
const modeDetails = {
|
|
@@ -870,121 +882,207 @@
|
|
| 870 |
}, 3000);
|
| 871 |
}
|
| 872 |
|
| 873 |
-
// [修改]
|
| 874 |
-
function
|
| 875 |
const wrapper = document.getElementById('canvas-wrapper');
|
| 876 |
if (!wrapper) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 877 |
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 885 |
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
canvas.style.width = '100%';
|
| 891 |
-
canvas.style.height = '250px';
|
| 892 |
-
canvas.style.backgroundColor = '#ffffff';
|
| 893 |
-
canvas.style.border = '2px dashed #cbd5e1';
|
| 894 |
-
canvas.style.borderRadius = '0.75rem';
|
| 895 |
-
canvas.style.cursor = 'crosshair';
|
| 896 |
-
canvas.style.touchAction = 'none'; // 防止觸控時捲動
|
| 897 |
-
canvas.style.display = 'block';
|
| 898 |
-
|
| 899 |
-
// 設定 HTML 屬性的寬高 (解析度),必須與 CSS 顯示大小匹配
|
| 900 |
-
canvas.width = rect.width;
|
| 901 |
-
canvas.height = 250;
|
| 902 |
-
|
| 903 |
-
wrapper.appendChild(canvas);
|
| 904 |
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
// [修改] 為了 TrOCR 模型,強制將畫布背景填滿白色像素 (透明背景會導致辨識失敗)
|
| 909 |
-
canvasCtx.fillStyle = '#ffffff';
|
| 910 |
-
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
|
| 911 |
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 916 |
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 920 |
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
canvas.addEventListener('
|
| 924 |
-
canvas.addEventListener('
|
| 925 |
-
|
| 926 |
-
canvas.addEventListener('
|
| 927 |
-
|
| 928 |
-
canvas.addEventListener('
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
console.log('Native Handwriting board initialized with width:', rect.width);
|
| 933 |
}
|
| 934 |
|
| 935 |
-
// [
|
| 936 |
-
function startStroke(e) {
|
| 937 |
-
if (e.type === 'touchstart') e.preventDefault(); // 防止滾動
|
| 938 |
isDrawing = true;
|
| 939 |
-
const
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
currentStroke = { x: [pos.x], y: [pos.y], t: [Date.now()] };
|
| 944 |
|
| 945 |
-
|
| 946 |
-
|
| 947 |
|
| 948 |
-
|
| 949 |
-
document.getElementById('handwriting-canvas').classList.add('canvas-active');
|
| 950 |
}
|
| 951 |
|
| 952 |
-
// [
|
| 953 |
-
function moveStroke(e) {
|
| 954 |
if (!isDrawing) return;
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
const pos = getPos(e);
|
| 958 |
-
currentStroke.x.push(pos.x);
|
| 959 |
-
currentStroke.y.push(pos.y);
|
| 960 |
-
currentStroke.t.push(Date.now());
|
| 961 |
|
| 962 |
-
|
| 963 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 964 |
}
|
| 965 |
|
| 966 |
-
// [
|
| 967 |
-
function endStroke(e) {
|
| 968 |
if (!isDrawing) return;
|
| 969 |
isDrawing = false;
|
| 970 |
-
|
|
|
|
|
|
|
|
|
|
| 971 |
|
| 972 |
-
|
| 973 |
-
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
currentStroke.
|
| 977 |
-
currentStroke.y,
|
| 978 |
-
currentStroke.t
|
| 979 |
]);
|
| 980 |
}
|
| 981 |
|
| 982 |
-
|
| 983 |
}
|
| 984 |
|
| 985 |
-
// [
|
| 986 |
-
function getPos(e) {
|
| 987 |
-
const canvas = document.getElementById('handwriting-canvas');
|
| 988 |
const rect = canvas.getBoundingClientRect();
|
| 989 |
let clientX, clientY;
|
| 990 |
|
|
@@ -1002,50 +1100,50 @@
|
|
| 1002 |
};
|
| 1003 |
}
|
| 1004 |
|
| 1005 |
-
// [
|
| 1006 |
function clearCanvas() {
|
| 1007 |
-
const
|
| 1008 |
-
if(!
|
| 1009 |
|
| 1010 |
-
|
| 1011 |
-
|
| 1012 |
-
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
|
| 1013 |
|
| 1014 |
-
|
| 1015 |
-
|
|
|
|
|
|
|
|
|
|
| 1016 |
candidateBar.innerHTML = '';
|
| 1017 |
candidateBar.classList.add('hidden');
|
| 1018 |
}
|
| 1019 |
|
| 1020 |
-
// [
|
| 1021 |
function undoStroke() {
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
|
|
|
|
| 1025 |
}
|
| 1026 |
|
| 1027 |
-
// [
|
| 1028 |
-
function redrawCanvas() {
|
| 1029 |
-
|
| 1030 |
-
if(!canvas || !canvasCtx) return;
|
| 1031 |
|
| 1032 |
-
|
| 1033 |
-
|
| 1034 |
-
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
|
| 1035 |
|
| 1036 |
-
|
| 1037 |
-
|
| 1038 |
-
// stroke is [[x...], [y...], [t...]]
|
| 1039 |
const xs = stroke[0];
|
| 1040 |
const ys = stroke[1];
|
| 1041 |
if (xs.length > 0) {
|
| 1042 |
-
|
| 1043 |
for (let i = 1; i < xs.length; i++) {
|
| 1044 |
-
|
| 1045 |
}
|
| 1046 |
}
|
| 1047 |
});
|
| 1048 |
-
|
| 1049 |
}
|
| 1050 |
|
| 1051 |
// [新增] 載入 TrOCR AI 模型 (使用 dynamic import)
|
|
@@ -1086,8 +1184,9 @@
|
|
| 1086 |
|
| 1087 |
// [修正] 辨識手寫 (改用 TrOCR)
|
| 1088 |
async function recognizeHandwriting() {
|
| 1089 |
-
//
|
| 1090 |
-
|
|
|
|
| 1091 |
showToast("請先手寫內容!");
|
| 1092 |
return;
|
| 1093 |
}
|
|
@@ -1102,26 +1201,50 @@
|
|
| 1102 |
hwRecognizeBtn.textContent = "AI 辨識中...";
|
| 1103 |
|
| 1104 |
try {
|
| 1105 |
-
|
| 1106 |
-
|
| 1107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1108 |
|
| 1109 |
-
//
|
| 1110 |
-
const
|
| 1111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1112 |
|
| 1113 |
-
const
|
| 1114 |
|
| 1115 |
-
if (
|
| 1116 |
-
// TrOCR 可能會回傳句點,如果是單字練習可以移除
|
| 1117 |
-
const cleanedText = text.replace(/\.$/, '');
|
| 1118 |
-
|
| 1119 |
answerInput.value = cleanedText;
|
| 1120 |
answerInput.classList.add('border-green-500', 'bg-green-50');
|
| 1121 |
setTimeout(() => answerInput.classList.remove('border-green-500', 'bg-green-50'), 500);
|
| 1122 |
showToast("辨識結果:" + cleanedText);
|
| 1123 |
-
|
| 1124 |
-
// 也可以把原文當作候選字 (TrOCR 通常只回傳最可能的結果,不像 API 有多個候選)
|
| 1125 |
showCandidates([cleanedText]);
|
| 1126 |
} else {
|
| 1127 |
showToast("未能辨識出文字");
|
|
@@ -1160,25 +1283,51 @@
|
|
| 1160 |
|
| 1161 |
|
| 1162 |
// [新增] 更新輸入介面 (控制手寫板顯示與輸入法)
|
| 1163 |
-
|
|
|
|
| 1164 |
const hwContainer = document.getElementById('handwriting-container');
|
| 1165 |
|
| 1166 |
// [修改] 為了方便在 PC 上除錯,暫時移除了 effectiveVersion === 'mobile' 的檢查
|
| 1167 |
// 現在只要是輸入英文模式 (expectedLang === 'en'),都會顯示手寫板
|
| 1168 |
-
const shouldUseHandwriting = (
|
| 1169 |
|
| 1170 |
if (shouldUseHandwriting) {
|
| 1171 |
// --- 啟用手寫模式 ---
|
| 1172 |
-
// 1. 顯示
|
| 1173 |
hwContainer.classList.remove('hidden');
|
| 1174 |
hwContainer.classList.add('flex');
|
| 1175 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1176 |
// 2. 初始化 (延遲以確保 flex 渲染完成)
|
| 1177 |
-
|
| 1178 |
-
setTimeout(() => initHandwritingBoard(), 400);
|
| 1179 |
|
| 1180 |
// 3. 觸發模型預載 (Lazy Load)
|
| 1181 |
-
// 當使用者切換到英文輸入模式時,就開始在背景下載模型
|
| 1182 |
if (!ocrPipeline && !isModelLoading) {
|
| 1183 |
initOCR();
|
| 1184 |
}
|
|
@@ -1187,11 +1336,7 @@
|
|
| 1187 |
answerInput.readOnly = true;
|
| 1188 |
answerInput.placeholder = "請在下方手寫英文...";
|
| 1189 |
answerInput.classList.add('handwriting-mode');
|
| 1190 |
-
|
| 1191 |
-
// 5. 嘗試讓輸入框失去焦點,避免鍵盤彈出
|
| 1192 |
-
if (document.activeElement === answerInput) {
|
| 1193 |
-
answerInput.blur();
|
| 1194 |
-
}
|
| 1195 |
|
| 1196 |
} else {
|
| 1197 |
// --- 啟用鍵盤模式 (PC 或 輸入中文) ---
|
|
@@ -1199,11 +1344,11 @@
|
|
| 1199 |
hwContainer.classList.remove('flex');
|
| 1200 |
|
| 1201 |
answerInput.readOnly = false;
|
| 1202 |
-
answerInput.placeholder = (
|
| 1203 |
answerInput.classList.remove('handwriting-mode');
|
| 1204 |
}
|
| 1205 |
|
| 1206 |
-
console.log(`輸入模式: ${shouldUseHandwriting ? '手寫
|
| 1207 |
}
|
| 1208 |
|
| 1209 |
// --- 視圖管理 ---
|
|
@@ -1398,6 +1543,8 @@
|
|
| 1398 |
|
| 1399 |
let frontText, backText;
|
| 1400 |
let targetLanguage = 'en'; // 預設輸入英文
|
|
|
|
|
|
|
| 1401 |
|
| 1402 |
if (isSpeed) {
|
| 1403 |
// 極速挑戰
|
|
@@ -1406,6 +1553,7 @@
|
|
| 1406 |
frontText = card.chinese;
|
| 1407 |
backText = card.english;
|
| 1408 |
targetLanguage = 'en';
|
|
|
|
| 1409 |
break;
|
| 1410 |
case 'en-zh':
|
| 1411 |
frontText = card.english;
|
|
@@ -1419,6 +1567,7 @@
|
|
| 1419 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.remove('hidden'));
|
| 1420 |
speakWord(card);
|
| 1421 |
targetLanguage = 'en';
|
|
|
|
| 1422 |
break;
|
| 1423 |
}
|
| 1424 |
} else {
|
|
@@ -1433,10 +1582,13 @@
|
|
| 1433 |
// 複習模式下:沒翻面(看英文)=輸入中文,翻面(看中文)=輸入英文
|
| 1434 |
const isFlipped = flashcardContainer.classList.contains('flipped');
|
| 1435 |
targetLanguage = isFlipped ? 'en' : 'zh';
|
|
|
|
|
|
|
| 1436 |
break;
|
| 1437 |
case 'zh-en': case 'hard':
|
| 1438 |
frontText = card.chinese; backText = card.english;
|
| 1439 |
targetLanguage = 'en';
|
|
|
|
| 1440 |
break;
|
| 1441 |
case 'en-zh':
|
| 1442 |
frontText = card.english; backText = card.chinese;
|
|
@@ -1448,6 +1600,7 @@
|
|
| 1448 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.remove('hidden'));
|
| 1449 |
speakWord(card);
|
| 1450 |
targetLanguage = 'en';
|
|
|
|
| 1451 |
break;
|
| 1452 |
case 'sentence-cloze':
|
| 1453 |
clozeQuestionContainer.classList.remove('hidden');
|
|
@@ -1457,12 +1610,13 @@
|
|
| 1457 |
toggleTranslationBtn.textContent = '顯示翻譯';
|
| 1458 |
backText = card.sentence.answer || card.english;
|
| 1459 |
targetLanguage = 'en';
|
|
|
|
| 1460 |
break;
|
| 1461 |
}
|
| 1462 |
}
|
| 1463 |
|
| 1464 |
-
// [新增]
|
| 1465 |
-
updateInputInterface(targetLanguage);
|
| 1466 |
|
| 1467 |
if (frontText) {
|
| 1468 |
frontDisplay.classList.remove('hidden');
|
|
@@ -1768,6 +1922,11 @@
|
|
| 1768 |
endSpeedChallenge();
|
| 1769 |
}
|
| 1770 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1771 |
|
| 1772 |
const endSpeedChallenge = () => {
|
| 1773 |
clearInterval(timerInterval);
|
|
@@ -2215,7 +2374,16 @@
|
|
| 2215 |
|
| 2216 |
// [智慧輸入切換] 複習模式下:翻面後看到中文->要輸入英文
|
| 2217 |
const targetLanguage = isFlipped ? 'en' : 'zh';
|
| 2218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2219 |
|
| 2220 |
feedbackDisplay.textContent = '';
|
| 2221 |
setTimeout(() => answerInput.focus(), 100);
|
|
@@ -2579,6 +2747,19 @@
|
|
| 2579 |
} else {
|
| 2580 |
// 【修改】使用新的預設單字,展示文法感知功能
|
| 2581 |
const defaultWords = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2582 |
{
|
| 2583 |
english: 'watch (v.)',
|
| 2584 |
chinese: '觀看',
|
|
@@ -2588,21 +2769,8 @@
|
|
| 2588 |
answer: 'watches'
|
| 2589 |
}
|
| 2590 |
},
|
| 2591 |
-
{
|
| 2592 |
-
english: 'jog (v.)',
|
| 2593 |
-
chinese: '慢跑',
|
| 2594 |
-
sentence: {
|
| 2595 |
-
en: 'She is ___ in the park now.',
|
| 2596 |
-
zh: '她現在正在公園慢跑。',
|
| 2597 |
-
answer: 'jogging'
|
| 2598 |
-
}
|
| 2599 |
-
},
|
| 2600 |
{ english: 'study (v.)', chinese: '研讀' },
|
| 2601 |
{ english: 'last (adj.)', chinese: '前一個的' },
|
| 2602 |
-
{ english: 'death (n.)', chinese: '死亡' },
|
| 2603 |
-
{ english: 'a few (adj.)', chinese: '一些' },
|
| 2604 |
-
{ english: 'ago (adv.)', chinese: '以前' },
|
| 2605 |
-
{ english: 'parents (n.)', chinese: '父母親' },
|
| 2606 |
];
|
| 2607 |
words = defaultWords.map(word => ({ ...word, proficiency: 0, incorrectCount: 0 }));
|
| 2608 |
saveWordsToStorage();
|
|
|
|
| 135 |
}
|
| 136 |
|
| 137 |
/* 觸摸手寫板時的視覺回饋類別 */
|
| 138 |
+
.canvas-container {
|
| 139 |
+
border: 2px dashed #cbd5e1;
|
| 140 |
+
border-radius: 0.75rem;
|
| 141 |
+
overflow: hidden;
|
| 142 |
+
transition: all 0.2s;
|
| 143 |
+
}
|
| 144 |
+
.canvas-container.active-canvas {
|
| 145 |
border-color: #6366f1 !important; /* Indigo-500 */
|
| 146 |
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
|
| 147 |
+
}
|
| 148 |
+
.canvas-active-interaction {
|
| 149 |
background-color: #f0fdf4 !important; /* Green-50 */
|
| 150 |
}
|
| 151 |
|
|
|
|
| 425 |
|
| 426 |
<div id="handwriting-container" class="hidden flex-col gap-2">
|
| 427 |
<!-- 使用 wrapper 包裹 canvas,並強制設定固定高度,防止高度塌陷 -->
|
| 428 |
+
<!-- [修改] 改為 flex 容器以容納多個畫布 -->
|
| 429 |
+
<div id="canvas-wrapper" class="relative w-full min-h-[250px] flex flex-wrap gap-2 justify-center items-center">
|
| 430 |
<!-- Canvas 將會由 JS 動態生成插入 -->
|
| 431 |
</div>
|
| 432 |
|
|
|
|
| 673 |
// [第四步] JavaScript 邏輯 (Script)
|
| 674 |
let effectiveVersion = 'pc'; // 預設為 PC 版
|
| 675 |
|
| 676 |
+
// [修改] 移除單一畫布變數,改用陣列管理多個畫布實例
|
| 677 |
let isDrawing = false;
|
| 678 |
+
// hwInstances 存放物件: { id, element, ctx, strokes: [], history: [] }
|
| 679 |
+
let hwInstances = [];
|
| 680 |
+
let activeHwIndex = 0; // 當前選取的畫布索引
|
| 681 |
+
|
| 682 |
// [新增] TrOCR 相關變數
|
| 683 |
let ocrPipeline = null; // 存放載入好的模型
|
| 684 |
let isModelLoading = false;
|
|
|
|
| 861 |
let currentSpeedQuestionType = '';
|
| 862 |
let quizStartTime = 0;
|
| 863 |
let parsedWordsFromAI = [];
|
| 864 |
+
// 用來存放多重手寫板的結構,例如 ['cheer', '...', 'on']
|
| 865 |
+
let currentHandwritingStructure = [];
|
| 866 |
|
| 867 |
|
| 868 |
const modeDetails = {
|
|
|
|
| 882 |
}, 3000);
|
| 883 |
}
|
| 884 |
|
| 885 |
+
// [修改] 核心重寫:初始化多重手寫板功能
|
| 886 |
+
function initMultiHandwritingBoard(structure) {
|
| 887 |
const wrapper = document.getElementById('canvas-wrapper');
|
| 888 |
if (!wrapper) return;
|
| 889 |
+
|
| 890 |
+
// 重置狀態
|
| 891 |
+
wrapper.innerHTML = '';
|
| 892 |
+
hwInstances = [];
|
| 893 |
+
activeHwIndex = 0;
|
| 894 |
+
|
| 895 |
+
// 根據傳入的結構 (例如 ['cheer', '...', 'on'] 或 ['come', 'true']) 建立手寫板
|
| 896 |
+
structure.forEach((part, index) => {
|
| 897 |
+
if (part === '...') {
|
| 898 |
+
// 插入省略號文字
|
| 899 |
+
const ellipsis = document.createElement('div');
|
| 900 |
+
ellipsis.className = 'text-3xl font-bold text-gray-400 mx-1 select-none';
|
| 901 |
+
ellipsis.textContent = '...';
|
| 902 |
+
wrapper.appendChild(ellipsis);
|
| 903 |
+
// 省略號不佔用 hwInstances 索引
|
| 904 |
+
} else {
|
| 905 |
+
// 建立 Canvas 容器
|
| 906 |
+
const container = document.createElement('div');
|
| 907 |
+
container.className = 'canvas-container relative';
|
| 908 |
+
// 根據字數調整寬度,如果只有一個字就全寬,多個字則均分但有最小寬度
|
| 909 |
+
const wordCount = structure.filter(s => s !== '...').length;
|
| 910 |
+
if (wordCount === 1) {
|
| 911 |
+
container.style.width = '100%';
|
| 912 |
+
} else {
|
| 913 |
+
container.style.flex = '1 1 140px'; // Flex grow, shrink, basis
|
| 914 |
+
container.style.minWidth = '140px';
|
| 915 |
+
}
|
| 916 |
+
container.style.height = '250px';
|
| 917 |
+
|
| 918 |
+
// 建立 Canvas
|
| 919 |
+
const canvas = document.createElement('canvas');
|
| 920 |
+
canvas.dataset.index = hwInstances.length; // 暫存索引
|
| 921 |
+
canvas.style.width = '100%';
|
| 922 |
+
canvas.style.height = '100%';
|
| 923 |
+
canvas.style.cursor = 'crosshair';
|
| 924 |
+
canvas.style.touchAction = 'none';
|
| 925 |
+
container.appendChild(canvas);
|
| 926 |
+
wrapper.appendChild(container);
|
| 927 |
+
|
| 928 |
+
// 等待 DOM 渲染後設定 Canvas 實際��析度
|
| 929 |
+
// 使用 requestAnimationFrame 確保樣式套用後再讀取尺寸
|
| 930 |
+
requestAnimationFrame(() => {
|
| 931 |
+
const rect = container.getBoundingClientRect();
|
| 932 |
+
canvas.width = rect.width;
|
| 933 |
+
canvas.height = 250; // 固定高度
|
| 934 |
+
|
| 935 |
+
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
| 936 |
+
// 填滿白色背景
|
| 937 |
+
ctx.fillStyle = '#ffffff';
|
| 938 |
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
| 939 |
+
ctx.lineWidth = 10;
|
| 940 |
+
ctx.lineCap = 'round';
|
| 941 |
+
ctx.lineJoin = 'round';
|
| 942 |
+
ctx.strokeStyle = '#000000';
|
| 943 |
+
|
| 944 |
+
// 存入實例陣列
|
| 945 |
+
// 注意:由於 requestAnimationFrame 是非同步,這裡直接 push 可能會順序錯亂
|
| 946 |
+
// 但在這個簡單情境下,通常還好。嚴謹做法是預先建立物件佔位。
|
| 947 |
+
// 這裡我們直接修改已存在的物件引用 (稍微複雜,簡單點做)
|
| 948 |
+
// 修正:同步建立物件,非同步設定 Context
|
| 949 |
+
// 但為了簡化,我們在建立 element 時就同步 push 到 array
|
| 950 |
+
});
|
| 951 |
|
| 952 |
+
// 同步建立資料結構
|
| 953 |
+
const instance = {
|
| 954 |
+
id: hwInstances.length,
|
| 955 |
+
element: canvas,
|
| 956 |
+
container: container,
|
| 957 |
+
ctx: null, // 稍後初始化
|
| 958 |
+
strokes: [],
|
| 959 |
+
currentStroke: {x:[], y:[], t:[]} // 暫存筆畫
|
| 960 |
+
};
|
| 961 |
+
hwInstances.push(instance);
|
| 962 |
+
|
| 963 |
+
// 延遲初始化 Context (確保 width 正確)
|
| 964 |
+
setTimeout(() => {
|
| 965 |
+
const rect = container.getBoundingClientRect();
|
| 966 |
+
if (rect.width > 0) {
|
| 967 |
+
canvas.width = rect.width;
|
| 968 |
+
canvas.height = 250;
|
| 969 |
+
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
| 970 |
+
ctx.fillStyle = '#ffffff';
|
| 971 |
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
| 972 |
+
ctx.lineWidth = 10;
|
| 973 |
+
ctx.lineCap = 'round';
|
| 974 |
+
ctx.lineJoin = 'round';
|
| 975 |
+
ctx.strokeStyle = '#000000';
|
| 976 |
+
instance.ctx = ctx;
|
| 977 |
+
|
| 978 |
+
// 預設第一個畫布為啟用狀態
|
| 979 |
+
if (instance.id === 0) setActiveCanvas(0);
|
| 980 |
+
}
|
| 981 |
+
}, 50);
|
| 982 |
|
| 983 |
+
// 綁定事件
|
| 984 |
+
bindCanvasEvents(canvas, instance.id);
|
| 985 |
+
}
|
| 986 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 987 |
|
| 988 |
+
console.log('Multi-Handwriting board initialized with parts:', structure);
|
| 989 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 990 |
|
| 991 |
+
// 設定當前活躍的 Canvas
|
| 992 |
+
function setActiveCanvas(index) {
|
| 993 |
+
if (index < 0 || index >= hwInstances.length) return;
|
| 994 |
+
activeHwIndex = index;
|
| 995 |
+
|
| 996 |
+
// 更新視覺效果
|
| 997 |
+
hwInstances.forEach(inst => {
|
| 998 |
+
if (inst.id === index) {
|
| 999 |
+
inst.container.classList.add('active-canvas');
|
| 1000 |
+
} else {
|
| 1001 |
+
inst.container.classList.remove('active-canvas');
|
| 1002 |
+
}
|
| 1003 |
+
});
|
| 1004 |
+
}
|
| 1005 |
|
| 1006 |
+
// 綁定單一 Canvas 的事件
|
| 1007 |
+
function bindCanvasEvents(canvas, index) {
|
| 1008 |
+
const start = (e) => {
|
| 1009 |
+
if (e.type === 'touchstart') e.preventDefault();
|
| 1010 |
+
setActiveCanvas(index);
|
| 1011 |
+
startStroke(e, index);
|
| 1012 |
+
};
|
| 1013 |
+
const move = (e) => {
|
| 1014 |
+
if (e.type === 'touchmove') e.preventDefault();
|
| 1015 |
+
moveStroke(e, index);
|
| 1016 |
+
};
|
| 1017 |
+
const end = (e) => {
|
| 1018 |
+
endStroke(e, index);
|
| 1019 |
+
};
|
| 1020 |
|
| 1021 |
+
canvas.addEventListener('mousedown', start);
|
| 1022 |
+
canvas.addEventListener('mousemove', move);
|
| 1023 |
+
canvas.addEventListener('mouseup', end);
|
| 1024 |
+
canvas.addEventListener('mouseleave', end);
|
| 1025 |
+
|
| 1026 |
+
canvas.addEventListener('touchstart', start, {passive: false});
|
| 1027 |
+
canvas.addEventListener('touchmove', move, {passive: false});
|
| 1028 |
+
canvas.addEventListener('touchend', end);
|
| 1029 |
+
|
| 1030 |
+
// 點擊容器也能啟用
|
| 1031 |
+
canvas.parentElement.addEventListener('click', () => setActiveCanvas(index));
|
|
|
|
| 1032 |
}
|
| 1033 |
|
| 1034 |
+
// [修改] 筆畫開始
|
| 1035 |
+
function startStroke(e, index) {
|
|
|
|
| 1036 |
isDrawing = true;
|
| 1037 |
+
const inst = hwInstances[index];
|
| 1038 |
+
if (!inst || !inst.ctx) return;
|
| 1039 |
+
|
| 1040 |
+
const pos = getPos(e, inst.element);
|
| 1041 |
+
inst.currentStroke = { x: [pos.x], y: [pos.y], t: [Date.now()] };
|
| 1042 |
|
| 1043 |
+
inst.ctx.beginPath();
|
| 1044 |
+
inst.ctx.moveTo(pos.x, pos.y);
|
| 1045 |
|
| 1046 |
+
inst.container.classList.add('canvas-active-interaction');
|
|
|
|
| 1047 |
}
|
| 1048 |
|
| 1049 |
+
// [修改] 筆畫移動
|
| 1050 |
+
function moveStroke(e, index) {
|
| 1051 |
if (!isDrawing) return;
|
| 1052 |
+
const inst = hwInstances[index];
|
| 1053 |
+
if (!inst || !inst.ctx) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1054 |
|
| 1055 |
+
const pos = getPos(e, inst.element);
|
| 1056 |
+
inst.currentStroke.x.push(pos.x);
|
| 1057 |
+
inst.currentStroke.y.push(pos.y);
|
| 1058 |
+
inst.currentStroke.t.push(Date.now());
|
| 1059 |
+
|
| 1060 |
+
inst.ctx.lineTo(pos.x, pos.y);
|
| 1061 |
+
inst.ctx.stroke();
|
| 1062 |
}
|
| 1063 |
|
| 1064 |
+
// [修改] 筆畫結束
|
| 1065 |
+
function endStroke(e, index) {
|
| 1066 |
if (!isDrawing) return;
|
| 1067 |
isDrawing = false;
|
| 1068 |
+
const inst = hwInstances[index];
|
| 1069 |
+
if (!inst || !inst.ctx) return;
|
| 1070 |
+
|
| 1071 |
+
inst.ctx.closePath();
|
| 1072 |
|
| 1073 |
+
if (inst.currentStroke.x.length > 0) {
|
| 1074 |
+
inst.strokes.push([
|
| 1075 |
+
inst.currentStroke.x,
|
| 1076 |
+
inst.currentStroke.y,
|
| 1077 |
+
inst.currentStroke.t
|
|
|
|
|
|
|
| 1078 |
]);
|
| 1079 |
}
|
| 1080 |
|
| 1081 |
+
inst.container.classList.remove('canvas-active-interaction');
|
| 1082 |
}
|
| 1083 |
|
| 1084 |
+
// [修改] 取得座標 (需傳入特定 canvas 元素)
|
| 1085 |
+
function getPos(e, canvas) {
|
|
|
|
| 1086 |
const rect = canvas.getBoundingClientRect();
|
| 1087 |
let clientX, clientY;
|
| 1088 |
|
|
|
|
| 1100 |
};
|
| 1101 |
}
|
| 1102 |
|
| 1103 |
+
// [修改] 清空當前畫布
|
| 1104 |
function clearCanvas() {
|
| 1105 |
+
const inst = hwInstances[activeHwIndex];
|
| 1106 |
+
if(!inst || !inst.ctx) return;
|
| 1107 |
|
| 1108 |
+
inst.ctx.fillStyle = '#ffffff';
|
| 1109 |
+
inst.ctx.fillRect(0, 0, inst.element.width, inst.element.height);
|
|
|
|
| 1110 |
|
| 1111 |
+
inst.strokes = [];
|
| 1112 |
+
// 如果只有一個畫布,也清空輸入框,多個畫布時不清空輸入框以免誤刪
|
| 1113 |
+
if (hwInstances.length === 1) {
|
| 1114 |
+
answerInput.value = '';
|
| 1115 |
+
}
|
| 1116 |
candidateBar.innerHTML = '';
|
| 1117 |
candidateBar.classList.add('hidden');
|
| 1118 |
}
|
| 1119 |
|
| 1120 |
+
// [修改] 復原當前畫布上一筆
|
| 1121 |
function undoStroke() {
|
| 1122 |
+
const inst = hwInstances[activeHwIndex];
|
| 1123 |
+
if (!inst || inst.strokes.length === 0) return;
|
| 1124 |
+
inst.strokes.pop();
|
| 1125 |
+
redrawCanvas(inst);
|
| 1126 |
}
|
| 1127 |
|
| 1128 |
+
// [修改] 重繪特定畫布
|
| 1129 |
+
function redrawCanvas(inst) {
|
| 1130 |
+
if(!inst || !inst.ctx) return;
|
|
|
|
| 1131 |
|
| 1132 |
+
inst.ctx.fillStyle = '#ffffff';
|
| 1133 |
+
inst.ctx.fillRect(0, 0, inst.element.width, inst.element.height);
|
|
|
|
| 1134 |
|
| 1135 |
+
inst.ctx.beginPath();
|
| 1136 |
+
inst.strokes.forEach(stroke => {
|
|
|
|
| 1137 |
const xs = stroke[0];
|
| 1138 |
const ys = stroke[1];
|
| 1139 |
if (xs.length > 0) {
|
| 1140 |
+
inst.ctx.moveTo(xs[0], ys[0]);
|
| 1141 |
for (let i = 1; i < xs.length; i++) {
|
| 1142 |
+
inst.ctx.lineTo(xs[i], ys[i]);
|
| 1143 |
}
|
| 1144 |
}
|
| 1145 |
});
|
| 1146 |
+
inst.ctx.stroke();
|
| 1147 |
}
|
| 1148 |
|
| 1149 |
// [新增] 載入 TrOCR AI 模型 (使用 dynamic import)
|
|
|
|
| 1184 |
|
| 1185 |
// [修正] 辨識手寫 (改用 TrOCR)
|
| 1186 |
async function recognizeHandwriting() {
|
| 1187 |
+
// 檢查是否有任何筆跡
|
| 1188 |
+
const hasStrokes = hwInstances.some(inst => inst.strokes.length > 0);
|
| 1189 |
+
if (!hasStrokes) {
|
| 1190 |
showToast("請先手寫內容!");
|
| 1191 |
return;
|
| 1192 |
}
|
|
|
|
| 1201 |
hwRecognizeBtn.textContent = "AI 辨識中...";
|
| 1202 |
|
| 1203 |
try {
|
| 1204 |
+
let finalParts = [];
|
| 1205 |
+
let hwIndex = 0;
|
| 1206 |
+
|
| 1207 |
+
// 依據 currentHandwritingStructure 組裝答案
|
| 1208 |
+
// 結構範例: ['cheer', '...', 'on']
|
| 1209 |
+
// hwInstances 對應: [Canvas1(cheer), Canvas2(on)]
|
| 1210 |
+
|
| 1211 |
+
// 為了平行處理提升速度,先建立所有的 Promise
|
| 1212 |
+
const recognitionPromises = hwInstances.map(async (inst) => {
|
| 1213 |
+
if (inst.strokes.length === 0) return ""; // 空畫布回傳空字串
|
| 1214 |
+
const imageData = inst.element.toDataURL("image/png");
|
| 1215 |
+
const output = await ocrPipeline(imageData);
|
| 1216 |
+
let text = output[0]?.generated_text?.trim() || "";
|
| 1217 |
+
return text.replace(/\.$/, ''); // 移除句點
|
| 1218 |
+
});
|
| 1219 |
|
| 1220 |
+
// 等待所有畫布辨識完成
|
| 1221 |
+
const recognizedTexts = await Promise.all(recognitionPromises);
|
| 1222 |
+
|
| 1223 |
+
// 依照結構組裝
|
| 1224 |
+
let recIndex = 0;
|
| 1225 |
+
currentHandwritingStructure.forEach(part => {
|
| 1226 |
+
if (part === '...') {
|
| 1227 |
+
finalParts.push('...');
|
| 1228 |
+
} else {
|
| 1229 |
+
// 取出對應的辨識結果
|
| 1230 |
+
let text = recognizedTexts[recIndex] || "";
|
| 1231 |
+
if (text) finalParts.push(text);
|
| 1232 |
+
recIndex++;
|
| 1233 |
+
}
|
| 1234 |
+
});
|
| 1235 |
+
|
| 1236 |
+
// 組合最終字串 (如果是單字就是單字,片語中間加空白,省略符號兩邊加空白)
|
| 1237 |
+
// 這裡的邏輯:把 finalParts 用空白連接即可,因為結構已經決定了順序
|
| 1238 |
+
// 例如: ['recognized_cheer', '...', 'recognized_on'] -> "recognized_cheer ... recognized_on"
|
| 1239 |
+
// 例如: ['come', 'true'] -> "come true"
|
| 1240 |
|
| 1241 |
+
const cleanedText = finalParts.join(' ');
|
| 1242 |
|
| 1243 |
+
if (cleanedText.trim()) {
|
|
|
|
|
|
|
|
|
|
| 1244 |
answerInput.value = cleanedText;
|
| 1245 |
answerInput.classList.add('border-green-500', 'bg-green-50');
|
| 1246 |
setTimeout(() => answerInput.classList.remove('border-green-500', 'bg-green-50'), 500);
|
| 1247 |
showToast("辨識結果:" + cleanedText);
|
|
|
|
|
|
|
| 1248 |
showCandidates([cleanedText]);
|
| 1249 |
} else {
|
| 1250 |
showToast("未能辨識出文字");
|
|
|
|
| 1283 |
|
| 1284 |
|
| 1285 |
// [新增] 更新輸入介面 (控制手寫板顯示與輸入法)
|
| 1286 |
+
// 參數 targetAnswer: 正確答案字串,用來判斷是否需要多重畫布
|
| 1287 |
+
function updateInputInterface(targetLanguage, targetAnswer = "") {
|
| 1288 |
const hwContainer = document.getElementById('handwriting-container');
|
| 1289 |
|
| 1290 |
// [修改] 為了方便在 PC 上除錯,暫時移除了 effectiveVersion === 'mobile' 的檢查
|
| 1291 |
// 現在只要是輸入英文模式 (expectedLang === 'en'),都會顯示手寫板
|
| 1292 |
+
const shouldUseHandwriting = (targetLanguage === 'en');
|
| 1293 |
|
| 1294 |
if (shouldUseHandwriting) {
|
| 1295 |
// --- 啟用手寫模式 ---
|
|
|
|
| 1296 |
hwContainer.classList.remove('hidden');
|
| 1297 |
hwContainer.classList.add('flex');
|
| 1298 |
|
| 1299 |
+
// 分析目標單字結構
|
| 1300 |
+
// 1. 移除詞性標記 (v.) 等
|
| 1301 |
+
let cleanAnswer = targetAnswer.replace(/\(.*\)/g, '').trim();
|
| 1302 |
+
|
| 1303 |
+
// 2. 判斷結構
|
| 1304 |
+
let structure = [];
|
| 1305 |
+
if (cleanAnswer.includes('...')) {
|
| 1306 |
+
// 分離式片語: cheer ... on -> ['cheer', '...', 'on']
|
| 1307 |
+
// 使用正則分割,但保留分隔符
|
| 1308 |
+
// 簡單做法:先拆分
|
| 1309 |
+
const parts = cleanAnswer.split('...');
|
| 1310 |
+
parts.forEach((p, i) => {
|
| 1311 |
+
if (p.trim()) structure.push(p.trim());
|
| 1312 |
+
if (i < parts.length - 1) structure.push('...');
|
| 1313 |
+
});
|
| 1314 |
+
} else if (cleanAnswer.includes(' ')) {
|
| 1315 |
+
// 一般片語: come true -> ['come', 'true']
|
| 1316 |
+
structure = cleanAnswer.split(/\s+/).filter(s => s);
|
| 1317 |
+
} else {
|
| 1318 |
+
// 單字: apple -> ['apple']
|
| 1319 |
+
structure = [cleanAnswer];
|
| 1320 |
+
}
|
| 1321 |
+
|
| 1322 |
+
// 如果 structure 是空的 (例如意外狀況),預設一個
|
| 1323 |
+
if (structure.length === 0) structure = ['word'];
|
| 1324 |
+
|
| 1325 |
+
currentHandwritingStructure = structure;
|
| 1326 |
+
|
| 1327 |
// 2. 初始化 (延遲以確保 flex 渲染完成)
|
| 1328 |
+
setTimeout(() => initMultiHandwritingBoard(structure), 400);
|
|
|
|
| 1329 |
|
| 1330 |
// 3. 觸發模型預載 (Lazy Load)
|
|
|
|
| 1331 |
if (!ocrPipeline && !isModelLoading) {
|
| 1332 |
initOCR();
|
| 1333 |
}
|
|
|
|
| 1336 |
answerInput.readOnly = true;
|
| 1337 |
answerInput.placeholder = "請在下方手寫英文...";
|
| 1338 |
answerInput.classList.add('handwriting-mode');
|
| 1339 |
+
if (document.activeElement === answerInput) answerInput.blur();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1340 |
|
| 1341 |
} else {
|
| 1342 |
// --- 啟用鍵盤模式 (PC 或 輸入中文) ---
|
|
|
|
| 1344 |
hwContainer.classList.remove('flex');
|
| 1345 |
|
| 1346 |
answerInput.readOnly = false;
|
| 1347 |
+
answerInput.placeholder = (targetLanguage === 'zh') ? "輸入中文答案 (按 Enter 提交)" : "輸入英文答案 (按 Enter 提交)";
|
| 1348 |
answerInput.classList.remove('handwriting-mode');
|
| 1349 |
}
|
| 1350 |
|
| 1351 |
+
console.log(`輸入模式: ${shouldUseHandwriting ? '手寫' : '鍵盤'}, 結構:`, currentHandwritingStructure);
|
| 1352 |
}
|
| 1353 |
|
| 1354 |
// --- 視圖管理 ---
|
|
|
|
| 1543 |
|
| 1544 |
let frontText, backText;
|
| 1545 |
let targetLanguage = 'en'; // 預設輸入英文
|
| 1546 |
+
// [新增] 紀錄正確答案以便生成手寫板結構
|
| 1547 |
+
let targetAnswerForHandwriting = '';
|
| 1548 |
|
| 1549 |
if (isSpeed) {
|
| 1550 |
// 極速挑戰
|
|
|
|
| 1553 |
frontText = card.chinese;
|
| 1554 |
backText = card.english;
|
| 1555 |
targetLanguage = 'en';
|
| 1556 |
+
targetAnswerForHandwriting = card.english;
|
| 1557 |
break;
|
| 1558 |
case 'en-zh':
|
| 1559 |
frontText = card.english;
|
|
|
|
| 1567 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.remove('hidden'));
|
| 1568 |
speakWord(card);
|
| 1569 |
targetLanguage = 'en';
|
| 1570 |
+
targetAnswerForHandwriting = card.english;
|
| 1571 |
break;
|
| 1572 |
}
|
| 1573 |
} else {
|
|
|
|
| 1582 |
// 複習模式下:沒翻面(看英文)=輸入中文,翻面(看中文)=輸入英文
|
| 1583 |
const isFlipped = flashcardContainer.classList.contains('flipped');
|
| 1584 |
targetLanguage = isFlipped ? 'en' : 'zh';
|
| 1585 |
+
// 複習模式翻面輸入英文時,目標答案是英文
|
| 1586 |
+
if (targetLanguage === 'en') targetAnswerForHandwriting = card.english;
|
| 1587 |
break;
|
| 1588 |
case 'zh-en': case 'hard':
|
| 1589 |
frontText = card.chinese; backText = card.english;
|
| 1590 |
targetLanguage = 'en';
|
| 1591 |
+
targetAnswerForHandwriting = card.english;
|
| 1592 |
break;
|
| 1593 |
case 'en-zh':
|
| 1594 |
frontText = card.english; backText = card.chinese;
|
|
|
|
| 1600 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.remove('hidden'));
|
| 1601 |
speakWord(card);
|
| 1602 |
targetLanguage = 'en';
|
| 1603 |
+
targetAnswerForHandwriting = card.english;
|
| 1604 |
break;
|
| 1605 |
case 'sentence-cloze':
|
| 1606 |
clozeQuestionContainer.classList.remove('hidden');
|
|
|
|
| 1610 |
toggleTranslationBtn.textContent = '顯示翻譯';
|
| 1611 |
backText = card.sentence.answer || card.english;
|
| 1612 |
targetLanguage = 'en';
|
| 1613 |
+
targetAnswerForHandwriting = card.sentence.answer;
|
| 1614 |
break;
|
| 1615 |
}
|
| 1616 |
}
|
| 1617 |
|
| 1618 |
+
// [新增] 傳入目標答案以判斷手寫板數量
|
| 1619 |
+
updateInputInterface(targetLanguage, targetAnswerForHandwriting);
|
| 1620 |
|
| 1621 |
if (frontText) {
|
| 1622 |
frontDisplay.classList.remove('hidden');
|
|
|
|
| 1922 |
endSpeedChallenge();
|
| 1923 |
}
|
| 1924 |
};
|
| 1925 |
+
|
| 1926 |
+
// ... (rest of the code remains the same)
|
| 1927 |
+
// 為了節省篇幅,後續程式碼保持不變
|
| 1928 |
+
// 包括 speakWord, initializeApp 等
|
| 1929 |
+
// ...
|
| 1930 |
|
| 1931 |
const endSpeedChallenge = () => {
|
| 1932 |
clearInterval(timerInterval);
|
|
|
|
| 2374 |
|
| 2375 |
// [智慧輸入切換] 複習模式下:翻面後看到中文->要輸入英文
|
| 2376 |
const targetLanguage = isFlipped ? 'en' : 'zh';
|
| 2377 |
+
// 複習模式下,翻到英文面輸入中文,翻到中文面輸入英文
|
| 2378 |
+
// 這裡的邏輯是: 沒翻面(看英文)->輸入中文(zh), 翻面(看中文)->輸入英文(en)
|
| 2379 |
+
// 所以 targetLanguage 已經是正確的目標語言
|
| 2380 |
+
// 但我們需要知道正確答案是什麼,才能判斷手寫板結構
|
| 2381 |
+
let targetAnswer = '';
|
| 2382 |
+
if (targetLanguage === 'en') {
|
| 2383 |
+
targetAnswer = wordsForCurrentMode[currentCardIndex].english;
|
| 2384 |
+
}
|
| 2385 |
+
|
| 2386 |
+
updateInputInterface(targetLanguage, targetAnswer);
|
| 2387 |
|
| 2388 |
feedbackDisplay.textContent = '';
|
| 2389 |
setTimeout(() => answerInput.focus(), 100);
|
|
|
|
| 2747 |
} else {
|
| 2748 |
// 【修改】使用新的預設單字,展示文法感知功能
|
| 2749 |
const defaultWords = [
|
| 2750 |
+
{
|
| 2751 |
+
english: 'cheer ... on',
|
| 2752 |
+
chinese: '為...加油',
|
| 2753 |
+
sentence: {
|
| 2754 |
+
en: 'We ___ the team ___.',
|
| 2755 |
+
zh: '我們為這支隊伍加油。',
|
| 2756 |
+
answer: 'cheered ... on'
|
| 2757 |
+
}
|
| 2758 |
+
},
|
| 2759 |
+
{
|
| 2760 |
+
english: 'come true',
|
| 2761 |
+
chinese: '成真'
|
| 2762 |
+
},
|
| 2763 |
{
|
| 2764 |
english: 'watch (v.)',
|
| 2765 |
chinese: '觀看',
|
|
|
|
| 2769 |
answer: 'watches'
|
| 2770 |
}
|
| 2771 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2772 |
{ english: 'study (v.)', chinese: '研讀' },
|
| 2773 |
{ english: 'last (adj.)', chinese: '前一個的' },
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2774 |
];
|
| 2775 |
words = defaultWords.map(word => ({ ...word, proficiency: 0, incorrectCount: 0 }));
|
| 2776 |
saveWordsToStorage();
|