nbugs commited on
Commit
5093685
·
verified ·
1 Parent(s): 2bf9d39

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.js +1403 -0
  2. index.html +0 -19
app.js ADDED
@@ -0,0 +1,1403 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 全局变量
2
+ let pdfFile = null;
3
+ let markdownContent = '';
4
+ let translationContent = '';
5
+ let imagesData = [];
6
+
7
+ // DOM 元素
8
+ const mistralApiKeyInput = document.getElementById('mistralApiKey');
9
+ const toggleMistralKeyBtn = document.getElementById('toggleMistralKey');
10
+ const rememberMistralKeyCheckbox = document.getElementById('rememberMistralKey');
11
+ const translationApiKeyInput = document.getElementById('translationApiKey');
12
+ const toggleTranslationKeyBtn = document.getElementById('toggleTranslationKey');
13
+ const rememberTranslationKeyCheckbox = document.getElementById('rememberTranslationKey');
14
+
15
+ const translationModelSelect = document.getElementById('translationModel');
16
+ const customModelSettings = document.getElementById('customModelSettings');
17
+
18
+ // 高级设置相关
19
+ const advancedSettingsToggle = document.getElementById('advancedSettingsToggle');
20
+ const advancedSettings = document.getElementById('advancedSettings');
21
+ const advancedSettingsIcon = document.getElementById('advancedSettingsIcon');
22
+ const maxTokensPerChunk = document.getElementById('maxTokensPerChunk');
23
+ const maxTokensPerChunkValue = document.getElementById('maxTokensPerChunkValue');
24
+
25
+ // 文件上传相关
26
+ const dropZone = document.getElementById('dropZone');
27
+ const pdfFileInput = document.getElementById('pdfFileInput');
28
+ const browseFilesBtn = document.getElementById('browseFilesBtn');
29
+ const fileInfo = document.getElementById('fileInfo');
30
+ const fileName = document.getElementById('fileName');
31
+ const fileSize = document.getElementById('fileSize');
32
+ const removeFileBtn = document.getElementById('removeFileBtn');
33
+
34
+ // 翻译相关
35
+ const targetLanguage = document.getElementById('targetLanguage');
36
+
37
+ // 按钮
38
+ const processBtn = document.getElementById('processBtn');
39
+ const downloadMarkdownBtn = document.getElementById('downloadMarkdownBtn');
40
+ const downloadTranslationBtn = document.getElementById('downloadTranslationBtn');
41
+
42
+ // 结果展示
43
+ const resultsSection = document.getElementById('resultsSection');
44
+ const markdownPreview = document.getElementById('markdownPreview');
45
+ const translationPreview = document.getElementById('translationPreview');
46
+ const translationResultCard = document.getElementById('translationResultCard');
47
+
48
+ // 进度相关
49
+ const progressSection = document.getElementById('progressSection');
50
+ const progressStep = document.getElementById('progressStep');
51
+ const progressPercentage = document.getElementById('progressPercentage');
52
+ const progressBar = document.getElementById('progressBar');
53
+ const progressLog = document.getElementById('progressLog');
54
+
55
+ document.addEventListener('DOMContentLoaded', () => {
56
+ // 初始化 - 从本地存储加载 API Key
57
+ if (localStorage.getItem('mistralApiKey')) {
58
+ mistralApiKeyInput.value = localStorage.getItem('mistralApiKey');
59
+ rememberMistralKeyCheckbox.checked = true;
60
+ }
61
+
62
+ if (localStorage.getItem('translationApiKey')) {
63
+ translationApiKeyInput.value = localStorage.getItem('translationApiKey');
64
+ rememberTranslationKeyCheckbox.checked = true;
65
+ }
66
+
67
+ // 加载设置
68
+ loadSettings();
69
+
70
+ // API Key 显示/隐藏切换
71
+ toggleMistralKeyBtn.addEventListener('click', () => {
72
+ if (mistralApiKeyInput.type === 'password') {
73
+ mistralApiKeyInput.type = 'text';
74
+ toggleMistralKeyBtn.innerHTML = '<iconify-icon icon="carbon:view-off" width="20"></iconify-icon>';
75
+ } else {
76
+ mistralApiKeyInput.type = 'password';
77
+ toggleMistralKeyBtn.innerHTML = '<iconify-icon icon="carbon:view" width="20"></iconify-icon>';
78
+ }
79
+ });
80
+
81
+ toggleTranslationKeyBtn.addEventListener('click', () => {
82
+ if (translationApiKeyInput.type === 'password') {
83
+ translationApiKeyInput.type = 'text';
84
+ toggleTranslationKeyBtn.innerHTML = '<iconify-icon icon="carbon:view-off" width="20"></iconify-icon>';
85
+ } else {
86
+ translationApiKeyInput.type = 'password';
87
+ toggleTranslationKeyBtn.innerHTML = '<iconify-icon icon="carbon:view" width="20"></iconify-icon>';
88
+ }
89
+ });
90
+
91
+ // API Key 记住选项
92
+ rememberMistralKeyCheckbox.addEventListener('change', () => {
93
+ if (rememberMistralKeyCheckbox.checked) {
94
+ localStorage.setItem('mistralApiKey', mistralApiKeyInput.value);
95
+ } else {
96
+ localStorage.removeItem('mistralApiKey');
97
+ }
98
+ });
99
+
100
+ rememberTranslationKeyCheckbox.addEventListener('change', () => {
101
+ if (rememberTranslationKeyCheckbox.checked) {
102
+ localStorage.setItem('translationApiKey', translationApiKeyInput.value);
103
+ } else {
104
+ localStorage.removeItem('translationApiKey');
105
+ }
106
+ });
107
+
108
+ mistralApiKeyInput.addEventListener('input', () => {
109
+ if (rememberMistralKeyCheckbox.checked) {
110
+ localStorage.setItem('mistralApiKey', mistralApiKeyInput.value);
111
+ }
112
+ });
113
+
114
+ translationApiKeyInput.addEventListener('input', () => {
115
+ if (rememberTranslationKeyCheckbox.checked) {
116
+ localStorage.setItem('translationApiKey', translationApiKeyInput.value);
117
+ }
118
+ });
119
+
120
+ // PDF 文件拖放上传
121
+ dropZone.addEventListener('dragover', (e) => {
122
+ e.preventDefault();
123
+ dropZone.classList.add('border-blue-500', 'bg-blue-50');
124
+ });
125
+
126
+ dropZone.addEventListener('dragleave', () => {
127
+ dropZone.classList.remove('border-blue-500', 'bg-blue-50');
128
+ });
129
+
130
+ dropZone.addEventListener('drop', (e) => {
131
+ e.preventDefault();
132
+ dropZone.classList.remove('border-blue-500', 'bg-blue-50');
133
+
134
+ if (e.dataTransfer.files.length > 0 && e.dataTransfer.files[0].type === 'application/pdf') {
135
+ handleFileSelection(e.dataTransfer.files[0]);
136
+ } else {
137
+ showNotification('请上传PDF文件', 'error');
138
+ }
139
+ });
140
+
141
+ // 浏览文件按钮
142
+ browseFilesBtn.addEventListener('click', () => {
143
+ pdfFileInput.click();
144
+ });
145
+
146
+ // 文件选择处理
147
+ pdfFileInput.addEventListener('change', (e) => {
148
+ if (e.target.files.length > 0) {
149
+ handleFileSelection(e.target.files[0]);
150
+ }
151
+ });
152
+
153
+ // 移除文件
154
+ removeFileBtn.addEventListener('click', () => {
155
+ pdfFile = null;
156
+ fileInfo.classList.add('hidden');
157
+ pdfFileInput.value = '';
158
+ updateProcessButtonState();
159
+ });
160
+
161
+ // 处理按钮
162
+ processBtn.addEventListener('click', async () => {
163
+ try {
164
+ const mistralKey = mistralApiKeyInput.value.trim();
165
+
166
+ if (!mistralKey) {
167
+ showNotification('请输入Mistral API Key', 'error');
168
+ return;
169
+ }
170
+
171
+ if (!pdfFile) {
172
+ showNotification('请上传PDF文件', 'error');
173
+ return;
174
+ }
175
+
176
+ // 开始处理
177
+ processBtn.disabled = true;
178
+ showProgressSection();
179
+ updateProgress('开始处理...', 5);
180
+ addProgressLog('开始OCR处理...');
181
+
182
+ try {
183
+ // 执行OCR处理
184
+ await processPdfWithMistral(mistralKey);
185
+
186
+ // 如果选择了翻译,则执行翻译
187
+ if (translationModelSelect.value !== 'none') {
188
+ const translationKey = translationApiKeyInput.value.trim();
189
+ if (translationModelSelect.value !== 'none' && !translationKey) {
190
+ showNotification('请输入翻译API Key', 'error');
191
+ updateProgress('翻译需要API Key', 100);
192
+ addProgressLog('错误: 缺少翻译API Key');
193
+ processBtn.disabled = false;
194
+ return;
195
+ }
196
+ updateProgress('开始翻译...', 60);
197
+ addProgressLog(`使用${translationModelSelect.value}模型进行翻译...`);
198
+
199
+ // 获取文档大小估计
200
+ const estimatedTokens = estimateTokenCount(markdownContent);
201
+ const tokenLimit = 12000; // 设置一个安全的token限制
202
+
203
+ if (estimatedTokens > tokenLimit) {
204
+ // 使用分段翻译
205
+ addProgressLog(`文档较大(~${Math.round(estimatedTokens/1000)}K tokens),将进行分段翻译`);
206
+ translationContent = await translateLongDocument(markdownContent, targetLanguage.value, translationModelSelect.value, translationKey);
207
+ } else {
208
+ // 直接翻译
209
+ addProgressLog(`文档较小(~${Math.round(estimatedTokens/1000)}K tokens),不分段直接翻译`);
210
+ translationContent = await translateMarkdown(markdownContent, targetLanguage.value, translationModelSelect.value, translationKey);
211
+ }
212
+ }
213
+
214
+ // 显示结果
215
+ updateProgress('处理完成!', 100);
216
+ addProgressLog('全部处理完成!');
217
+ showResultsSection();
218
+ } catch (error) {
219
+ console.error('处理错误:', error);
220
+ showNotification('处理过程中出错: ' + error.message, 'error');
221
+ addProgressLog('错误: ' + error.message);
222
+ updateProgress('处理失败', 100);
223
+ } finally {
224
+ processBtn.disabled = false;
225
+ }
226
+ } catch (error) {
227
+ console.error('处理错误:', error);
228
+ showNotification('处理过程中出错: ' + error.message, 'error');
229
+ addProgressLog('错误: ' + error.message);
230
+ updateProgress('处理失败', 100);
231
+ processBtn.disabled = false;
232
+ }
233
+ });
234
+
235
+ // 下载按钮
236
+ downloadMarkdownBtn.addEventListener('click', () => {
237
+ if (markdownContent) {
238
+ downloadMarkdownWithImages();
239
+ }
240
+ });
241
+
242
+ downloadTranslationBtn.addEventListener('click', () => {
243
+ if (translationContent) {
244
+ //downloadText(translationContent, 'translation.md');
245
+ downloadTranslationWithImages();
246
+ }
247
+ });
248
+
249
+ // 翻译模型变更
250
+ translationModelSelect.addEventListener('change', function() {
251
+ if (this.value === 'custom') {
252
+ customModelSettings.classList.remove('hidden');
253
+ } else {
254
+ customModelSettings.classList.add('hidden');
255
+ }
256
+
257
+ // 更新翻译界面可见性
258
+ updateTranslationUIVisibility();
259
+
260
+ // 保存设置
261
+ saveSettings();
262
+ });
263
+
264
+ // 高级设置开关
265
+ advancedSettingsToggle.addEventListener('click', function() {
266
+ advancedSettings.classList.toggle('hidden');
267
+
268
+ // 更新图标方向
269
+ if (advancedSettings.classList.contains('hidden')) {
270
+ advancedSettingsIcon.setAttribute('icon', 'carbon:chevron-down');
271
+ } else {
272
+ advancedSettingsIcon.setAttribute('icon', 'carbon:chevron-up');
273
+ }
274
+
275
+ // 保存设置
276
+ saveSettings();
277
+ });
278
+
279
+ // 最大Token数设置滑动条
280
+ maxTokensPerChunk.addEventListener('input', function() {
281
+ maxTokensPerChunkValue.textContent = this.value;
282
+ saveSettings();
283
+ });
284
+
285
+ // 为自定义模型设置添加变更事件监听器
286
+ const customModelInputs = [
287
+ document.getElementById('customModelName'),
288
+ document.getElementById('customApiEndpoint'),
289
+ document.getElementById('customModelId'),
290
+ document.getElementById('customRequestFormat')
291
+ ];
292
+
293
+ customModelInputs.forEach(input => {
294
+ input.addEventListener('change', function() {
295
+ saveSettings();
296
+ });
297
+ // 同时监听输入事件,实时保存
298
+ input.addEventListener('input', function() {
299
+ saveSettings();
300
+ });
301
+ });
302
+
303
+ // 初始化 UI 状态
304
+ updateProcessButtonState();
305
+ updateTranslationUIVisibility();
306
+ });
307
+
308
+ // 辅助函数
309
+ function handleFileSelection(file) {
310
+ pdfFile = file;
311
+ document.getElementById('fileName').textContent = file.name;
312
+ document.getElementById('fileSize').textContent = formatFileSize(file.size);
313
+ document.getElementById('fileInfo').classList.remove('hidden');
314
+ updateProcessButtonState();
315
+ }
316
+
317
+ function updateProcessButtonState() {
318
+ const mistralKey = document.getElementById('mistralApiKey').value.trim();
319
+ const processBtn = document.getElementById('processBtn');
320
+ processBtn.disabled = !pdfFile || !mistralKey;
321
+ }
322
+
323
+ function updateTranslationUIVisibility() {
324
+ const translationModelValue = translationModelSelect.value;
325
+
326
+ // 如果选择了翻译模型,显示API Key输入框和提示
327
+ const translationApiKeySection = document.querySelector('#translationApiKey').closest('div').parentNode;
328
+ if (translationModelValue !== 'none') {
329
+ translationApiKeySection.style.display = 'block';
330
+ } else {
331
+ translationApiKeySection.style.display = 'none';
332
+ }
333
+ }
334
+
335
+ function showResultsSection() {
336
+ document.getElementById('progressSection').classList.add('hidden');
337
+ document.getElementById('resultsSection').classList.remove('hidden');
338
+
339
+ // 显示Markdown内容
340
+ document.getElementById('markdownPreview').textContent = markdownContent.substring(0, 500) + '...';
341
+
342
+ // 显示翻译内容(如果有)
343
+ if (translationContent) {
344
+ document.getElementById('translationPreview').textContent = translationContent.substring(0, 500) + '...';
345
+ document.getElementById('translationResultCard').classList.remove('hidden');
346
+ } else {
347
+ document.getElementById('translationResultCard').classList.add('hidden');
348
+ }
349
+
350
+ window.scrollTo({
351
+ top: document.getElementById('resultsSection').offsetTop - 20,
352
+ behavior: 'smooth'
353
+ });
354
+ }
355
+
356
+ function showProgressSection() {
357
+ document.getElementById('resultsSection').classList.add('hidden');
358
+ document.getElementById('progressSection').classList.remove('hidden');
359
+ document.getElementById('progressLog').innerHTML = '';
360
+ updateProgress('初始化...', 0);
361
+
362
+ window.scrollTo({
363
+ top: document.getElementById('progressSection').offsetTop - 20,
364
+ behavior: 'smooth'
365
+ });
366
+ }
367
+
368
+ function updateProgress(stepText, percentage) {
369
+ document.getElementById('progressStep').textContent = stepText;
370
+ document.getElementById('progressPercentage').textContent = `${percentage}%`;
371
+ document.getElementById('progressBar').style.width = `${percentage}%`;
372
+ }
373
+
374
+ function addProgressLog(text) {
375
+ const logElement = document.getElementById('progressLog');
376
+ const timestamp = new Date().toLocaleTimeString();
377
+ logElement.innerHTML += `<div>[${timestamp}] ${text}</div>`;
378
+ logElement.scrollTop = logElement.scrollHeight;
379
+ }
380
+
381
+ function showNotification(message, type = 'info') {
382
+ // 创建通知元素
383
+ const notification = document.createElement('div');
384
+
385
+ // ��据类型设置样式和图标
386
+ let bgColor, iconName, textColor;
387
+ switch (type) {
388
+ case 'success':
389
+ bgColor = 'bg-green-50 border-green-500';
390
+ textColor = 'text-green-800';
391
+ iconName = 'carbon:checkmark-filled';
392
+ break;
393
+ case 'error':
394
+ bgColor = 'bg-red-50 border-red-500';
395
+ textColor = 'text-red-800';
396
+ iconName = 'carbon:error-filled';
397
+ break;
398
+ case 'warning':
399
+ bgColor = 'bg-yellow-50 border-yellow-500';
400
+ textColor = 'text-yellow-800';
401
+ iconName = 'carbon:warning-filled';
402
+ break;
403
+ default: // info
404
+ bgColor = 'bg-blue-50 border-blue-500';
405
+ textColor = 'text-blue-800';
406
+ iconName = 'carbon:information-filled';
407
+ }
408
+
409
+ // 设置通知样式
410
+ notification.className = `flex items-center p-4 mb-4 max-w-md border-l-4 ${bgColor} ${textColor} shadow-md rounded-r-lg transform transition-all duration-300 ease-in-out`;
411
+ notification.style.opacity = '0';
412
+ notification.style.transform = 'translateX(100%)';
413
+
414
+ // 设置通知内容
415
+ notification.innerHTML = `
416
+ <iconify-icon icon="${iconName}" class="flex-shrink-0 w-5 h-5 mr-2"></iconify-icon>
417
+ <div class="ml-3 text-sm font-medium flex-grow">${message}</div>
418
+ <button type="button" class="ml-auto -mx-1.5 -my-1.5 rounded-lg p-1.5 inline-flex h-8 w-8 hover:bg-gray-200 focus:ring-2 focus:ring-gray-400">
419
+ <iconify-icon icon="carbon:close" class="w-5 h-5"></iconify-icon>
420
+ </button>
421
+ `;
422
+
423
+ // 获取通知容器
424
+ const container = document.getElementById('notification-container');
425
+ container.appendChild(notification);
426
+
427
+ // 显示动画
428
+ setTimeout(() => {
429
+ notification.style.opacity = '1';
430
+ notification.style.transform = 'translateX(0)';
431
+ }, 10);
432
+
433
+ // 添加关闭按钮点击事件
434
+ const closeButton = notification.querySelector('button');
435
+ closeButton.addEventListener('click', () => {
436
+ closeNotification(notification);
437
+ });
438
+
439
+ // 自动关闭(5秒后)
440
+ const timeout = setTimeout(() => {
441
+ closeNotification(notification);
442
+ }, 5000);
443
+
444
+ // 保存timeout引用,以便可以在手动关闭时清除
445
+ notification.dataset.timeout = timeout;
446
+
447
+ // 返回通知元素,以便可以手动关闭
448
+ return notification;
449
+ }
450
+
451
+ // 关闭通知的辅助函数
452
+ function closeNotification(notification) {
453
+ // 清除自动关闭的timeout
454
+ clearTimeout(notification.dataset.timeout);
455
+
456
+ // 淡出动画
457
+ notification.style.opacity = '0';
458
+ notification.style.transform = 'translateX(100%)';
459
+
460
+ // 动画完成后移除元素
461
+ setTimeout(() => {
462
+ if (notification.parentNode) {
463
+ notification.parentNode.removeChild(notification);
464
+ }
465
+ }, 300);
466
+ }
467
+
468
+ function formatFileSize(bytes) {
469
+ if (bytes < 1024) return bytes + ' B';
470
+ else if (bytes < 1048576) return (bytes / 1024).toFixed(2) + ' KB';
471
+ else return (bytes / 1048576).toFixed(2) + ' MB';
472
+ }
473
+
474
+ // Mistral OCR 处理
475
+ async function processPdfWithMistral(apiKey) {
476
+ try {
477
+ addProgressLog('准备PDF文件...');
478
+ updateProgress('PDF处理准备中', 10);
479
+
480
+ // 检查API密钥长度
481
+ if (apiKey.length < 30) {
482
+ throw new Error('Mistral API密钥格式可能不正确,请检查');
483
+ }
484
+
485
+ // 从上传文件到获取OCR结果的完整流程
486
+ const formData = new FormData();
487
+ // 关键点:文件上传字段名必须是file
488
+ formData.append('file', pdfFile);
489
+ formData.append('purpose', 'ocr');
490
+
491
+ addProgressLog('准备上传PDF文件...');
492
+ updateProgress('上传文件中...', 20);
493
+ addProgressLog('开始上传到Mistral...');
494
+
495
+ console.log('开始上传文件,文件名:', pdfFile.name, '文件大小:', pdfFile.size);
496
+
497
+ // 尝试上传文件
498
+ let response;
499
+ try {
500
+ response = await fetch('https://api.mistral.ai/v1/files', {
501
+ method: 'POST',
502
+ headers: {
503
+ 'Authorization': `Bearer ${apiKey}`
504
+ // 不要设置Content-Type,让浏览器自动设置multipart/form-data和boundary
505
+ },
506
+ body: formData
507
+ });
508
+ } catch (uploadError) {
509
+ console.error('上传错误详情:', uploadError);
510
+ addProgressLog(`网络错误: ${uploadError.message || '未知网络错误'}`);
511
+ throw new Error(`文件上传失败,网络错误: ${uploadError.message || '未知网络错误'}`);
512
+ }
513
+
514
+ if (!response.ok) {
515
+ let errorInfo = '未知错误';
516
+ try {
517
+ const responseText = await response.text();
518
+ console.error('上传失败原始响应:', responseText);
519
+ try {
520
+ const jsonError = JSON.parse(responseText);
521
+ errorInfo = jsonError.error?.message || jsonError.message || jsonError.detail || responseText;
522
+ } catch (e) {
523
+ errorInfo = responseText || `HTTP错误: ${response.status} ${response.statusText}`;
524
+ }
525
+ } catch (e) {
526
+ errorInfo = `HTTP错误: ${response.status} ${response.statusText}`;
527
+ }
528
+
529
+ addProgressLog(`上传失败: ${response.status} - ${errorInfo}`);
530
+
531
+ if (response.status === 401) {
532
+ throw new Error('API密钥无效或未授权,请检查您的Mistral API密钥');
533
+ } else {
534
+ throw new Error(`文件上传失败 (${response.status}): ${errorInfo}`);
535
+ }
536
+ }
537
+
538
+ let fileData;
539
+ try {
540
+ fileData = await response.json();
541
+ console.log('文件上传响应:', JSON.stringify(fileData));
542
+ } catch (e) {
543
+ console.error('解析文件数据错误:', e);
544
+ throw new Error('无法解析文件上传响应数据');
545
+ }
546
+
547
+ if (!fileData || !fileData.id) {
548
+ console.error('文件数据无效:', fileData);
549
+ throw new Error('上传成功但未返回有效的文件ID');
550
+ }
551
+
552
+ const fileId = fileData.id;
553
+ addProgressLog(`文件上传成功,ID: ${fileId}`);
554
+ updateProgress('获取文件访问权限...', 30);
555
+
556
+ // 确保fileId是有效的字符串
557
+ if (typeof fileId !== 'string' || fileId.trim() === '') {
558
+ throw new Error('文件ID无效,无法继续处理');
559
+ }
560
+
561
+ // 等待一下确保文件已经处理完成
562
+ await new Promise(resolve => setTimeout(resolve, 1000));
563
+
564
+ // 获取签名URL - 使用文档中的确切格式
565
+ try {
566
+ // 使用/url端点并传递expiry参数
567
+ const urlEndpoint = `https://api.mistral.ai/v1/files/${fileId}/url?expiry=24`;
568
+ console.log('请求签名URL:', urlEndpoint);
569
+
570
+ response = await fetch(urlEndpoint, {
571
+ method: 'GET',
572
+ headers: {
573
+ 'Authorization': `Bearer ${apiKey}`,
574
+ 'Accept': 'application/json'
575
+ }
576
+ });
577
+ } catch (urlError) {
578
+ console.error('获取URL错误详情:', urlError);
579
+ addProgressLog(`获取URL错误: ${urlError.message || '未知网络错误'}`);
580
+ throw new Error(`获取签名URL失败,网络错误: ${urlError.message || '未知网络错误'}`);
581
+ }
582
+
583
+ if (!response.ok) {
584
+ let errorInfo = '未知错误';
585
+ try {
586
+ const responseText = await response.text();
587
+ console.error('获取URL失败原始响应:', responseText);
588
+ try {
589
+ const jsonError = JSON.parse(responseText);
590
+ errorInfo = jsonError.error?.message || jsonError.message || jsonError.detail || responseText;
591
+ } catch (e) {
592
+ errorInfo = responseText || `HTTP错误: ${response.status} ${response.statusText}`;
593
+ }
594
+ } catch (e) {
595
+ errorInfo = `HTTP错误: ${response.status} ${response.statusText}`;
596
+ }
597
+
598
+ addProgressLog(`获取签名URL失败: ${response.status} - ${errorInfo}`);
599
+ throw new Error(`获取签名URL失败 (${response.status}): ${errorInfo}`);
600
+ }
601
+
602
+ let urlData;
603
+ try {
604
+ urlData = await response.json();
605
+ console.log('签名URL响应:', JSON.stringify(urlData));
606
+ } catch (e) {
607
+ console.error('解析URL数据错误:', e);
608
+ throw new Error('无法解析签名URL响应数据');
609
+ }
610
+
611
+ if (!urlData || !urlData.url) {
612
+ console.error('URL数据无效:', urlData);
613
+ addProgressLog('返回的URL格式不正确');
614
+ throw new Error('获取的签名URL格式不正确');
615
+ }
616
+
617
+ const signedUrl = urlData.url;
618
+ addProgressLog('成功获取文件访问URL');
619
+ updateProgress('开始OCR处理...', 40);
620
+
621
+ // 进行OCR处理 - 请求体需要匹配最新文档
622
+ try {
623
+ response = await fetch('https://api.mistral.ai/v1/ocr', {
624
+ method: 'POST',
625
+ headers: {
626
+ 'Authorization': `Bearer ${apiKey}`,
627
+ 'Content-Type': 'application/json',
628
+ 'Accept': 'application/json'
629
+ },
630
+ body: JSON.stringify({
631
+ // 完全匹配文档示例
632
+ model: 'mistral-ocr-latest',
633
+ document: {
634
+ type: "document_url",
635
+ document_url: signedUrl
636
+ },
637
+ include_image_base64: true
638
+ })
639
+ });
640
+ } catch (ocrError) {
641
+ console.error('OCR错误详情:', ocrError);
642
+ addProgressLog(`OCR处理网络错误: ${ocrError.message || '未知网络错误'}`);
643
+ throw new Error(`OCR处理失败,网络错误: ${ocrError.message || '未知网络错误'}`);
644
+ }
645
+
646
+ if (!response.ok) {
647
+ let errorInfo = '未知错误';
648
+ try {
649
+ const responseText = await response.text();
650
+ console.error('OCR处理失败原始响应:', responseText);
651
+ try {
652
+ const jsonError = JSON.parse(responseText);
653
+ errorInfo = jsonError.error?.message || jsonError.message || jsonError.detail || responseText;
654
+ } catch (e) {
655
+ errorInfo = responseText || `HTTP错误: ${response.status} ${response.statusText}`;
656
+ }
657
+ } catch (e) {
658
+ errorInfo = `HTTP错误: ${response.status} ${response.statusText}`;
659
+ }
660
+
661
+ addProgressLog(`OCR处理失败: ${response.status} - ${errorInfo}`);
662
+ throw new Error(`OCR处理失败 (${response.status}): ${errorInfo}`);
663
+ }
664
+
665
+ let ocrData;
666
+ try {
667
+ ocrData = await response.json();
668
+ console.log('OCR处理成功,返回数据类型:', typeof ocrData);
669
+ } catch (e) {
670
+ console.error('解析OCR数据错误:', e);
671
+ throw new Error('无法解析OCR处理响应数据');
672
+ }
673
+
674
+ if (!ocrData || !ocrData.pages) {
675
+ console.error('OCR数据无效:', ocrData);
676
+ throw new Error('OCR处理成功但返回的数据格式不正确');
677
+ }
678
+
679
+ addProgressLog('OCR处理完成,开始生成Markdown');
680
+ updateProgress('生成Markdown...', 50);
681
+
682
+ // 处理OCR结果
683
+ await processOcrResults(ocrData);
684
+ addProgressLog('Markdown生成完成');
685
+
686
+ return true;
687
+ } catch (error) {
688
+ console.error('Mistral OCR处理错误:', error);
689
+ addProgressLog(`处理失败: ${error.message || '未知错误'}`);
690
+ throw error;
691
+ }
692
+ }
693
+
694
+ // 处理OCR结果
695
+ async function processOcrResults(ocrResponse) {
696
+ try {
697
+ markdownContent = '';
698
+ imagesData = [];
699
+
700
+ // 处理每一页
701
+ for (const page of ocrResponse.pages) {
702
+ // 处理图片
703
+ const pageImages = {};
704
+
705
+ for (const img of page.images) {
706
+ const imgId = img.id;
707
+ const imgData = img.image_base64;
708
+ imagesData.push({
709
+ id: imgId,
710
+ data: imgData
711
+ });
712
+ pageImages[imgId] = `images/${imgId}.png`;
713
+ }
714
+
715
+ // 替换Markdown中的图片引用
716
+ let pageMarkdown = page.markdown;
717
+ for (const [imgName, imgPath] of Object.entries(pageImages)) {
718
+ pageMarkdown = pageMarkdown.replace(
719
+ new RegExp(`!\\[${imgName}\\]\\(${imgName}\\)`, 'g'),
720
+ `![${imgName}](${imgPath})`
721
+ );
722
+ }
723
+
724
+ markdownContent += pageMarkdown + '\n\n';
725
+ }
726
+
727
+ return true;
728
+ } catch (error) {
729
+ console.error('处理OCR结果错误:', error);
730
+ throw new Error('处理OCR结果失败: ' + error.message);
731
+ }
732
+ }
733
+
734
+ // 翻译Markdown
735
+ async function translateMarkdown(markdownText, targetLang, model, apiKey) {
736
+ try {
737
+ // 允许使用全局变量或传入参数
738
+ const content = markdownText || markdownContent;
739
+ const lang = targetLang || document.getElementById('targetLanguage').value;
740
+ const selectedModel = model || document.getElementById('translationModel').value;
741
+ const key = apiKey || document.getElementById('translationApiKey').value.trim();
742
+
743
+ if (!content) {
744
+ throw new Error('没有要翻译的内容');
745
+ }
746
+
747
+ if (!key) {
748
+ throw new Error('未提供API密钥');
749
+ }
750
+
751
+ if (selectedModel === 'none') {
752
+ return content; // 不需要翻译
753
+ }
754
+
755
+ // 修正targetLanguage值
756
+ const actualLang = lang === 'chinese' ? 'zh' : lang;
757
+
758
+ // 构建统一的翻译提示词
759
+ const translationPromptTemplate = `请将以下${actualLang === 'zh' ? '英文' : '中文'}内容翻译为${actualLang === 'zh' ? '中文' : '英文'},
760
+ 要求:
761
+
762
+ 1. 保持所有Markdown语法元素不变(如#标题, *斜体*, **粗体**, [链接](), ![图片]()等)
763
+
764
+ 2. 学术/专业术语应准确翻译,必要时可保留英文原文在括号中
765
+
766
+ 3. 保持原文的段落结构和格式
767
+
768
+ 4. 仅翻译内容,不要添加额外解释
769
+
770
+ 5. 对于行间公式,使用:
771
+ $$
772
+ ...
773
+ $$
774
+ 标记
775
+
776
+ 文档内容��
777
+
778
+ ${content}`;
779
+
780
+ //对温度等参数做统一默认设置, 若未单独设置, 则使用默认值
781
+ const temperature = 0.5;
782
+ const maxTokens = 100000;
783
+ const sys_prompt = "你是一个专业的文档翻译助手,擅长保持原文档格式进行精确翻译。";
784
+
785
+ // 配置各种翻译API
786
+ const apiConfigs = {
787
+ 'deepseek': {
788
+ endpoint: 'https://api.deepseek.com/v1/chat/completions',
789
+ modelName: 'DeepSeek v3 (deepseek-v3)',
790
+ headers: {
791
+ 'Content-Type': 'application/json',
792
+ 'Authorization': `Bearer ${key}`
793
+ },
794
+ bodyBuilder: () => ({
795
+ model: "deepseek-v3",
796
+ messages: [
797
+ { role: "system", content: sys_prompt },
798
+ { role: "user", content: translationPromptTemplate }
799
+ ],
800
+ temperature: temperature,
801
+ max_tokens: maxTokens
802
+ }),
803
+ responseExtractor: (data) => data.choices[0].message.content
804
+ },
805
+ 'gemini': {
806
+ endpoint: `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${key}`,
807
+ modelName: 'Google Gemini 2.0 Flash',
808
+ headers: { 'Content-Type': 'application/json' },
809
+ bodyBuilder: () => ({
810
+ contents: [
811
+ {
812
+ role: "user",
813
+ parts: [{ text: translationPromptTemplate }]
814
+ }
815
+ ],
816
+ generationConfig: {
817
+ temperature: temperature,
818
+ maxOutputTokens: maxTokens
819
+ }
820
+ }),
821
+ responseExtractor: (data) => data.candidates[0].content.parts[0].text
822
+ },
823
+ 'claude': {
824
+ endpoint: 'https://api.anthropic.com/v1/messages',
825
+ modelName: 'Claude 3.5 Sonnet',
826
+ headers: {
827
+ 'Content-Type': 'application/json',
828
+ 'x-api-key': key,
829
+ 'anthropic-version': '2023-06-01'
830
+ },
831
+ bodyBuilder: () => ({
832
+ model: "claude-3-5-sonnet",
833
+ max_tokens: maxTokens,
834
+ messages: [
835
+ { role: "user", content: translationPromptTemplate }
836
+ ]
837
+ }),
838
+ responseExtractor: (data) => data.content[0].text
839
+ },
840
+ 'mistral': {
841
+ endpoint: 'https://api.mistral.ai/v1/chat/completions',
842
+ modelName: 'Mistral Large (mistral-large-latest)',
843
+ headers: {
844
+ 'Content-Type': 'application/json',
845
+ 'Authorization': `Bearer ${key}`
846
+ },
847
+ bodyBuilder: () => ({
848
+ model: "mistral-large-latest",
849
+ messages: [
850
+ { role: "system", content: sys_prompt },
851
+ { role: "user", content: translationPromptTemplate }
852
+ ],
853
+ temperature: temperature,
854
+ max_tokens: maxTokens
855
+ }),
856
+ responseExtractor: (data) => data.choices[0].message.content
857
+ },
858
+ 'tongyi-deepseek-v3': {
859
+ endpoint: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
860
+ modelName: '阿里云通义百炼 DeepSeek v3',
861
+ headers: {
862
+ 'Content-Type': 'application/json',
863
+ 'Authorization': `Bearer ${key}`
864
+ },
865
+ bodyBuilder: () => ({
866
+ model: "deepseek-v3",
867
+ messages: [
868
+ { role: "system", content: sys_prompt },
869
+ { role: "user", content: translationPromptTemplate }
870
+ ],
871
+ temperature: temperature,
872
+ max_tokens: maxTokens
873
+ }),
874
+ responseExtractor: (data) => data.choices[0].message.content
875
+ },
876
+ 'volcano-deepseek-v3': {
877
+ endpoint: 'https://api.volcengine.com/ml/api/v1/open/llm/inference',
878
+ modelName: '火山引擎 DeepSeek v3',
879
+ headers: {
880
+ 'Content-Type': 'application/json',
881
+ 'Authorization': `Bearer ${key}`
882
+ },
883
+ bodyBuilder: () => ({
884
+ model: "deepseek-v3",
885
+ messages: [
886
+ { role: "system", content: sys_prompt },
887
+ { role: "user", content: translationPromptTemplate }
888
+ ],
889
+ parameters: {
890
+ temperature: temperature,
891
+ max_tokens: maxTokens
892
+ }
893
+ }),
894
+ responseExtractor: (data) => data.choices[0].message.content
895
+ },
896
+ 'custom': {
897
+ // 自定义模型配置将动态生成
898
+ createConfig: () => {
899
+ // 获取用户设置的参数
900
+ const customModelName = document.getElementById('customModelName').value.trim();
901
+ const customApiEndpoint = document.getElementById('customApiEndpoint').value.trim();
902
+ const customModelId = document.getElementById('customModelId').value.trim();
903
+ const customRequestFormat = document.getElementById('customRequestFormat').value;
904
+
905
+ if (!customModelName || !customApiEndpoint || !customModelId) {
906
+ throw new Error('请填写完整的自定义模型信息');
907
+ }
908
+
909
+ // 根据选择的请求格式创建不同的配置
910
+ const config = {
911
+ endpoint: customApiEndpoint,
912
+ modelName: customModelName,
913
+ headers: {
914
+ 'Content-Type': 'application/json'
915
+ },
916
+ bodyBuilder: null,
917
+ responseExtractor: null
918
+ };
919
+
920
+ // 添加授权头
921
+ if (customApiEndpoint.includes('anthropic')) {
922
+ config.headers['x-api-key'] = key;
923
+ config.headers['anthropic-version'] = '2023-06-01';
924
+ } else {
925
+ config.headers['Authorization'] = `Bearer ${key}`;
926
+ }
927
+
928
+ // 根据格式设置请求体构建器和响应提取器
929
+ switch (customRequestFormat) {
930
+ case 'openai':
931
+ config.bodyBuilder = () => ({
932
+ model: customModelId,
933
+ messages: [
934
+ { role: "system", content: sys_prompt },
935
+ { role: "user", content: translationPromptTemplate }
936
+ ],
937
+ temperature: temperature,
938
+ max_tokens: maxTokens
939
+ });
940
+ config.responseExtractor = (data) => data.choices[0].message.content;
941
+ break;
942
+
943
+ case 'anthropic':
944
+ config.bodyBuilder = () => ({
945
+ model: customModelId,
946
+ max_tokens: maxTokens,
947
+ messages: [
948
+ { role: "user", content: translationPromptTemplate }
949
+ ]
950
+ });
951
+ config.responseExtractor = (data) => data.content[0].text;
952
+ break;
953
+
954
+ case 'gemini':
955
+ config.bodyBuilder = () => ({
956
+ contents: [
957
+ {
958
+ role: "user",
959
+ parts: [{ text: translationPromptTemplate }]
960
+ }
961
+ ],
962
+ generationConfig: {
963
+ temperature: temperature,
964
+ maxOutputTokens: maxTokens
965
+ }
966
+ });
967
+ config.responseExtractor = (data) => data.candidates[0].content.parts[0].text;
968
+ break;
969
+ }
970
+
971
+ return config;
972
+ }
973
+ }
974
+ };
975
+
976
+ // 选择API配置
977
+ const apiConfig = apiConfigs[selectedModel];
978
+
979
+ if (!apiConfig) {
980
+ throw new Error(`不支持的翻译模型: ${selectedModel}`);
981
+ }
982
+
983
+ addProgressLog(`正在调用${apiConfig.modelName || selectedModel}翻译API...`);
984
+ let response;
985
+ // 使用常规模型配置
986
+ if (selectedModel !== 'custom') {
987
+ response = await fetch(apiConfig.endpoint, {
988
+ method: 'POST',
989
+ headers: apiConfig.headers,
990
+ body: JSON.stringify(apiConfig.bodyBuilder())
991
+ });
992
+ } else {
993
+ // 使用自定义模型配置
994
+ const customConfig = apiConfig.createConfig();
995
+ response = await fetch(customConfig.endpoint, {
996
+ method: 'POST',
997
+ headers: customConfig.headers,
998
+ body: JSON.stringify(customConfig.bodyBuilder())
999
+ });
1000
+ }
1001
+
1002
+ if (!response.ok) {
1003
+ let errorText;
1004
+ try {
1005
+ const errorJson = await response.json();
1006
+ errorText = JSON.stringify(errorJson);
1007
+ } catch (e) {
1008
+ errorText = await response.text();
1009
+ }
1010
+
1011
+ console.error(`API错误 (${response.status}): ${errorText}`);
1012
+ throw new Error(`翻译API返回错误 (${response.status}): ${errorText.substring(0, 200)}`);
1013
+ }
1014
+
1015
+ const data = await response.json();
1016
+
1017
+ // 提取翻译后的内容
1018
+ let translatedContent;
1019
+
1020
+ if (selectedModel !== 'custom') {
1021
+ // 使用预定义模型的响应提取器
1022
+ translatedContent = apiConfig.responseExtractor(data);
1023
+ } else {
1024
+ // 使用自定义模型的响应提取器
1025
+ const customConfig = apiConfig.createConfig();
1026
+ translatedContent = customConfig.responseExtractor(data);
1027
+ }
1028
+
1029
+ try {
1030
+ // 提取翻译结果
1031
+ if (!translatedContent) {
1032
+ throw new Error('译文为空');
1033
+ }
1034
+
1035
+ return translatedContent;
1036
+ } catch (error) {
1037
+ console.error('提取翻译结果错误:', error, '原始响应:', data);
1038
+ throw new Error(`提取翻译结果失败: ${error.message}`);
1039
+ }
1040
+ } catch (error) {
1041
+ console.error('翻译错误:', error);
1042
+ throw new Error(`调用${model}翻译API失败: ${error.message}`);
1043
+ }
1044
+ }
1045
+
1046
+ // 长文档翻译函数
1047
+ async function translateLongDocument(markdownText, targetLang, model, apiKey) {
1048
+ const parts = splitMarkdownIntoChunks(markdownText);
1049
+ console.log(`将文档分割为${parts.length}个部分进行翻译`);
1050
+ addProgressLog(`文档被分割为${parts.length}个部分进行翻译`);
1051
+
1052
+ let translatedContent = '';
1053
+
1054
+ for (let i = 0; i < parts.length; i++) {
1055
+ updateProgress(`翻译第 ${i+1}/${parts.length} 部分...`, 60 + Math.floor((i / parts.length) * 30));
1056
+ addProgressLog(`正在翻译第 ${i+1}/${parts.length} 部分...`);
1057
+
1058
+ try {
1059
+ // 翻译当前部分
1060
+ const partResult = await translateMarkdown(parts[i], targetLang, model, apiKey);
1061
+ translatedContent += partResult;
1062
+
1063
+ // 添加分隔符(如果不是最后一部分)
1064
+ if (i < parts.length - 1) {
1065
+ translatedContent += '\n\n';
1066
+ }
1067
+
1068
+ // 简单的节流,避免API速率限制
1069
+ if (i < parts.length - 1) {
1070
+ await new Promise(resolve => setTimeout(resolve, 1000));
1071
+ }
1072
+ } catch (error) {
1073
+ console.error(`第 ${i+1} 部分翻译失败:`, error);
1074
+ addProgressLog(`第 ${i+1} 部分翻译失败: ${error.message}`);
1075
+
1076
+ // 继续尝试其余部分
1077
+ translatedContent += `\n\n> **翻译错误 (第 ${i+1} 部分), 使用原语言**: ${error.message}\n\n${parts[i]}\n\n`;
1078
+ }
1079
+ }
1080
+
1081
+
1082
+ return translatedContent;
1083
+ }
1084
+
1085
+ // 智能分割Markdown为多个片段
1086
+ function splitMarkdownIntoChunks(markdown) {
1087
+ // 估计每个标记的平均长度
1088
+ const estimatedTokens = estimateTokenCount(markdown);
1089
+ // 从用户设置获取最大token数限制
1090
+ const tokenLimit = parseInt(maxTokensPerChunk.value) || 2000;
1091
+
1092
+ // 如果文档足够小,不需要分割
1093
+ if (estimatedTokens <= tokenLimit) {
1094
+ return [markdown];
1095
+ }
1096
+
1097
+ // 按章节分割
1098
+ const chunks = [];
1099
+ const lines = markdown.split('\n');
1100
+ let currentChunk = [];
1101
+ let currentTokenCount = 0;
1102
+ let inCodeBlock = false;
1103
+
1104
+ // 定义标题行的正则表达式
1105
+ const headingRegex = /^#{1,6}\s+.+$/;
1106
+
1107
+ for (let i = 0; i < lines.length; i++) {
1108
+ const line = lines[i];
1109
+
1110
+ // 检测代码块
1111
+ if (line.trim().startsWith('```')) {
1112
+ inCodeBlock = !inCodeBlock;
1113
+ }
1114
+
1115
+ // 估计当前行的token数
1116
+ const lineTokens = estimateTokenCount(line);
1117
+
1118
+ // 判断是否应该在这里分割
1119
+ const isHeading = headingRegex.test(line) && !inCodeBlock;
1120
+ const wouldExceedLimit = currentTokenCount + lineTokens > tokenLimit;
1121
+
1122
+ if (isHeading && currentChunk.length > 0 && (wouldExceedLimit || currentTokenCount > tokenLimit * 0.7)) {
1123
+ // 在遇到标题且当前段已积累足够内容时分割
1124
+ chunks.push(currentChunk.join('\n'));
1125
+ currentChunk = [];
1126
+ currentTokenCount = 0;
1127
+ }
1128
+
1129
+ // 如果当前段落即使加上这一行也超过限制,而且已经有内容了
1130
+ if (!isHeading && wouldExceedLimit && currentChunk.length > 0) {
1131
+ chunks.push(currentChunk.join('\n'));
1132
+ currentChunk = [];
1133
+ currentTokenCount = 0;
1134
+ }
1135
+
1136
+ // 添加当前行到当前段落
1137
+ currentChunk.push(line);
1138
+ currentTokenCount += lineTokens;
1139
+ }
1140
+
1141
+ // 添加最后一段
1142
+ if (currentChunk.length > 0) {
1143
+ chunks.push(currentChunk.join('\n'));
1144
+ }
1145
+
1146
+ // 处理过大的段落(可能是因为没有标题或标记导致的)
1147
+ const finalChunks = [];
1148
+ for (const chunk of chunks) {
1149
+ const chunkTokens = estimateTokenCount(chunk);
1150
+ if (chunkTokens > tokenLimit) {
1151
+ // 如果段落仍然过大,按段落分割
1152
+ const subChunks = splitByParagraphs(chunk, tokenLimit);
1153
+ finalChunks.push(...subChunks);
1154
+ } else {
1155
+ finalChunks.push(chunk);
1156
+ }
1157
+ }
1158
+
1159
+ return finalChunks;
1160
+ }
1161
+
1162
+ // 按段落分割过大的文本块
1163
+ function splitByParagraphs(text, tokenLimit) {
1164
+ const paragraphs = text.split('\n\n');
1165
+ const chunks = [];
1166
+ let currentChunk = [];
1167
+ let currentTokenCount = 0;
1168
+
1169
+ for (const paragraph of paragraphs) {
1170
+ const paragraphTokens = estimateTokenCount(paragraph);
1171
+
1172
+ // 如果单个段落就超过了限制,则需要进一步分割
1173
+ if (paragraphTokens > tokenLimit) {
1174
+ // 如果当前段已有内容,先保存
1175
+ if (currentChunk.length > 0) {
1176
+ chunks.push(currentChunk.join('\n\n'));
1177
+ currentChunk = [];
1178
+ currentTokenCount = 0;
1179
+ }
1180
+
1181
+ // 按句子分割大段落
1182
+ const sentenceChunks = splitBySentences(paragraph, tokenLimit);
1183
+ chunks.push(...sentenceChunks);
1184
+ continue;
1185
+ }
1186
+
1187
+ // 检查是否加上这个段落会超出限制
1188
+ if (currentTokenCount + paragraphTokens > tokenLimit && currentChunk.length > 0) {
1189
+ chunks.push(currentChunk.join('\n\n'));
1190
+ currentChunk = [];
1191
+ currentTokenCount = 0;
1192
+ }
1193
+
1194
+ currentChunk.push(paragraph);
1195
+ currentTokenCount += paragraphTokens;
1196
+ }
1197
+
1198
+ // 添加最后一段
1199
+ if (currentChunk.length > 0) {
1200
+ chunks.push(currentChunk.join('\n\n'));
1201
+ }
1202
+
1203
+ return chunks;
1204
+ }
1205
+
1206
+ // 按句子分割过大的段落
1207
+ function splitBySentences(paragraph, tokenLimit) {
1208
+ // 简单的句子分割规则(这里可以根据需要改进)
1209
+ const sentences = paragraph.replace(/([.!?。!?])\s*/g, "$1\n").split('\n');
1210
+ const chunks = [];
1211
+ let currentChunk = [];
1212
+ let currentTokenCount = 0;
1213
+
1214
+ for (const sentence of sentences) {
1215
+ if (!sentence.trim()) continue;
1216
+
1217
+ const sentenceTokens = estimateTokenCount(sentence);
1218
+
1219
+ // 检查是否加上这个句子会超出限制
1220
+ if (currentTokenCount + sentenceTokens > tokenLimit && currentChunk.length > 0) {
1221
+ chunks.push(currentChunk.join(' '));
1222
+ currentChunk = [];
1223
+ currentTokenCount = 0;
1224
+ }
1225
+
1226
+ currentChunk.push(sentence);
1227
+ currentTokenCount += sentenceTokens;
1228
+ }
1229
+
1230
+ // 添加最后一段
1231
+ if (currentChunk.length > 0) {
1232
+ chunks.push(currentChunk.join(' '));
1233
+ }
1234
+
1235
+ return chunks;
1236
+ }
1237
+
1238
+ // 合并翻译后的片段
1239
+ function mergeTranslatedChunks(chunks) {
1240
+ // 简单合并,可以根据需要添加更复杂的处理逻辑
1241
+ return chunks.join('\n\n');
1242
+ }
1243
+
1244
+ // 估计文本的token数量(粗略估计)
1245
+ function estimateTokenCount(text) {
1246
+ if (!text) return 0;
1247
+
1248
+ // 中文和其他语言的处理方式不同
1249
+ // 英文大约每75个字符对应20个token
1250
+ // 中文大约每字符对应1.5个token
1251
+
1252
+ // 检测是否包含大量中文
1253
+ const chineseRatio = (text.match(/[\u4e00-\u9fa5]/g) || []).length / text.length;
1254
+
1255
+ if (chineseRatio > 0.5) {
1256
+ // 主要是中文文本
1257
+ return Math.ceil(text.length * 1.5);
1258
+ } else {
1259
+ // 主要是英文或其他文本
1260
+ return Math.ceil(text.length / 3.75);
1261
+ }
1262
+ }
1263
+
1264
+ // 将文件转换为Base64
1265
+ function fileToBase64(file) {
1266
+ return new Promise((resolve, reject) => {
1267
+ const reader = new FileReader();
1268
+ reader.readAsDataURL(file);
1269
+ reader.onload = () => resolve(reader.result);
1270
+ reader.onerror = error => reject(error);
1271
+ });
1272
+ }
1273
+
1274
+ // 下载单个文本文件
1275
+ function downloadText(content, filename) {
1276
+ const blob = new Blob([content], { type: 'text/markdown' });
1277
+ saveAs(blob, filename);
1278
+ }
1279
+
1280
+ // 下载包含图像的Markdown
1281
+ async function downloadMarkdownWithImages() {
1282
+ try {
1283
+ const zip = new JSZip();
1284
+
1285
+ // 添加Markdown文件
1286
+ zip.file('document.md', markdownContent);
1287
+
1288
+ // 创建images文件夹
1289
+ const imagesFolder = zip.folder('images');
1290
+
1291
+ // 添加图片
1292
+ for (const img of imagesData) {
1293
+ const imgData = img.data.split(',')[1];
1294
+ imagesFolder.file(`${img.id}.png`, imgData, { base64: true });
1295
+ }
1296
+
1297
+ // 生成并下载zip文件
1298
+ const zipBlob = await zip.generateAsync({ type: 'blob' });
1299
+ const pdfName = pdfFile ? pdfFile.name.replace('.pdf', '') : 'document';
1300
+ saveAs(zipBlob, `${pdfName}_markdown.zip`);
1301
+ } catch (error) {
1302
+ console.error('创建ZIP文件失败:', error);
1303
+ showNotification('下载失败: ' + error.message, 'error');
1304
+ }
1305
+ }
1306
+
1307
+ downloadTranslationWithImages = async () => {
1308
+ try {
1309
+ const zip = new JSZip();
1310
+
1311
+ // 直接添加声明到翻译内容
1312
+ const currentDate = new Date().toISOString().split('T')[0];
1313
+ const headerDeclaration = `> *本文档由 Paper Burner 工具制作 (${currentDate})。内容由 AI 大模型翻译生成,不保证翻译内容的准确性和完整性。*\n\n`;
1314
+ const footerDeclaration = `\n\n---\n> *免责声明:本文档内容由大模型API自动翻译生成,Paper Burner 工具不对翻译内容的准确性、完整性和合法性负责。*`;
1315
+
1316
+ // 添加Markdown文件,包含声明
1317
+ const contentToDownload = headerDeclaration + translationContent + footerDeclaration;
1318
+ zip.file('document.md', contentToDownload);
1319
+
1320
+ // 创建images文件夹
1321
+ const imagesFolder = zip.folder('images');
1322
+
1323
+ // 添加图片
1324
+ for (const img of imagesData) {
1325
+ const imgData = img.data.split(',')[1];
1326
+ imagesFolder.file(`${img.id}.png`, imgData, { base64: true });
1327
+ }
1328
+
1329
+ // 生成并下载zip文件
1330
+ const zipBlob = await zip.generateAsync({ type: 'blob' });
1331
+ const pdfName = pdfFile ? pdfFile.name.replace('.pdf', '') : 'document';
1332
+ saveAs(zipBlob, `${pdfName}_translation.zip`);
1333
+ } catch (error) {
1334
+ console.error('创建ZIP文件失败:', error);
1335
+ showNotification('下载失败: ' + error.message, 'error');
1336
+ }
1337
+ }
1338
+
1339
+ // 保存设置
1340
+ function saveSettings() {
1341
+ // 保存高级设置到本地存储
1342
+ localStorage.setItem('advancedSettings', JSON.stringify({
1343
+ maxTokensPerChunk: maxTokensPerChunk.value
1344
+ }));
1345
+
1346
+ // 如果是自定义模型,保存自定义模型设置
1347
+ if (translationModelSelect.value === 'custom') {
1348
+ localStorage.setItem('customModelSettings', JSON.stringify({
1349
+ modelName: document.getElementById('customModelName').value,
1350
+ apiEndpoint: document.getElementById('customApiEndpoint').value,
1351
+ modelId: document.getElementById('customModelId').value,
1352
+ requestFormat: document.getElementById('customRequestFormat').value
1353
+ }));
1354
+ }
1355
+
1356
+ // 保存选中的翻译模型
1357
+ localStorage.setItem('selectedTranslationModel', translationModelSelect.value);
1358
+ }
1359
+
1360
+ // 加载设置
1361
+ function loadSettings() {
1362
+ // 加载高级设置
1363
+ try {
1364
+ const advancedSettingsData = localStorage.getItem('advancedSettings');
1365
+ if (advancedSettingsData) {
1366
+ const settings = JSON.parse(advancedSettingsData);
1367
+ if (settings.maxTokensPerChunk) {
1368
+ maxTokensPerChunk.value = settings.maxTokensPerChunk;
1369
+ maxTokensPerChunkValue.textContent = settings.maxTokensPerChunk;
1370
+ }
1371
+ }
1372
+ } catch (e) {
1373
+ console.error('加载高级设置失败:', e);
1374
+ }
1375
+
1376
+ // 加载自定义模型设置
1377
+ try {
1378
+ const customModelData = localStorage.getItem('customModelSettings');
1379
+ if (customModelData) {
1380
+ const settings = JSON.parse(customModelData);
1381
+ document.getElementById('customModelName').value = settings.modelName || '';
1382
+ document.getElementById('customApiEndpoint').value = settings.apiEndpoint || '';
1383
+ document.getElementById('customModelId').value = settings.modelId || '';
1384
+ document.getElementById('customRequestFormat').value = settings.requestFormat || 'openai';
1385
+ }
1386
+ } catch (e) {
1387
+ console.error('加载自定义模型设置失败:', e);
1388
+ }
1389
+
1390
+ // 加载选中的翻译模型
1391
+ try {
1392
+ const selectedModel = localStorage.getItem('selectedTranslationModel');
1393
+ if (selectedModel) {
1394
+ translationModelSelect.value = selectedModel;
1395
+ // 如果是自定义模型,显示自定义设置
1396
+ if (selectedModel === 'custom') {
1397
+ customModelSettings.classList.remove('hidden');
1398
+ }
1399
+ }
1400
+ } catch (e) {
1401
+ console.error('加载选中的翻译模型失败:', e);
1402
+ }
1403
+ }
index.html CHANGED
@@ -1,19 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>