docslight 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,405 @@
1
+ const storageKey = "docslight.language";
2
+
3
+ export const supportedLanguages = [
4
+ { code: "en", label: "English", htmlLang: "en" },
5
+ { code: "zh-CN", label: "简体中文", htmlLang: "zh-CN" },
6
+ { code: "zh-TW", label: "繁體中文", htmlLang: "zh-TW" },
7
+ ];
8
+
9
+ const translations = {
10
+ en: {
11
+ "app.title": "DocSlight Workbench",
12
+ "nav.parse": "Parse",
13
+ "nav.extract": "Extract",
14
+ "language.label": "Language",
15
+ "health.checking": "Checking service...",
16
+ "health.status": "{service}: {status}",
17
+ "health.unavailable": "Local service unavailable",
18
+ "mode.label": "Processing mode",
19
+ "mode.cloud": "Cloud",
20
+ "mode.local": "Local",
21
+ "cloud.baseUrl": "Cloud Base URL",
22
+ "cloud.apiKey": "API key",
23
+ "cloud.apiKeyPlaceholder": "Cloud API key",
24
+ "cloud.extractMode": "Cloud model",
25
+ "cloud.enableGrounding": "Enable grounding",
26
+ "drop.choose": "Choose document",
27
+ "drop.formats": "PDF, image, DOCX, PPTX, XLSX",
28
+ "drop.none": "No file selected",
29
+ "common.download": "Download",
30
+ "common.metadataPreview": "Metadata preview",
31
+ "preview.title": "Document preview",
32
+ "preview.specimen": "Document specimen",
33
+ "preview.noHighlight": "No highlight selected",
34
+ "preview.empty": "Select a file to load a preview.",
35
+ "preview.officeUnsupported": "Office files can be processed, but preview and positioning highlight are not supported in this version.",
36
+ "preview.loadingTitle": "Loading preview",
37
+ "preview.rendering": "Rendering document preview...",
38
+ "preview.failedHttp": "Preview failed with HTTP {status}",
39
+ "preview.unexpectedError": "Unexpected preview error",
40
+ "preview.unavailable": "Preview is not available for this document type.",
41
+ "preview.pageLabel": "Page {page}",
42
+ "highlight.noPositioning": "No positioning data available for this selection.",
43
+ "highlight.cloud": "Precise cloud positioning highlighted {count} region(s).",
44
+ "highlight.local": "Coarse local positioning highlighted {count} region(s).",
45
+ "highlight.parse": "Parse block positioning highlighted {count} region(s).",
46
+ "parse.pageTitle": "Parse | DocSlight Workbench",
47
+ "parse.workbench": "Parse workbench",
48
+ "parse.eyebrow": "Parse setup",
49
+ "parse.title": "Parse documents",
50
+ "parse.description": "Convert documents into layout blocks, Markdown, and raw JSON.",
51
+ "parse.localNote": "Local parsing uses the configured local runtime.",
52
+ "parse.run": "Run parse",
53
+ "parse.resultsTitle": "Parse results",
54
+ "parse.tabs.blocks": "Blocks",
55
+ "parse.tabs.markdown": "Markdown",
56
+ "parse.tabs.json": "JSON",
57
+ "parse.placeholder": "Parse results will appear here after you run a parse.",
58
+ "parse.metadataEmpty": "No parse metadata yet.",
59
+ "parse.failed": "Parse failed.",
60
+ "extract.pageTitle": "Extract | DocSlight Workbench",
61
+ "extract.workbench": "Extract workbench",
62
+ "extract.eyebrow": "Extract setup",
63
+ "extract.title": "Extract fields",
64
+ "extract.description": "Define fields and tables, then extract structured values from a document.",
65
+ "extract.localLlm": "Local LLM",
66
+ "extract.provider": "Provider",
67
+ "extract.model": "Model",
68
+ "extract.baseUrl": "Base URL",
69
+ "extract.apiKey": "API key",
70
+ "extract.optionalPlaceholder": "optional",
71
+ "extract.run": "Run extract",
72
+ "extract.resultsTitle": "Extract results",
73
+ "extract.tabs.fields": "Fields",
74
+ "extract.tabs.json": "JSON",
75
+ "extract.placeholder": "Extract results will appear here after you run an extract.",
76
+ "extract.metadataEmpty": "No extract metadata yet.",
77
+ "extract.failed": "Extract failed.",
78
+ "fields.title": "Fields",
79
+ "fields.templateName": "Template name",
80
+ "fields.templatePlaceholder": "Invoice",
81
+ "fields.addField": "Add field",
82
+ "fields.addTable": "Add table",
83
+ "fields.field": "Field",
84
+ "fields.name": "Name",
85
+ "fields.prompt": "Prompt",
86
+ "fields.mapping": "Mapping",
87
+ "fields.remove": "Remove",
88
+ "fields.column": "Column",
89
+ "fields.removeColumn": "Remove column",
90
+ "fields.table": "Table",
91
+ "fields.tableName": "Table name",
92
+ "fields.addColumn": "Add column",
93
+ "fields.removeTable": "Remove table",
94
+ "fields.namePlaceholder": "Title",
95
+ "fields.promptPlaceholder": "Optional extraction prompt",
96
+ "fields.mappingPlaceholder": "Optional mapping key",
97
+ "fields.tableNamePlaceholder": "Table_1",
98
+ "fields.columnNamePlaceholder": "Unit Price",
99
+ "fields.defaultFieldName": "Field",
100
+ "fields.initialFieldName": "Title",
101
+ "fields.defaultColumnName": "Column",
102
+ "fields.initialColumnName": "Unit Price",
103
+ "error.selectDocument": "Please select a document first.",
104
+ "error.cloudApiKeyRequired": "Cloud mode requires an API key.",
105
+ "error.localLlmRequired": "Please complete Local LLM provider, model, and base URL.",
106
+ "error.fieldsRequired": "Please add at least one field or table column.",
107
+ },
108
+ "zh-CN": {
109
+ "app.title": "DocSlight 工作台",
110
+ "nav.parse": "解析",
111
+ "nav.extract": "抽取",
112
+ "language.label": "语言",
113
+ "health.checking": "正在检查服务...",
114
+ "health.status": "{service}: {status}",
115
+ "health.unavailable": "本地服务不可用",
116
+ "mode.label": "处理模式",
117
+ "mode.cloud": "云端",
118
+ "mode.local": "本地",
119
+ "cloud.baseUrl": "云端 Base URL",
120
+ "cloud.apiKey": "API 密钥",
121
+ "cloud.apiKeyPlaceholder": "云端 API 密钥",
122
+ "cloud.extractMode": "云端模型",
123
+ "cloud.enableGrounding": "启用 grounding",
124
+ "drop.choose": "选择文档",
125
+ "drop.formats": "PDF、图片、DOCX、PPTX、XLSX",
126
+ "drop.none": "未选择文件",
127
+ "common.download": "下载",
128
+ "common.metadataPreview": "元数据预览",
129
+ "preview.title": "文档预览",
130
+ "preview.specimen": "文档样本",
131
+ "preview.noHighlight": "未选择高亮区域",
132
+ "preview.empty": "选择文件后加载预览。",
133
+ "preview.officeUnsupported": "Office 文件可以处理,但当前版本不支持预览和定位高亮。",
134
+ "preview.loadingTitle": "正在加载预览",
135
+ "preview.rendering": "正在渲染文档预览...",
136
+ "preview.failedHttp": "预览失败,HTTP {status}",
137
+ "preview.unexpectedError": "预览出现意外错误",
138
+ "preview.unavailable": "此文档类型暂不支持预览。",
139
+ "preview.pageLabel": "第 {page} 页",
140
+ "highlight.noPositioning": "当前选择没有可用的定位数据。",
141
+ "highlight.cloud": "已高亮 {count} 个云端精准定位区域。",
142
+ "highlight.local": "已高亮 {count} 个本地粗略定位区域。",
143
+ "highlight.parse": "已高亮 {count} 个解析块定位区域。",
144
+ "parse.pageTitle": "解析 | DocSlight 工作台",
145
+ "parse.workbench": "解析工作台",
146
+ "parse.eyebrow": "解析设置",
147
+ "parse.title": "解析文档",
148
+ "parse.description": "将文档转换为版面块、Markdown 和原始 JSON。",
149
+ "parse.localNote": "本地解析会使用已配置的本地运行环境。",
150
+ "parse.run": "开始解析",
151
+ "parse.resultsTitle": "解析结果",
152
+ "parse.tabs.blocks": "区块",
153
+ "parse.tabs.markdown": "Markdown",
154
+ "parse.tabs.json": "JSON",
155
+ "parse.placeholder": "运行解析后,结果会显示在这里。",
156
+ "parse.metadataEmpty": "暂无解析元数据。",
157
+ "parse.failed": "解析失败。",
158
+ "extract.pageTitle": "抽取 | DocSlight 工作台",
159
+ "extract.workbench": "抽取工作台",
160
+ "extract.eyebrow": "抽取设置",
161
+ "extract.title": "抽取字段",
162
+ "extract.description": "定义字段和表格,并从文档中抽取结构化值。",
163
+ "extract.localLlm": "本地 LLM",
164
+ "extract.provider": "提供商",
165
+ "extract.model": "模型",
166
+ "extract.baseUrl": "Base URL",
167
+ "extract.apiKey": "API 密钥",
168
+ "extract.optionalPlaceholder": "可选",
169
+ "extract.run": "开始抽取",
170
+ "extract.resultsTitle": "抽取结果",
171
+ "extract.tabs.fields": "字段",
172
+ "extract.tabs.json": "JSON",
173
+ "extract.placeholder": "运行抽取后,结果会显示在这里。",
174
+ "extract.metadataEmpty": "暂无抽取元数据。",
175
+ "extract.failed": "抽取失败。",
176
+ "fields.title": "字段",
177
+ "fields.templateName": "模板名称",
178
+ "fields.templatePlaceholder": "发票",
179
+ "fields.addField": "添加字段",
180
+ "fields.addTable": "添加表格",
181
+ "fields.field": "字段",
182
+ "fields.name": "名称",
183
+ "fields.prompt": "提示词",
184
+ "fields.mapping": "映射",
185
+ "fields.remove": "移除",
186
+ "fields.column": "列",
187
+ "fields.removeColumn": "移除列",
188
+ "fields.table": "表格",
189
+ "fields.tableName": "表格名称",
190
+ "fields.addColumn": "添加列",
191
+ "fields.removeTable": "移除表格",
192
+ "fields.namePlaceholder": "标题",
193
+ "fields.promptPlaceholder": "可选抽取提示词",
194
+ "fields.mappingPlaceholder": "可选映射键",
195
+ "fields.tableNamePlaceholder": "表格_1",
196
+ "fields.columnNamePlaceholder": "单价",
197
+ "fields.defaultFieldName": "字段",
198
+ "fields.initialFieldName": "标题",
199
+ "fields.defaultColumnName": "列",
200
+ "fields.initialColumnName": "单价",
201
+ "error.selectDocument": "请先选择一个文档。",
202
+ "error.cloudApiKeyRequired": "云端模式需要 API 密钥。",
203
+ "error.localLlmRequired": "请完整填写本地 LLM 提供商、模型和 Base URL。",
204
+ "error.fieldsRequired": "请至少添加一个字段或表格列。",
205
+ },
206
+ "zh-TW": {
207
+ "app.title": "DocSlight 工作台",
208
+ "nav.parse": "解析",
209
+ "nav.extract": "擷取",
210
+ "language.label": "語言",
211
+ "health.checking": "正在檢查服務...",
212
+ "health.status": "{service}: {status}",
213
+ "health.unavailable": "本機服務無法使用",
214
+ "mode.label": "處理模式",
215
+ "mode.cloud": "雲端",
216
+ "mode.local": "本機",
217
+ "cloud.baseUrl": "雲端 Base URL",
218
+ "cloud.apiKey": "API 金鑰",
219
+ "cloud.apiKeyPlaceholder": "雲端 API 金鑰",
220
+ "cloud.extractMode": "雲端模型",
221
+ "cloud.enableGrounding": "啟用 grounding",
222
+ "drop.choose": "選擇文件",
223
+ "drop.formats": "PDF、圖片、DOCX、PPTX、XLSX",
224
+ "drop.none": "尚未選擇檔案",
225
+ "common.download": "下載",
226
+ "common.metadataPreview": "中繼資料預覽",
227
+ "preview.title": "文件預覽",
228
+ "preview.specimen": "文件樣本",
229
+ "preview.noHighlight": "尚未選擇高亮區域",
230
+ "preview.empty": "選擇檔案後載入預覽。",
231
+ "preview.officeUnsupported": "Office 檔案可以處理,但目前版本不支援預覽和定位高亮。",
232
+ "preview.loadingTitle": "正在載入預覽",
233
+ "preview.rendering": "正在渲染文件預覽...",
234
+ "preview.failedHttp": "預覽失敗,HTTP {status}",
235
+ "preview.unexpectedError": "預覽發生未預期錯誤",
236
+ "preview.unavailable": "此文件類型暫不支援預覽。",
237
+ "preview.pageLabel": "第 {page} 頁",
238
+ "highlight.noPositioning": "目前選擇沒有可用的定位資料。",
239
+ "highlight.cloud": "已高亮 {count} 個雲端精準定位區域。",
240
+ "highlight.local": "已高亮 {count} 個本機粗略定位區域。",
241
+ "highlight.parse": "已高亮 {count} 個解析區塊定位區域。",
242
+ "parse.pageTitle": "解析 | DocSlight 工作台",
243
+ "parse.workbench": "解析工作台",
244
+ "parse.eyebrow": "解析設定",
245
+ "parse.title": "解析文件",
246
+ "parse.description": "將文件轉換為版面區塊、Markdown 和原始 JSON。",
247
+ "parse.localNote": "本機解析會使用已設定的本機執行環境。",
248
+ "parse.run": "開始解析",
249
+ "parse.resultsTitle": "解析結果",
250
+ "parse.tabs.blocks": "區塊",
251
+ "parse.tabs.markdown": "Markdown",
252
+ "parse.tabs.json": "JSON",
253
+ "parse.placeholder": "執行解析後,結果會顯示在這裡。",
254
+ "parse.metadataEmpty": "暫無解析中繼資料。",
255
+ "parse.failed": "解析失敗。",
256
+ "extract.pageTitle": "擷取 | DocSlight 工作台",
257
+ "extract.workbench": "擷取工作台",
258
+ "extract.eyebrow": "擷取設定",
259
+ "extract.title": "擷取欄位",
260
+ "extract.description": "定義欄位和表格,並從文件中擷取結構化值。",
261
+ "extract.localLlm": "本機 LLM",
262
+ "extract.provider": "提供商",
263
+ "extract.model": "模型",
264
+ "extract.baseUrl": "Base URL",
265
+ "extract.apiKey": "API 金鑰",
266
+ "extract.optionalPlaceholder": "選填",
267
+ "extract.run": "開始擷取",
268
+ "extract.resultsTitle": "擷取結果",
269
+ "extract.tabs.fields": "欄位",
270
+ "extract.tabs.json": "JSON",
271
+ "extract.placeholder": "執行擷取後,結果會顯示在這裡。",
272
+ "extract.metadataEmpty": "暫無擷取中繼資料。",
273
+ "extract.failed": "擷取失敗。",
274
+ "fields.title": "欄位",
275
+ "fields.templateName": "範本名稱",
276
+ "fields.templatePlaceholder": "發票",
277
+ "fields.addField": "新增欄位",
278
+ "fields.addTable": "新增表格",
279
+ "fields.field": "欄位",
280
+ "fields.name": "名稱",
281
+ "fields.prompt": "提示詞",
282
+ "fields.mapping": "映射",
283
+ "fields.remove": "移除",
284
+ "fields.column": "欄",
285
+ "fields.removeColumn": "移除欄",
286
+ "fields.table": "表格",
287
+ "fields.tableName": "表格名稱",
288
+ "fields.addColumn": "新增欄",
289
+ "fields.removeTable": "移除表格",
290
+ "fields.namePlaceholder": "標題",
291
+ "fields.promptPlaceholder": "選填擷取提示詞",
292
+ "fields.mappingPlaceholder": "選填映射鍵",
293
+ "fields.tableNamePlaceholder": "表格_1",
294
+ "fields.columnNamePlaceholder": "單價",
295
+ "fields.defaultFieldName": "欄位",
296
+ "fields.initialFieldName": "標題",
297
+ "fields.defaultColumnName": "欄",
298
+ "fields.initialColumnName": "單價",
299
+ "error.selectDocument": "請先選擇一份文件。",
300
+ "error.cloudApiKeyRequired": "雲端模式需要 API 金鑰。",
301
+ "error.localLlmRequired": "請完整填寫本機 LLM 提供商、模型和 Base URL。",
302
+ "error.fieldsRequired": "請至少新增一個欄位或表格欄。",
303
+ },
304
+ };
305
+
306
+ let currentLanguage = "en";
307
+
308
+ function normalizeLanguage(language) {
309
+ if (!language) return null;
310
+ const normalized = String(language).replace("_", "-");
311
+ if (translations[normalized]) return normalized;
312
+ const lower = normalized.toLowerCase();
313
+ if (lower === "zh-cn" || lower === "zh-hans") return "zh-CN";
314
+ if (lower === "zh-tw" || lower === "zh-hk" || lower === "zh-mo" || lower === "zh-hant") return "zh-TW";
315
+ if (lower.startsWith("zh")) return "zh-CN";
316
+ if (lower.startsWith("en")) return "en";
317
+ return null;
318
+ }
319
+
320
+ function browserLanguage() {
321
+ const candidates = [navigator.language, ...(navigator.languages || [])];
322
+ return candidates.map(normalizeLanguage).find(Boolean) || "en";
323
+ }
324
+
325
+ function storedLanguage() {
326
+ try {
327
+ return normalizeLanguage(localStorage.getItem(storageKey));
328
+ } catch {
329
+ return null;
330
+ }
331
+ }
332
+
333
+ function saveLanguage(language) {
334
+ try {
335
+ localStorage.setItem(storageKey, language);
336
+ } catch {
337
+ // Ignore storage errors in private or restricted browser contexts.
338
+ }
339
+ }
340
+
341
+ function interpolate(message, values) {
342
+ return message.replace(/\{(\w+)\}/g, (match, key) => String(values?.[key] ?? match));
343
+ }
344
+
345
+ function setLanguage(language, { persist = false } = {}) {
346
+ currentLanguage = normalizeLanguage(language) || "en";
347
+ const metadata = supportedLanguages.find((entry) => entry.code === currentLanguage) || supportedLanguages[0];
348
+ document.documentElement.lang = metadata.htmlLang;
349
+ if (persist) saveLanguage(currentLanguage);
350
+ }
351
+
352
+ function applyElementTranslation(element, attributeName, key) {
353
+ const value = t(key);
354
+ if (attributeName === "text") {
355
+ element.textContent = value;
356
+ return;
357
+ }
358
+ element.setAttribute(attributeName, value);
359
+ }
360
+
361
+ export function t(key, values = {}) {
362
+ const message = translations[currentLanguage]?.[key] ?? translations.en[key] ?? key;
363
+ return interpolate(message, values);
364
+ }
365
+
366
+ export function getCurrentLanguage() {
367
+ return currentLanguage;
368
+ }
369
+
370
+ export function applyTranslations(root = document) {
371
+ root.querySelectorAll("[data-i18n]").forEach((element) => {
372
+ applyElementTranslation(element, "text", element.dataset.i18n);
373
+ });
374
+ root.querySelectorAll("[data-i18n-placeholder]").forEach((element) => {
375
+ applyElementTranslation(element, "placeholder", element.dataset.i18nPlaceholder);
376
+ });
377
+ root.querySelectorAll("[data-i18n-aria-label]").forEach((element) => {
378
+ applyElementTranslation(element, "aria-label", element.dataset.i18nAriaLabel);
379
+ });
380
+
381
+ const page = document.body?.dataset.page;
382
+ if (page === "parse") document.title = t("parse.pageTitle");
383
+ else if (page === "extract") document.title = t("extract.pageTitle");
384
+ else document.title = t("app.title");
385
+ }
386
+
387
+ export function onLanguageChange(callback) {
388
+ const handler = (event) => callback(event.detail.language);
389
+ window.addEventListener("docslight:languagechange", handler);
390
+ return () => window.removeEventListener("docslight:languagechange", handler);
391
+ }
392
+
393
+ export function initI18n() {
394
+ const languageSelect = document.querySelector("#languageSelect");
395
+ setLanguage(storedLanguage() || browserLanguage());
396
+ if (languageSelect) {
397
+ languageSelect.value = currentLanguage;
398
+ languageSelect.addEventListener("change", () => {
399
+ setLanguage(languageSelect.value, { persist: true });
400
+ applyTranslations();
401
+ window.dispatchEvent(new CustomEvent("docslight:languagechange", { detail: { language: currentLanguage } }));
402
+ });
403
+ }
404
+ applyTranslations();
405
+ }
@@ -0,0 +1,161 @@
1
+ import {
2
+ bindDropzone,
3
+ bindResultTabs,
4
+ downloadBlob,
5
+ downloadText,
6
+ highlightBboxes,
7
+ initHealthBadge,
8
+ loadPreview,
9
+ postForm,
10
+ renderBlocksView,
11
+ renderJsonView,
12
+ renderMarkdownView,
13
+ renderPlaceholder,
14
+ renderPreview,
15
+ normalizeParsePayload,
16
+ setFormError,
17
+ } from "./common.js";
18
+ import { initI18n, onLanguageChange, t } from "./i18n.js";
19
+
20
+ const parseForm = document.querySelector("#parseForm");
21
+ const modeSelect = document.querySelector("#modeSelect");
22
+ const cloudConfig = document.querySelector("#cloudConfig");
23
+ const localParseNote = document.querySelector("#localParseNote");
24
+ const fileInput = document.querySelector("#fileInput");
25
+ const dropZone = document.querySelector("#dropZone");
26
+ const fileName = document.querySelector("#fileName");
27
+ const previewTitle = document.querySelector("#previewTitle");
28
+ const previewCanvas = document.querySelector("#previewCanvas");
29
+ const officePreviewNotice = document.querySelector("#officePreviewNotice");
30
+ const highlightStatus = document.querySelector("#highlightStatus");
31
+ const formError = document.querySelector("#formError");
32
+ const submitButton = document.querySelector("#submitButton");
33
+ const downloadButton = document.querySelector("#downloadButton");
34
+ const metadataPreview = document.querySelector("#metadataPreview");
35
+ const blocksPanel = document.querySelector("#blocksPanel");
36
+ const markdownPanel = document.querySelector("#markdownPanel");
37
+ const jsonPanel = document.querySelector("#jsonPanel");
38
+ const parseResultTabs = document.querySelector("#parseResultTabs");
39
+ const healthStatus = document.querySelector("#healthStatus");
40
+
41
+ const state = {
42
+ currentTab: "blocks",
43
+ hasResult: false,
44
+ latestMarkdown: "",
45
+ latestJson: "",
46
+ previewRequestId: 0,
47
+ };
48
+
49
+ function syncRuntimeControls() {
50
+ const isCloud = modeSelect?.value !== "local";
51
+ if (cloudConfig) cloudConfig.hidden = !isCloud;
52
+ if (localParseNote) localParseNote.hidden = isCloud;
53
+ }
54
+
55
+ function refreshPreview() {
56
+ return loadPreview({
57
+ fileInput,
58
+ previewTitle,
59
+ previewCanvas,
60
+ officePreviewNotice,
61
+ highlightStatus,
62
+ state,
63
+ });
64
+ }
65
+
66
+ function renderEmptyResult() {
67
+ const placeholder = t("parse.placeholder");
68
+ renderPlaceholder(blocksPanel, placeholder);
69
+ renderPlaceholder(markdownPanel, placeholder);
70
+ renderPlaceholder(jsonPanel, placeholder);
71
+ state.hasResult = false;
72
+ if (metadataPreview) metadataPreview.textContent = t("parse.metadataEmpty");
73
+ if (downloadButton) downloadButton.disabled = true;
74
+ }
75
+
76
+ function renderParseResult(result) {
77
+ state.hasResult = true;
78
+ const normalized = normalizeParsePayload(result);
79
+ state.latestMarkdown = normalized.markdown || "";
80
+ state.latestJson = JSON.stringify(result || {}, null, 2);
81
+
82
+ renderBlocksView(result, blocksPanel, {
83
+ onPick: (boxes) => {
84
+ if (!boxes) {
85
+ highlightBboxes(null, "parse", { previewCanvas, highlightStatus });
86
+ return;
87
+ }
88
+ highlightBboxes(boxes, "parse", { previewCanvas, highlightStatus });
89
+ },
90
+ });
91
+ renderMarkdownView(state.latestMarkdown, markdownPanel);
92
+ renderJsonView(result, jsonPanel);
93
+
94
+ if (metadataPreview) {
95
+ metadataPreview.textContent = JSON.stringify(normalized.metadata || {}, null, 2);
96
+ }
97
+ if (downloadButton) downloadButton.disabled = false;
98
+ }
99
+
100
+ function validateForm() {
101
+ if (!fileInput?.files?.length) return t("error.selectDocument");
102
+ const isCloud = modeSelect?.value !== "local";
103
+ const apiKey = parseForm?.querySelector('[name="api_key"]')?.value?.trim();
104
+ if (isCloud && !apiKey) return t("error.cloudApiKeyRequired");
105
+ return "";
106
+ }
107
+
108
+ function refreshLocalizedDynamicCopy() {
109
+ if (!state.hasResult) renderEmptyResult();
110
+ renderPreview(state.preview || null, { previewTitle, previewCanvas, officePreviewNotice, highlightStatus, state });
111
+ setFormError(formError, formError?.hidden ? "" : formError?.textContent || "");
112
+ }
113
+
114
+ modeSelect?.addEventListener("change", syncRuntimeControls);
115
+
116
+ parseForm?.addEventListener("submit", async (event) => {
117
+ event.preventDefault();
118
+ setFormError(formError, "");
119
+
120
+ const validationError = validateForm();
121
+ if (validationError) {
122
+ setFormError(formError, validationError);
123
+ return;
124
+ }
125
+
126
+ const body = new FormData(parseForm);
127
+ body.set("file", fileInput.files[0]);
128
+ if (submitButton) submitButton.disabled = true;
129
+
130
+ try {
131
+ const payload = await postForm("/api/parse", body);
132
+ if (payload.blob) {
133
+ downloadBlob(payload.blob, payload.filename || "docslight-parse.zip");
134
+ return;
135
+ }
136
+ renderParseResult(payload.result || payload);
137
+ } catch (error) {
138
+ setFormError(formError, error instanceof Error ? error.message : t("parse.failed"));
139
+ } finally {
140
+ if (submitButton) submitButton.disabled = false;
141
+ }
142
+ });
143
+
144
+ downloadButton?.addEventListener("click", () => {
145
+ if (state.currentTab === "json") {
146
+ downloadText(state.latestJson, "docslight-parse.json");
147
+ return;
148
+ }
149
+ downloadText(state.latestMarkdown, "docslight-parse.md");
150
+ });
151
+
152
+ initI18n();
153
+ initHealthBadge(healthStatus);
154
+ bindDropzone({ dropZone, fileInput, fileName, onFileChange: refreshPreview });
155
+ bindResultTabs(parseResultTabs, (tab) => {
156
+ state.currentTab = tab;
157
+ });
158
+ onLanguageChange(refreshLocalizedDynamicCopy);
159
+ syncRuntimeControls();
160
+ renderPreview(null, { previewTitle, previewCanvas, officePreviewNotice, highlightStatus, state });
161
+ renderEmptyResult();