-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmain.js
More file actions
379 lines (329 loc) · 18.7 KB
/
Copy pathmain.js
File metadata and controls
379 lines (329 loc) · 18.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
import { elements } from './dom.js';
import * as ui from './ui.js';
import * as utils from './utils.js';
import * as state from './state.js';
import * as api from './api.js';
// Import Handlers
import * as SessionHandlers from './handlers/session.js';
import * as GenHandlers from './handlers/generator.js';
import * as LibHandlers from './handlers/library.js';
import * as ContentHandlers from './handlers/content.js';
import * as AnalyzerHandlers from './handlers/contentAnalyzer.js';
import * as IOHandlers from './handlers/io.js';
import * as EditorHandlers from './handlers/editor.js';
import * as AdaptiveUIHandlers from './handlers/adaptive_ui.js'; // [Phase 2]
import { registerNotifier } from './utils/errorHandler.js'; // [New]
// ---------------------------------------------------------
// 1. 全域功能優先暴露 (即便後續報錯,這些也能在 HTML 中被呼叫)
// ---------------------------------------------------------
window.switchAppMode = (mode) => { if (typeof ui.switchAppMode === 'function') ui.switchAppMode(mode); };
window.switchExpertType = (btn, type) => { if (typeof ui.switchExpertType === 'function') ui.switchExpertType(btn, type); };
document.addEventListener('DOMContentLoaded', () => {
console.log('%c[QuestWiz] Starting Resilient Bootloader...', 'color: #6366f1; font-weight: bold;');
// [Init] 注入 UI 通知器給錯誤處理模組 (解決循環依賴)
registerNotifier(ui.showToast);
const safeExecute = (name, task) => {
try {
task();
// console.log(`[Init] ${name} success.`);
} catch (e) {
console.error(`[Init] ${name} failed:`, e);
}
};
// ---------------------------------------------------------
// 2. 響應式系統初始化
// ---------------------------------------------------------
safeExecute('Reactive Core', () => {
if (!state.subscribe) return;
const editorCallbacks = {
onUpdateField: (index, field, value) => EditorHandlers.updateQuestionField(index, field, value),
onUpdateOption: (index, optIndex, value) => EditorHandlers.updateOption(index, optIndex, value),
onUpdateCorrect: (index, correctArr) => EditorHandlers.updateCorrectAnswer(index, correctArr),
onDelete: (index) => EditorHandlers.deleteQuestion(index),
onCopy: (index) => { EditorHandlers.copyQuestion(index); ui.showToast('題目已複製', 'success'); }
};
state.subscribe((key, value) => {
try {
if (key === 'generatedQuestions') {
if (state.isBusy('generate') || !document.activeElement?.closest('#questions-container')) {
ui.renderQuestionsForEditing(value || [], editorCallbacks);
ui.initializeSortable();
}
} else if (key === 'quizSummary') {
if (value) ui.renderQuizSummary(value);
else document.getElementById('quiz-summary-card')?.remove();
} else if (key === 'selectedKeywords') {
AnalyzerHandlers.updateHighlighter();
if (ui.refreshUI) ui.refreshUI();
} else if (['uploadedImages', 'uiRefreshTrigger'].includes(key)) {
ui.hideLoader();
if (ui.refreshUI) ui.refreshUI();
}
} catch (err) { console.error(`[Reactive Notification] Error for ${key}:`, err); }
});
});
// ---------------------------------------------------------
// 3. UI 基礎組件初始化 (分段執行,確保一處壞掉不影響他處)
// ---------------------------------------------------------
safeExecute('Populate History', () => ui.populateVersionHistory());
safeExecute('Populate Dropdowns', () => ui.populateDropdowns());
// [New] 管理員模式偵測
safeExecute('Detect Admin', () => {
const params = new URLSearchParams(window.location.search);
if (params.get('admin') === 'hsuliang') {
state.setAdminMode(true);
console.log('%c[System] 管理員權限已啟動', 'color: #ef4444; font-weight: bold;');
ui.showToast('管理員模式已啟動', 'success');
}
});
safeExecute('Apply Layout', () => ui.applyLayoutPreference());
safeExecute('Apply Theme', () => ui.applyThemePreference());
safeExecute('Init Language', () => ui.initLanguage());
safeExecute('Visitor Count', () => ui.updateVisitorCount());
safeExecute('Restore Mode', () => {
const savedMode = localStorage.getItem('appMode_v1') || 'quick';
ui.switchAppMode(savedMode);
});
safeExecute('Restore API Key', () => {
const keyDataString = sessionStorage.getItem('gemini_api_key_data');
if (keyDataString) {
const keyData = JSON.parse(keyDataString);
if (Date.now() < keyData.expires) {
// [Fix] 相容舊格式:若有 value 則還原 (支援陣列或單字串)
const val = Array.isArray(keyData.value) ? keyData.value.join('\n') : keyData.value;
if (elements.apiKeyInput) elements.apiKeyInput.value = val;
ui.startKeyTimer(keyData.expires);
}
}
});
// ---------------------------------------------------------
// 4. 事件監聽器安全綁定
// ---------------------------------------------------------
const bind = (el, event, handler, name) => {
if (el) utils.addSafeEventListener(el, event, handler, name);
};
safeExecute('Event Bindings', () => {
// 出題
bind(elements.regenerateBtn, 'click', () => { ui.showLoader('AI正在出題...'); GenHandlers.triggerQuestionGeneration(); }, 'regenerateBtn');
bind(elements.expertGenerateBtn, 'click', () => { ui.showLoader('專家模式正在出題...'); GenHandlers.triggerQuestionGeneration(); }, 'expertGenerateBtn');
// 分析
bind(elements.analyzeContentBtn, 'click', AnalyzerHandlers.handleAnalyzeContent, 'analyzeContentBtn');
bind(elements.reAnalyzeBtn, 'click', AnalyzerHandlers.handleAnalyzeContent, 'reAnalyzeBtn');
bind(elements.addCustomKeywordBtn, 'click', () => AnalyzerHandlers.handleAddCustomKeyword(), 'addCustomKeywordBtn');
if (elements.customKeywordInput) {
bind(elements.customKeywordInput, 'keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); AnalyzerHandlers.handleAddCustomKeyword(); } }, 'customKeywordInput');
}
// 文字輸入區
if (elements.textInput) {
bind(elements.textInput, 'input', (e) => {
SessionHandlers.triggerOrUpdate(e);
AnalyzerHandlers.updateHighlighter();
if (ui.refreshUI) ui.refreshUI();
elements.analyzeContentBtn?.classList.toggle('hidden', e.target.value.trim().length < 50);
}, 'textInputMain');
bind(elements.textInput, 'scroll', () => AnalyzerHandlers.syncScroll(), 'textInputScrollSync');
bind(elements.textInput, 'mouseup', AnalyzerHandlers.handleSelectionChange, 'textInputSelection');
bind(elements.textInput, 'keyup', AnalyzerHandlers.handleSelectionChange, 'textInputSelectionKey');
}
// 下載與其他
bind(elements.fileInput, 'change', (event) => IOHandlers.handleFile(event.target.files[0]), 'fileInput');
bind(elements.imageInput, 'change', (event) => {
IOHandlers.handleImageFiles(event.target.files);
setTimeout(() => { if (ui.refreshUI) ui.refreshUI(); }, 500);
}, 'imageInput');
if (elements.textInput) ui.setupDragDrop(elements.textInput, IOHandlers.handleFile, false);
if (elements.imageDropZone) ui.setupDragDrop(elements.imageDropZone, IOHandlers.handleImageFiles, true);
bind(elements.downloadBtn, 'click', IOHandlers.exportFile, 'downloadBtn');
bind(elements.clearContentBtn, 'click', SessionHandlers.clearContent, 'clearContentBtn');
bind(elements.resetBtn, 'click', SessionHandlers.hardReset, 'resetBtn');
bind(elements.extractFromUrlBtn, 'click', ContentHandlers.handleExtractFromUrl, 'extractFromUrlBtn');
bind(elements.generateContentBtn, 'click', ContentHandlers.generateContentFromTopic, 'generateContentBtn');
bind(elements.downloadTxtBtn, 'click', ContentHandlers.handleDownloadTxt, 'downloadTxtBtn');
bind(elements.shareContentBtn, 'click', ContentHandlers.handleShareContent, 'shareContentBtn');
bind(elements.editPromptBtn, 'click', ContentHandlers.handlePreviewPrompt, 'editPromptBtn');
// 彈窗與分享按鈕
bind(elements.uploadCommunityBtn, 'click', LibHandlers.handleUploadModalOpen, 'uploadCommunityBtn');
bind(elements.closeUploadModalBtn, 'click', () => ui.toggleUploadModal(false), 'closeUploadModalBtn');
bind(elements.uploadForm, 'submit', LibHandlers.handleUploadSubmit, 'uploadForm'); // [Fix] 綁定表單提交
bind(elements.continueEditingBtn, 'click', () => ui.hidePostDownloadModal(), 'continueEditingBtn');
bind(elements.clearAndNewBtn, 'click', () => {
ui.hidePostDownloadModal();
SessionHandlers.clearAllInputs();
}, 'clearAndNewBtn');
bind(elements.saveApiKeyBtn, 'click', async () => {
const rawValue = elements.apiKeyInput?.value || '';
// 解析多組金鑰:支援換行、逗號、分號分隔
const keys = rawValue.split(/[\n,;]+/).map(k => k.trim()).filter(k => k.length > 5);
if (keys.length === 0) {
ui.showToast('請輸入有效的 API Key', 'error');
return;
}
ui.showLoader('正在驗證金鑰可用性...');
try {
// 並行呼叫探針以驗證金鑰是否正常
const validationPromises = keys.map(async (key) => {
await api.validateApiKey(key);
return key;
});
const results = await Promise.allSettled(validationPromises);
const validKeys = [];
const networkFailedKeys = [];
const invalidKeys = [];
results.forEach((r, idx) => {
const key = keys[idx];
if (r.status === 'fulfilled') {
validKeys.push(r.value);
} else {
const errMsg = r.reason?.message || '';
if (errMsg.includes('NETWORK_ERROR:')) {
networkFailedKeys.push(key);
} else {
invalidKeys.push(key);
}
}
});
// 狀況一:有部分金鑰成功通過驗證
if (validKeys.length > 0) {
const expires = Date.now() + (2 * 60 * 60 * 1000);
const storageData = { value: validKeys, expires, isMulti: validKeys.length > 1 };
sessionStorage.setItem('gemini_api_key_data', JSON.stringify(storageData));
if (elements.apiKeyInput) {
elements.apiKeyInput.value = validKeys.join('\n');
}
const excludedCount = keys.length - validKeys.length;
if (excludedCount > 0) {
ui.showToast(`已成功儲存 ${validKeys.length} 組有效金鑰,自動排除 ${excludedCount} 組失效金鑰!`, 'warning');
} else {
const msg = validKeys.length > 1 ? `已儲存 ${validKeys.length} 組金鑰!` : 'API Key 已儲存!';
ui.showToast(msg, 'success');
}
ui.startKeyTimer(expires);
return;
}
// 狀況二:全數失敗,但全部/部分是因為網路或 CORS 問題無法驗證,且沒有明確判定失效的金鑰
if (networkFailedKeys.length > 0 && invalidKeys.length === 0) {
ui.hideLoader(); // 先關閉載入框,避免 confirm 阻擋時畫面全黑
const confirmSave = confirm(
`【網路連線或 CORS 限制】\n系統無法與 Google 伺服器連線驗證您的金鑰狀態(共 ${networkFailedKeys.length} 組)。\n這通常是暫時的網路問題或瀏覽器安全性限制。\n\n您是否要直接儲存這些金鑰?`
);
if (confirmSave) {
ui.showLoader('正在儲存金鑰...');
const expires = Date.now() + (2 * 60 * 60 * 1000);
const storageData = { value: networkFailedKeys, expires, isMulti: networkFailedKeys.length > 1 };
sessionStorage.setItem('gemini_api_key_data', JSON.stringify(storageData));
if (elements.apiKeyInput) {
elements.apiKeyInput.value = networkFailedKeys.join('\n');
}
ui.showToast(`已儲存 ${networkFailedKeys.length} 組金鑰(未通過線上驗證)`, 'warning');
ui.startKeyTimer(expires);
}
return;
}
// 狀況三:全數驗證失敗,且包含明確被判定無效的金鑰
ui.showToast('輸入的金鑰均無效或已耗盡額度!儲存已被攔截。', 'error');
} catch (err) {
console.error("[System] Validation failed:", err);
ui.showToast('驗證金鑰時發生未知錯誤,請稍後再試。', 'error');
} finally {
ui.hideLoader();
}
}, 'saveApiKeyBtn');
bind(elements.clearApiKeyBtn, 'click', () => {
sessionStorage.removeItem('gemini_api_key_data');
if (elements.apiKeyInput) elements.apiKeyInput.value = '';
ui.showToast('API Key 已清除', 'success');
}, 'clearApiKeyBtn');
bind(elements.toggleApiStepsBtn, 'click', () => {
if (elements.apiStepsContainer && elements.apiStepsArrow) {
elements.apiStepsContainer.classList.toggle('hidden');
elements.apiStepsArrow.classList.toggle('rotate-180');
}
}, 'toggleApiStepsBtn');
bind(elements.toggleApiKeyVisibilityBtn, 'click', () => {
if (elements.apiKeyInput) {
// 文字框改用 class 切換遮罩
elements.apiKeyInput.classList.toggle('password-mask');
}
}, 'toggleApiKeyVisibilityBtn');
// 初始狀態:預設遮罩
if (elements.apiKeyInput) {
elements.apiKeyInput.classList.add('password-mask');
}
// 文字選取與標記
const handleKeywordAdd = () => {
AnalyzerHandlers.addKeywordFromSelection();
if (elements.textInput) {
// 隱藏選單
const menu = document.getElementById('text-selection-menu');
if (menu) menu.classList.add('hidden');
}
};
// 綁定「設為考點」按鈕 (假設 ID 為 add-selection-as-keyword-btn)
const addKeyBtn = document.getElementById('add-selection-as-keyword-btn');
bind(addKeyBtn, 'click', handleKeywordAdd, 'addSelectionKeywordBtn');
// 分享彈窗按鈕
bind(elements.copyLinkBtn, 'click', ContentHandlers.handleCopyLink, 'copyLinkBtn');
bind(elements.closeShareModalBtn, 'click', ui.hideShareModal, 'closeShareModalBtn');
// 版本歷程
bind(elements.versionBtn, 'click', () => ui.showVersionModal(), 'versionBtn');
bind(elements.closeModalBtn, 'click', () => ui.hideVersionModal(), 'closeModalBtn');
bind(elements.collapseSettingsBtn, 'click', () => {
elements.commonSettingsCard?.classList.toggle('is-collapsed');
}, 'collapseSettingsBtn');
bind(elements.toggleBloomBtn, 'click', () => {
elements.bloomOptionsContainer?.classList.toggle('hidden');
elements.bloomArrow?.classList.toggle('rotate-180');
}, 'toggleBloomBtn');
bind(elements.mobileFab, 'click', () => {
const isUp = elements.mobileFab?.classList.contains('rotate-180');
if (!isUp) elements.previewColumn?.scrollIntoView({ behavior: 'smooth' });
else window.scrollTo({ top: 0, behavior: 'smooth' });
}, 'mobileFab');
// Tabs
elements.tabs.settings.buttons?.forEach((btn, i) => bind(btn, 'click', () => ui.switchTab('settings', i)));
elements.tabs.input.buttons?.forEach((btn, i) => bind(btn, 'click', () => ui.switchTab('input', i)));
elements.workTabs.buttons?.forEach((btn, i) => bind(btn, 'click', () => {
const id = i === 0 ? 'edit' : 'library';
ui.switchWorkTab(id);
if (id === 'library') LibHandlers.refreshLibrary();
}));
// 學生程度連動 (治本版:事件委派)
const updateAllLevels = (val) => {
document.querySelectorAll('.student-level-sync').forEach(s => {
if (s.value !== val) s.value = val;
});
// [Fix] 強制同步適性化滑桿
if (AdaptiveUIHandlers && typeof AdaptiveUIHandlers.updateSliderFromExternal === 'function') {
AdaptiveUIHandlers.updateSliderFromExternal(val);
}
if (typeof ui.refreshUI === 'function') ui.refreshUI();
};
// 監聽整個文件的 change 事件,只處理帶有 .student-level-sync 的元素
document.addEventListener('change', (e) => {
if (e.target && e.target.classList.contains('student-level-sync')) {
updateAllLevels(e.target.value);
}
});
// 題庫篩選器
const filterIds = ['lib-domain-select', 'lib-grade-select', 'lib-publisher-select', 'lib-issue-select'];
filterIds.forEach(id => {
const el = document.getElementById(id);
if (el) bind(el, 'change', LibHandlers.handleLibraryFilterChange, id);
});
});
// [Phase 2] 適性化 UI 初始化
safeExecute('Adaptive UI Init', () => {
AdaptiveUIHandlers.initAdaptiveUI();
});
// ---------------------------------------------------------
// 5. 終端啟動
// ---------------------------------------------------------
safeExecute('Final Post-Init', () => {
SessionHandlers.restoreDraft();
SessionHandlers.bindAutoSave();
SessionHandlers.checkContentAndToggleButton();
ui.updateRegenerateButtonState();
LibHandlers.refreshLibrary();
});
console.log('%c[QuestWiz] Resilient Boot Complete.', 'color: #10b981; font-weight: bold;');
});