feeday commited on
Commit
8e9cb50
·
verified ·
1 Parent(s): 0abd8a5

Create app.js

Browse files
Files changed (1) hide show
  1. app.js +671 -0
app.js ADDED
@@ -0,0 +1,671 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ===== 0. 全局状态与基础配置 ===== */
2
+ let extractedImages = [];
3
+ let currentImageIndex = -1;
4
+ let currentMediaImage = null;
5
+ let currentFilter = "";
6
+ let currentMediaDataURL = "";
7
+ let isFrameMode = false;
8
+ let chatContext = [{ role: "system", content: "你是一个得力的 AI 助手,请简明扼要地回答问题。" }];
9
+
10
+ window.addEventListener('load', () => {
11
+ const savedKey = localStorage.getItem('datxy_global_api_key');
12
+ if(savedKey) document.getElementById('globalApiKey').value = savedKey;
13
+
14
+ updateResolutionState();
15
+ document.getElementById('imageModelSelect').addEventListener('change', updateResolutionState);
16
+ updateStats();
17
+ clearApiLogs();
18
+ });
19
+
20
+ document.getElementById('globalApiKey').addEventListener('change', (e) => {
21
+ localStorage.setItem('datxy_global_api_key', e.target.value.trim());
22
+ });
23
+
24
+ // 全局监听左右键切图
25
+ window.addEventListener('keydown', (e) => {
26
+ const viewMedia = document.getElementById('view-media');
27
+ if (viewMedia.style.display !== 'none' && isFrameMode) {
28
+ if (e.key === 'ArrowLeft') {
29
+ if (currentImageIndex > 0) { currentImageIndex--; displayExtractedFrame(); }
30
+ } else if (e.key === 'ArrowRight') {
31
+ if (currentImageIndex < extractedImages.length - 1) { currentImageIndex++; displayExtractedFrame(); }
32
+ }
33
+ }
34
+ });
35
+
36
+ function switchTab(tabId) {
37
+ document.querySelectorAll('.editor-tab').forEach(t => t.classList.remove('active'));
38
+ document.querySelectorAll('.workspace-view').forEach(v => v.style.display = 'none');
39
+
40
+ document.getElementById('tab-' + tabId).classList.add('active');
41
+ document.getElementById('view-' + tabId).style.display = 'flex';
42
+ }
43
+
44
+ function toggleCollapse(contentId, headerElement) {
45
+ const content = document.getElementById(contentId);
46
+ content.classList.toggle('collapsed');
47
+ headerElement.classList.toggle('collapsed');
48
+ }
49
+
50
+ function showError(msg) {
51
+ const errorMsg = document.getElementById('errorMsg');
52
+ errorMsg.innerText = msg;
53
+ errorMsg.style.display = "block";
54
+ setTimeout(() => { errorMsg.style.display = "none"; }, 4000);
55
+ }
56
+
57
+ /* ===== 1. API 调试日志处理 ===== */
58
+ function logApiCall(type, endpoint, reqData, resData, isError = false) {
59
+ const container = document.getElementById('apiLogContainer');
60
+ if (container.innerHTML.includes("暂无 API 调用记录")) container.innerHTML = "";
61
+
62
+ const time = new Date().toLocaleTimeString();
63
+ const logHTML = `
64
+ <div class="log-entry">
65
+ <div class="log-time">[${time}] ${type} 请求: ${endpoint}</div>
66
+ <div class="log-req"><strong>📦 请求 Payload:</strong><br>${JSON.stringify(reqData, null, 2)}</div>
67
+ <div class="${isError ? 'log-err' : 'log-res'}" style="margin-top: 8px;"><strong>${isError ? '❌ 报错信息' : '✅ 响应结果'}:</strong><br>${JSON.stringify(resData, null, 2)}</div>
68
+ </div>
69
+ `;
70
+ container.innerHTML = logHTML + container.innerHTML;
71
+ }
72
+ function clearApiLogs() {
73
+ document.getElementById('apiLogContainer').innerHTML = '<div style="color: var(--text-tertiary); text-align:center; margin-top:20px;">暂无 API 调用记录...</div>';
74
+ }
75
+
76
+ /* ===== 2. 聊天对话系统 ===== */
77
+ function appendChatMessage(role, content) {
78
+ const history = document.getElementById('chatHistory');
79
+ const div = document.createElement('div');
80
+ div.className = `chat-msg ${role === 'user' ? 'msg-user' : 'msg-ai'}`;
81
+ div.innerText = content;
82
+ history.appendChild(div);
83
+ history.scrollTop = history.scrollHeight;
84
+ }
85
+
86
+ // 清空对话
87
+ document.getElementById('clearChatBtn').addEventListener('click', () => {
88
+ chatContext = [{ role: "system", content: "你是一个得力的 AI 助手,请简明扼要地回答问题。" }];
89
+ document.getElementById('chatHistory').innerHTML = '<div style="color: var(--text-tertiary); text-align: center; margin-top: 50px; font-size: 0.9rem;">对话记忆已清空。请在下方输入问题。</div>';
90
+ });
91
+
92
+ document.getElementById('chatInput').addEventListener('keydown', (e) => {
93
+ if (e.key === 'Enter' && !e.shiftKey) {
94
+ e.preventDefault();
95
+ document.getElementById('sendChatBtn').click();
96
+ }
97
+ });
98
+
99
+ document.getElementById('sendChatBtn').addEventListener('click', async () => {
100
+ const apiKey = document.getElementById('globalApiKey').value.trim();
101
+ const model = document.getElementById('chatModelSelect').value;
102
+ const inputEl = document.getElementById('chatInput');
103
+ const text = inputEl.value.trim();
104
+ const btn = document.getElementById('sendChatBtn');
105
+
106
+ if (!apiKey) return showError("❌ 请输入顶部全局 API Key");
107
+ if (!text) return;
108
+
109
+ localStorage.setItem('datxy_global_api_key', apiKey);
110
+
111
+ // UI 更新
112
+ appendChatMessage('user', text);
113
+ chatContext.push({ role: "user", content: String(text) });
114
+
115
+ inputEl.value = "";
116
+ inputEl.disabled = true;
117
+ btn.disabled = true;
118
+ btn.innerText = "思考中";
119
+
120
+ const safeMessages = chatContext.map(msg => ({ role: msg.role, content: msg.content }));
121
+ const reqPayload = { model: model, messages: safeMessages, stream: false, max_tokens: 1000 };
122
+
123
+ try {
124
+ const res = await fetch("https://api.poixe.com/v1/chat/completions", {
125
+ method: "POST",
126
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
127
+ body: JSON.stringify(reqPayload)
128
+ });
129
+
130
+ if (!res.ok) {
131
+ const errData = await res.json().catch(() => ({}));
132
+ throw new Error(errData.error?.message || `HTTP 错误码: ${res.status}`);
133
+ }
134
+
135
+ const result = await res.json();
136
+ logApiCall('智能对话', 'https://api.poixe.com/v1/chat/completions', reqPayload, result, false);
137
+
138
+ if (result.choices && result.choices[0] && result.choices[0].message) {
139
+ const aiMsg = result.choices[0].message.content;
140
+ appendChatMessage('ai', aiMsg);
141
+ chatContext.push({ role: "assistant", content: String(aiMsg) });
142
+ } else {
143
+ throw new Error('API 返回数据缺失 choices 节点');
144
+ }
145
+ } catch(e) {
146
+ showError("❌ 对话失败: " + e.message);
147
+ appendChatMessage('ai', `❌ 请求失败,请检查 API Key 或网络 (${e.message})`);
148
+ logApiCall('智能对话', 'https://api.poixe.com/v1/chat/completions', reqPayload, { error: e.message }, true);
149
+ chatContext.pop();
150
+ } finally {
151
+ inputEl.disabled = false;
152
+ btn.disabled = false;
153
+ btn.innerText = "发送";
154
+ inputEl.focus();
155
+ }
156
+ });
157
+
158
+ /* ===== 3. AI 文本测试 (针对文本区单次调用) ===== */
159
+ document.getElementById('aiProcessTextBtn').addEventListener('click', async () => {
160
+ const apiKey = document.getElementById('globalApiKey').value.trim();
161
+ const model = document.getElementById('textModelSelect').value;
162
+ const text = document.getElementById('textarea').value.trim();
163
+ const btn = document.getElementById('aiProcessTextBtn');
164
+
165
+ if (!apiKey) return showError("❌ 请输入全局 API Key");
166
+ if (!text) return showError("❌ 请在左侧文本区输入请求内容");
167
+
168
+ localStorage.setItem('datxy_global_api_key', apiKey);
169
+ switchTab('text');
170
+ btn.disabled = true;
171
+ btn.innerText = "⏳ 正在处理...";
172
+
173
+ const reqPayload = {
174
+ model: model,
175
+ messages: [{ role: "system", content: "你是一个文本处理助手。回复不超500字。" }, { role: "user", content: text }],
176
+ stream: false,
177
+ max_tokens: 1000
178
+ };
179
+
180
+ try {
181
+ const res = await fetch("https://api.poixe.com/v1/chat/completions", {
182
+ method: "POST",
183
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
184
+ body: JSON.stringify(reqPayload)
185
+ });
186
+
187
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
188
+ const result = await res.json();
189
+ logApiCall('文本单次处理', 'https://api.poixe.com/v1/chat/completions', reqPayload, result, false);
190
+
191
+ if (result.choices && result.choices[0] && result.choices[0].message) {
192
+ document.getElementById('textarea').value = result.choices[0].message.content;
193
+ } else {
194
+ document.getElementById('textarea').value = JSON.stringify(result, null, 2);
195
+ }
196
+ updateStats();
197
+ } catch(e) {
198
+ showError("❌ 请求失败: " + e.message);
199
+ logApiCall('文本单次处理', 'https://api.poixe.com/v1/chat/completions', reqPayload, { error: e.message }, true);
200
+ } finally {
201
+ btn.disabled = false;
202
+ btn.innerText = "🤖 AI 处理文本";
203
+ }
204
+ });
205
+
206
+ /* ===== 4. AI 图像生成 ===== */
207
+ function updateResolutionState() {
208
+ const modelSelect = document.getElementById('imageModelSelect');
209
+ const imageSizeSelect = document.getElementById('imageSize');
210
+ imageSizeSelect.disabled = (modelSelect.value !== 'nano-banana-pro');
211
+ }
212
+
213
+ document.getElementById('generateBtn').addEventListener('click', async () => {
214
+ const apiKey = document.getElementById('globalApiKey').value.trim();
215
+ const model = document.getElementById('imageModelSelect').value;
216
+ const prompt = document.getElementById('imagePrompt').value.trim();
217
+ const aspectRatio = document.getElementById('aspectRatio').value.split(' ')[0];
218
+ const imageSize = document.getElementById('imageSize').value.split(' ')[0];
219
+ const btn = document.getElementById('generateBtn');
220
+
221
+ if (!apiKey) return showError("❌ 请在顶部输入全局 API Key");
222
+ if (!prompt) return showError("❌ 图像提示词不能为空");
223
+
224
+ localStorage.setItem('datxy_global_api_key', apiKey);
225
+ switchTab('image');
226
+
227
+ document.getElementById('imagePlaceholder').style.display = "none";
228
+ document.getElementById('aiResultWrapper').style.display = "none";
229
+ document.getElementById('imageLoader').style.display = "block";
230
+ btn.disabled = true;
231
+ btn.innerText = "⏳ 正在绘制中...";
232
+
233
+ const requestBody = {
234
+ contents: [ { parts: [ { text: prompt } ] } ],
235
+ generationConfig: {
236
+ responseModalities: ["Text", "Image"],
237
+ imageConfig: { aspectRatio: aspectRatio, ...(model === 'nano-banana-pro' && { imageSize: imageSize }) }
238
+ }
239
+ };
240
+
241
+ try {
242
+ const response = await fetch(`https://api.poixe.com/v1beta/models/${model}:generateContent`, {
243
+ method: 'POST',
244
+ headers: { 'x-goog-api-key': apiKey, 'Content-Type': 'application/json' },
245
+ body: JSON.stringify(requestBody)
246
+ });
247
+
248
+ if (!response.ok) {
249
+ const errData = await response.json().catch(() => ({}));
250
+ throw new Error(errData.error?.message || `HTTP ${response.status}`);
251
+ }
252
+
253
+ const data = await response.json();
254
+
255
+ let logData = JSON.parse(JSON.stringify(data));
256
+ try {
257
+ if(logData.candidates && logData.candidates[0] && logData.candidates[0].content && logData.candidates[0].content.parts){
258
+ logData.candidates[0].content.parts.forEach(p => {
259
+ if(p.inlineData && p.inlineData.data) p.inlineData.data = "[Base64 Data Truncated For Log...]";
260
+ });
261
+ }
262
+ } catch(e){}
263
+ logApiCall('图像生成', `https://api.poixe.com/v1beta/models/${model}:generateContent`, requestBody, logData, false);
264
+
265
+ if (data.candidates && data.candidates.length > 0) {
266
+ let imageUrl = "";
267
+ data.candidates[0].content.parts.forEach(part => {
268
+ if (part.inlineData) imageUrl = `data:${part.inlineData.mimeType || 'image/png'};base64,${part.inlineData.data}`;
269
+ });
270
+
271
+ if (imageUrl) {
272
+ document.getElementById('aiResultImage').src = imageUrl;
273
+ document.getElementById('aiResultWrapper').style.display = "flex";
274
+ } else {
275
+ showError("⚠️ 未返回图片数据");
276
+ document.getElementById('imagePlaceholder').style.display = "flex";
277
+ }
278
+ } else {
279
+ showError("❌ 未获取到生成结果");
280
+ document.getElementById('imagePlaceholder').style.display = "flex";
281
+ }
282
+ } catch (error) {
283
+ showError("❌ 生成失败: " + error.message);
284
+ document.getElementById('imagePlaceholder').style.display = "flex";
285
+ logApiCall('图像生成', `https://api.poixe.com/v1beta/models/${model}:generateContent`, requestBody, { error: error.message }, true);
286
+ } finally {
287
+ document.getElementById('imageLoader').style.display = "none";
288
+ btn.disabled = false;
289
+ btn.innerText = "🚀 开始绘制图像";
290
+ }
291
+ });
292
+
293
+ function downloadImage() {
294
+ const img = document.getElementById('aiResultImage');
295
+ if (!img.src) return;
296
+ const link = document.createElement('a');
297
+ link.href = img.src;
298
+ link.download = `DatXY_Vision_${new Date().getTime()}.png`;
299
+ document.body.appendChild(link);
300
+ link.click();
301
+ document.body.removeChild(link);
302
+ }
303
+
304
+ /* ===== 5. 文件转码与媒体解析 ===== */
305
+ function parseMimeFromDataURI(dataURI) {
306
+ const m = /^data:([^;,]+)[^,]*,/.exec(dataURI);
307
+ return m ? m[1].trim() : '';
308
+ }
309
+
310
+ document.getElementById('btnToB64').addEventListener('click', () => {
311
+ const file = document.getElementById('fileInput').files[0];
312
+ if(!file) return showError('❌ 请选择一个文件');
313
+
314
+ if (file.type.startsWith('video/') && file.size > 5 * 1024 * 1024) {
315
+ return showError('❌ 视频文件大于5MB,不支持直接转码');
316
+ }
317
+
318
+ const btn = document.getElementById('btnToB64');
319
+ btn.innerText = "⏳ 处理中...";
320
+ const reader = new FileReader();
321
+ reader.onload = e => {
322
+ document.getElementById('textarea').value = e.target.result;
323
+ switchTab('text');
324
+ updateStats();
325
+ btn.innerText = "文件 ➔ Base64";
326
+ };
327
+ reader.onerror = () => { showError("❌ 读取失败"); btn.innerText = "文件 ➔ Base64"; };
328
+ reader.readAsDataURL(file);
329
+ });
330
+
331
+ document.getElementById('btnFromB64').addEventListener('click', () => {
332
+ const s = document.getElementById('textarea').value.trim();
333
+ if(!s || !s.startsWith('data:')) return showError('❌ 请在文本区提供有效的 Base64 DataURI');
334
+
335
+ const mime = parseMimeFromDataURI(s);
336
+ if(!mime) return showError('❌ 无法解析 MIME 类型');
337
+
338
+ switchTab('media');
339
+ document.getElementById('mediaPlaceholder').style.display = 'none';
340
+ const playerContainer = document.getElementById('mediaPlayerContainer');
341
+ const frameContainer = document.getElementById('frameDisplayContainer');
342
+ const wrapper = document.getElementById('mediaPlayerWrapper');
343
+
344
+ // Reset
345
+ wrapper.innerHTML = "";
346
+ playerContainer.style.display = 'none';
347
+ frameContainer.style.display = 'none';
348
+ currentFilter = '';
349
+ isFrameMode = false;
350
+
351
+ if(mime.startsWith('image/')){
352
+ const img = document.getElementById('frameDisplayedImage');
353
+ img.src = s;
354
+ currentMediaImage = img;
355
+ img.style.filter = currentFilter;
356
+ document.getElementById('mediaToolbar').style.display = 'flex';
357
+ document.querySelectorAll('.frame-btn').forEach(el => el.style.display = 'none');
358
+ document.getElementById('framePager').style.display = 'none';
359
+ frameContainer.style.display = 'flex';
360
+ } else if(mime.startsWith('audio/') || mime.startsWith('video/')){
361
+ const media = document.createElement(mime.startsWith('audio/') ? 'audio' : 'video');
362
+ media.controls = true; media.src = s; media.preload = 'metadata';
363
+ wrapper.appendChild(media);
364
+ currentMediaDataURL = s;
365
+ playerContainer.style.display = 'flex';
366
+ } else {
367
+ showError("⚠️ 不支持预览的媒体格式,保留在文本区");
368
+ switchTab('text');
369
+ }
370
+ });
371
+
372
+ document.getElementById('btnDownloadMedia').addEventListener('click', () => {
373
+ if(!currentMediaDataURL) return;
374
+ const mime = parseMimeFromDataURI(currentMediaDataURL);
375
+ const ext = mime.includes('audio/') ? 'mp3' : mime.includes('video/') ? 'mp4' : 'bin';
376
+ const a = document.createElement('a');
377
+ a.href = currentMediaDataURL; a.download = `media_decode.${ext}`; a.click();
378
+ });
379
+
380
+ // 视频抽帧逻辑
381
+ function seekTo(vid, t) {
382
+ return new Promise(resolve => {
383
+ const handler = () => { vid.removeEventListener('seeked', handler); resolve(); };
384
+ vid.addEventListener('seeked', handler, { once: true });
385
+ vid.currentTime = Math.min(t, Math.max(0, vid.duration - 0.001));
386
+ });
387
+ }
388
+
389
+ document.getElementById('btnFrames').addEventListener('click', async () => {
390
+ const file = document.getElementById('fileInput').files[0];
391
+ const fps = parseInt(document.getElementById('intervalInput').value, 10);
392
+ const btn = document.getElementById('btnFrames');
393
+
394
+ if(!file || !file.type.startsWith('video/')) return showError('❌ 请在上方选择一个视频文件');
395
+ if(isNaN(fps) || fps <= 0) return showError('❌ 请输入有效的抽帧率 (FPS)');
396
+
397
+ btn.innerText = "⏳ 提取中...";
398
+ const objectURL = URL.createObjectURL(file);
399
+ const video = document.createElement('video');
400
+ video.preload = 'metadata'; video.src = objectURL;
401
+
402
+ video.onloadedmetadata = async () => {
403
+ extractedImages = [];
404
+ currentImageIndex = -1;
405
+ currentFilter = '';
406
+ isFrameMode = true;
407
+
408
+ switchTab('media');
409
+ document.getElementById('mediaPlaceholder').style.display = 'none';
410
+ document.getElementById('mediaPlayerContainer').style.display = 'none';
411
+ document.getElementById('frameDisplayContainer').style.display = 'flex';
412
+
413
+ document.getElementById('mediaToolbar').style.display = 'flex';
414
+ document.querySelectorAll('.frame-btn').forEach(el => el.style.display = 'inline-flex');
415
+ document.getElementById('framePager').style.display = 'block';
416
+
417
+ const frameDuration = 1 / fps;
418
+ const total = Math.floor(video.duration * fps) + 1;
419
+ let currentTime = 0;
420
+
421
+ for(let i=0; i<total; i++){
422
+ await seekTo(video, currentTime);
423
+ const canvas = document.createElement('canvas');
424
+ canvas.width = video.videoWidth; canvas.height = video.videoHeight;
425
+ canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
426
+ extractedImages.push({ url: canvas.toDataURL('image/png'), time: currentTime });
427
+ document.getElementById('progressText') && (document.getElementById('progressText').innerText = `抽帧中: ${i+1}/${total}`);
428
+ currentTime += frameDuration;
429
+ }
430
+
431
+ btn.innerText = "🎞️ 提取视频帧";
432
+ if(extractedImages.length > 0){
433
+ currentImageIndex = 0; displayExtractedFrame();
434
+ } else {
435
+ showError('❌ 未能提取到任何帧');
436
+ }
437
+ };
438
+ });
439
+
440
+ function displayExtractedFrame() {
441
+ if(currentImageIndex < 0 || currentImageIndex >= extractedImages.length) return;
442
+ const f = extractedImages[currentImageIndex];
443
+ const img = document.getElementById('frameDisplayedImage');
444
+ img.src = f.url;
445
+ currentMediaImage = img;
446
+ img.style.filter = currentFilter;
447
+ document.getElementById('framePager').textContent = `[ ${currentImageIndex + 1} / ${extractedImages.length} ] 时间轴: ${f.time.toFixed(2)} 秒`;
448
+ }
449
+
450
+ document.getElementById('prevButton').addEventListener('click', () => { if(currentImageIndex > 0) { currentImageIndex--; displayExtractedFrame(); } });
451
+ document.getElementById('nextButton').addEventListener('click', () => { if(currentImageIndex < extractedImages.length - 1) { currentImageIndex++; displayExtractedFrame(); } });
452
+ document.getElementById('btnDownloadFrame').addEventListener('click', () => {
453
+ if(extractedImages.length === 0) return;
454
+ const f = extractedImages[currentImageIndex];
455
+ const a = document.createElement('a'); a.href = f.url; a.download = `frame_${String(currentImageIndex+1).padStart(4,'0')}.png`; a.click();
456
+ });
457
+
458
+ // 图像滤镜
459
+ document.getElementById('btnInvert').addEventListener('click', () => { currentFilter = (currentFilter + ' invert(1)').trim(); if(currentMediaImage) currentMediaImage.style.filter = currentFilter; });
460
+ document.getElementById('btnGray').addEventListener('click', () => { currentFilter = (currentFilter + ' grayscale(1)').trim(); if(currentMediaImage) currentMediaImage.style.filter = currentFilter; });
461
+ document.getElementById('btnRestore').addEventListener('click', () => { currentFilter = ''; if(currentMediaImage) currentMediaImage.style.filter = currentFilter; });
462
+ document.getElementById('btnDownloadEditedImage').addEventListener('click', () => {
463
+ if(!currentMediaImage || !currentMediaImage.src) return;
464
+ const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d');
465
+ const w = currentMediaImage.naturalWidth || currentMediaImage.width;
466
+ const h = currentMediaImage.naturalHeight || currentMediaImage.height;
467
+ canvas.width = w; canvas.height = h; ctx.filter = currentFilter || 'none';
468
+ ctx.drawImage(currentMediaImage, 0, 0, w, h);
469
+ const a = document.createElement('a'); a.href = canvas.toDataURL('image/png'); a.download = 'processed_image.png'; a.click();
470
+ });
471
+
472
+
473
+ /* 修改点:彻底清空各个视图和临时变量的所有状态 */
474
+ document.getElementById('btnClearOutput').addEventListener('click', () => {
475
+ // 1. 清空文本及查找框
476
+ document.getElementById('textarea').value = "";
477
+ document.getElementById('search-input').value = "";
478
+ document.getElementById('replace-input').value = "";
479
+ updateStats();
480
+
481
+ // 2. 清空媒体区及文件框
482
+ document.getElementById('fileInput').value = "";
483
+ extractedImages = []; currentImageIndex = -1; currentMediaImage = null; currentFilter = '';
484
+ document.getElementById('mediaPlaceholder').style.display = 'flex';
485
+ document.getElementById('mediaPlayerContainer').style.display = 'none';
486
+ document.getElementById('frameDisplayContainer').style.display = 'none';
487
+ document.getElementById('mediaPlayerWrapper').innerHTML = '';
488
+
489
+ // 3. 清空聊天记忆
490
+ chatContext = [{ role: "system", content: "你是一个得力的 AI 助手,请简明扼要地回答问题。" }];
491
+ document.getElementById('chatHistory').innerHTML = '<div style="color: var(--text-tertiary); text-align: center; margin-top: 50px; font-size: 0.9rem;">对话记忆已清空。请在下方输入问题。</div>';
492
+
493
+ // 4. 清空 AI 图像生成区
494
+ document.getElementById('imagePlaceholder').style.display = "flex";
495
+ document.getElementById('aiResultWrapper').style.display = "none";
496
+ document.getElementById('aiResultImage').src = "";
497
+ document.getElementById('imagePrompt').value = "";
498
+
499
+ // 5. 清除 API 调试日志与错误提示
500
+ clearApiLogs();
501
+ document.getElementById('errorMsg').style.display = "none";
502
+ });
503
+
504
+ /* ===== 6. 文本正则与编辑区 ===== */
505
+ const textarea = document.getElementById("textarea");
506
+ const searchInput = document.getElementById("search-input");
507
+ const replaceInput = document.getElementById("replace-input");
508
+
509
+ textarea.addEventListener("input", updateStats);
510
+
511
+ document.getElementById("remove-duplicates-btn").addEventListener("click", () => {
512
+ const textList = textarea.value.split("\n");
513
+ const uniqueList = [...new Set(textList)];
514
+ document.getElementById("duplicate").innerHTML = `重复:<span>${textList.length - uniqueList.length}</span>`;
515
+ textarea.value = uniqueList.join("\n");
516
+ updateStats();
517
+ });
518
+
519
+ document.getElementById("preserve-matches-btn").addEventListener("click", function() {
520
+ try {
521
+ const regex = new RegExp(searchInput.value, 'g');
522
+ let matches = textarea.value.match(regex);
523
+ if (matches && matches.length > 0) {
524
+ textarea.value = matches.join("\n");
525
+ document.getElementById("regex-replace-count").innerHTML = `正则:<span>${matches.length}</span>`;
526
+ } else {
527
+ document.getElementById("regex-replace-count").innerHTML = `正则:<span>0</span>`;
528
+ }
529
+ } catch (e) { showError("❌ 正则表达式有误"); }
530
+ });
531
+
532
+ document.getElementById("replace-btn").addEventListener("click", () => {
533
+ if (searchInput.value !== "") {
534
+ const textArr = textarea.value.split(searchInput.value);
535
+ document.getElementById("replace-count").innerHTML = `替换:<span>${textArr.length - 1}</span>`;
536
+ textarea.value = textArr.join(replaceInput.value);
537
+ updateStats();
538
+ }
539
+ });
540
+
541
+ document.getElementById("regex-replace-btn").addEventListener("click", () => {
542
+ if (searchInput.value !== "") {
543
+ try {
544
+ let matchCount = (textarea.value.match(new RegExp(searchInput.value, 'g')) || []).length;
545
+ textarea.value = textarea.value.replace(new RegExp(searchInput.value, 'g'), replaceInput.value);
546
+ document.getElementById("regex-replace-count").innerHTML = `正则:<span>${matchCount}</span>`;
547
+ updateStats();
548
+ } catch (e) { showError("❌ 正则表达式有误"); }
549
+ }
550
+ });
551
+
552
+ function updateStats() {
553
+ const text = textarea.value;
554
+ document.getElementById("total").innerHTML = `总数:<span>${text.length}</span>`;
555
+ document.getElementById("chinese").innerHTML = `汉字:<span>${(text.match(/[\u4e00-\u9fa5]/g) || []).length}</span>`;
556
+ document.getElementById("punctuation").innerHTML = `标点:<span>${(text.match(/[^\u4e00-\u9fa5\w]/g) || []).length}</span>`;
557
+ document.getElementById("alphabet").innerHTML = `字母:<span>${(text.match(/[a-zA-Z]/g) || []).length}</span>`;
558
+ document.getElementById("numbers").innerHTML = `数字:<span>${(text.match(/\d/g) || []).length}</span>`;
559
+
560
+ const searchVal = searchInput.value;
561
+ if(searchVal) {
562
+ document.getElementById("replace-count").innerHTML = `替换:<span>${text.split(searchVal).length - 1}</span>`;
563
+ try{ document.getElementById("regex-replace-count").innerHTML = `正则:<span>${(text.match(new RegExp(searchVal, 'g')) || []).length}</span>`; }catch(e){}
564
+ }
565
+
566
+ const lines = text.split('\n'); const map = new Map(); let dup=0;
567
+ for(const line of lines){ if(line) map.set(line,(map.get(line)||0)+1); }
568
+ for(const cnt of map.values()){ if(cnt>1) dup += (cnt-1); }
569
+ document.getElementById("duplicate").innerHTML = `重复:<span>${dup}</span>`;
570
+ }
571
+
572
+ document.getElementById("copy-btn").addEventListener("click", function() {
573
+ let textToCopy = textarea.value || textarea.getAttribute("placeholder");
574
+ const tempInput = document.createElement("textarea");
575
+ document.body.appendChild(tempInput); tempInput.value = textToCopy;
576
+ tempInput.select(); document.execCommand("copy"); document.body.removeChild(tempInput);
577
+
578
+ // UI 反馈
579
+ const originalText = this.innerHTML;
580
+ this.innerHTML = "已复";
581
+ setTimeout(() => { this.innerHTML = originalText; }, 2000);
582
+ });
583
+
584
+ document.getElementById("paste-btn").addEventListener("click", function() {
585
+ navigator.clipboard.readText().then(clipText => {
586
+ textarea.value = clipText;
587
+ updateStats();
588
+
589
+ // UI 反馈
590
+ const originalText = this.innerHTML;
591
+ this.innerHTML = "已粘";
592
+ setTimeout(() => { this.innerHTML = originalText; }, 2000);
593
+ }).catch(err => {
594
+ showError("❌ 无法访问剪贴板,请手动粘贴");
595
+ });
596
+ });
597
+
598
+ /* 修改点:新增了清空文本与网页预览功能 */
599
+ document.getElementById("clear-text-btn").addEventListener("click", function() {
600
+ textarea.value = "";
601
+ updateStats();
602
+ });
603
+
604
+ document.getElementById("preview-web-btn").addEventListener("click", function() {
605
+ const htmlContent = textarea.value.trim();
606
+ if (!htmlContent) return showError("❌ 文本区为空,无法预览");
607
+
608
+ // 生成 Blob URL 进行安全的新标签页渲染
609
+ const blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8' });
610
+ const url = URL.createObjectURL(blob);
611
+ window.open(url, '_blank');
612
+ });
613
+
614
+ // 7. Web Speech Synthesis (TTS)
615
+ (function() {
616
+ const voiceSelect = document.getElementById('tts-voiceSelect');
617
+ const filterChineseBox = document.getElementById('tts-filterChinese');
618
+ let speaking = false;
619
+
620
+ function listVoices() {
621
+ const voices = window.speechSynthesis.getVoices();
622
+ voiceSelect.innerHTML = '';
623
+ const filtered = filterChineseBox.checked ? voices.filter(v => v.lang && v.lang.toLowerCase().startsWith('zh')) : voices;
624
+ if (filtered.length === 0) return voiceSelect.innerHTML = '<option disabled>无可用语音</option>';
625
+ filtered.forEach(v => {
626
+ const opt = document.createElement('option');
627
+ opt.value = v.name; opt.textContent = `${v.name} (${v.lang})`; voiceSelect.appendChild(opt);
628
+ });
629
+ const saved = localStorage.getItem('tts_voice_name');
630
+ if (saved) {
631
+ const idx = [...voiceSelect.options].findIndex(o => o.value === saved);
632
+ if (idx >= 0) voiceSelect.selectedIndex = idx;
633
+ }
634
+ }
635
+
636
+ document.getElementById('tts-start').addEventListener('click', () => {
637
+ const text = document.getElementById('textarea').value.trim();
638
+ if (!text) return showError('❌ 请在左侧文本区输入要朗读的内容');
639
+ const v = window.speechSynthesis.getVoices().find(v => v.name === voiceSelect.value);
640
+ if (!v) return showError('❌ 请选择语音');
641
+
642
+ if (speaking) { window.speechSynthesis.cancel(); }
643
+ speaking = true;
644
+ switchTab('text');
645
+
646
+ const chunks = text.split(/\n+/).map(s=>s.trim()).filter(Boolean).flatMap(line => line.match(/[^。!?!?;;\.…]+[。!?!?;;\…]?/g) || [line]);
647
+ const next = () => {
648
+ if (!speaking || chunks.length === 0) { speaking = false; return; }
649
+ const utt = new SpeechSynthesisUtterance(chunks.shift().trim());
650
+ utt.voice = v; utt.lang = v.lang;
651
+ utt.rate = parseFloat(document.getElementById('tts-rate').value);
652
+ utt.pitch = parseFloat(document.getElementById('tts-pitch').value);
653
+ utt.volume = parseFloat(document.getElementById('tts-volume').value);
654
+ utt.onend = next; utt.onerror = next;
655
+ window.speechSynthesis.speak(utt);
656
+ };
657
+ next();
658
+ localStorage.setItem('tts_voice_name', v.name);
659
+ });
660
+
661
+ document.getElementById('tts-stop').addEventListener('click', () => { speaking = false; window.speechSynthesis.cancel(); });
662
+ filterChineseBox.addEventListener('change', listVoices);
663
+ voiceSelect.addEventListener('change', () => { localStorage.setItem('tts_voice_name', voiceSelect.value); });
664
+
665
+ document.getElementById('tts-rate').addEventListener('input', (e) => document.getElementById('tts-rate-val').textContent = e.target.value + 'x');
666
+ document.getElementById('tts-pitch').addEventListener('input', (e) => document.getElementById('tts-pitch-val').textContent = e.target.value);
667
+ document.getElementById('tts-volume').addEventListener('input', (e) => document.getElementById('tts-volume-val').textContent = e.target.value);
668
+
669
+ listVoices();
670
+ if (typeof window.speechSynthesis !== 'undefined') window.speechSynthesis.onvoiceschanged = listVoices;
671
+ })();