@lynn123411/dsh-chat-translate 1.1.0

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.
package/lib/client.js ADDED
@@ -0,0 +1,1565 @@
1
+ window.__ModuleLoader__.load({ id: "@lynn123411/dsh-chat-translate", factory: (require) => { var module = { exports: {} }; var exports = module.exports;
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name2 in all)
9
+ __defProp(target, name2, { get: all[name2], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/client/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ NonDestructiveTranslationMount: () => NonDestructiveTranslationMount,
25
+ StreamDebounceViewportObserver: () => StreamDebounceViewportObserver,
26
+ apply: () => apply,
27
+ chatTranslateObserver: () => chatTranslateObserver,
28
+ clientCache: () => clientCache,
29
+ inject: () => inject,
30
+ lazyQueue: () => lazyQueue,
31
+ name: () => name,
32
+ settingsStore: () => settingsStore,
33
+ setupSettingsUi: () => setupSettingsUi
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/client/translate/client-cache.ts
38
+ var CACHE_KEY = "dsh-chat-translate:cache";
39
+ var MAX_LOCAL_ENTRIES = 500;
40
+ var TTL_MS = 7 * 24 * 60 * 60 * 1e3;
41
+ var ClientCache = class {
42
+ // Map preserves insertion order in JS, enabling true LRU semantics.
43
+ memCache = /* @__PURE__ */ new Map();
44
+ dirty = false;
45
+ saveTimer = null;
46
+ constructor() {
47
+ this.load();
48
+ }
49
+ load() {
50
+ if (typeof localStorage === "undefined") return;
51
+ try {
52
+ const raw = localStorage.getItem(CACHE_KEY);
53
+ if (raw) {
54
+ const obj = JSON.parse(raw);
55
+ if (obj && typeof obj === "object") {
56
+ for (const [k, entry] of Object.entries(obj)) {
57
+ if (typeof entry === "string") {
58
+ this.memCache.set(k, { t: 0, v: entry });
59
+ } else if (entry && typeof entry === "object" && typeof entry.v === "string") {
60
+ this.memCache.set(k, entry);
61
+ }
62
+ }
63
+ }
64
+ }
65
+ } catch {
66
+ }
67
+ }
68
+ get(text) {
69
+ const key = text.trim().toLowerCase();
70
+ const entry = this.memCache.get(key);
71
+ if (entry === void 0) return void 0;
72
+ if (entry.v && entry.v.trim().toLowerCase() === key) {
73
+ this.memCache.delete(key);
74
+ this.dirty = true;
75
+ this.scheduleSave();
76
+ return void 0;
77
+ }
78
+ if (entry.t > 0 && Date.now() - entry.t > TTL_MS) {
79
+ this.memCache.delete(key);
80
+ this.dirty = true;
81
+ this.scheduleSave();
82
+ return void 0;
83
+ }
84
+ this.memCache.delete(key);
85
+ this.memCache.set(key, entry);
86
+ return entry.v;
87
+ }
88
+ set(text, translated) {
89
+ const key = text.trim().toLowerCase();
90
+ if (this.memCache.has(key)) {
91
+ this.memCache.delete(key);
92
+ } else if (this.memCache.size >= MAX_LOCAL_ENTRIES) {
93
+ const oldestKey = this.memCache.keys().next().value;
94
+ if (oldestKey !== void 0) {
95
+ this.memCache.delete(oldestKey);
96
+ }
97
+ }
98
+ this.memCache.set(key, { t: Date.now(), v: translated });
99
+ this.dirty = true;
100
+ this.scheduleSave();
101
+ }
102
+ scheduleSave() {
103
+ if (this.saveTimer !== null || typeof window === "undefined") return;
104
+ this.saveTimer = window.setTimeout(() => {
105
+ this.saveTimer = null;
106
+ this.flushSync();
107
+ }, 2e3);
108
+ }
109
+ flushSync() {
110
+ if (!this.dirty || typeof localStorage === "undefined") return;
111
+ this.dirty = false;
112
+ try {
113
+ const obj = {};
114
+ for (const [k, v] of this.memCache.entries()) {
115
+ obj[k] = v;
116
+ }
117
+ localStorage.setItem(CACHE_KEY, JSON.stringify(obj));
118
+ } catch {
119
+ }
120
+ }
121
+ clear() {
122
+ this.memCache.clear();
123
+ this.dirty = true;
124
+ this.flushSync();
125
+ }
126
+ size() {
127
+ return this.memCache.size;
128
+ }
129
+ };
130
+ var clientCache = new ClientCache();
131
+
132
+ // src/client/translate/api.ts
133
+ async function requestTranslateBatch(texts, options = {}) {
134
+ if (!Array.isArray(texts) || texts.length === 0) return [];
135
+ const validTexts = texts.map((t) => typeof t === "string" ? t : "");
136
+ if (validTexts.length === 0) return [];
137
+ const controller = new AbortController();
138
+ const timeoutId = options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : null;
139
+ const effectiveSignal = options.signal ? options.signal.aborted ? options.signal : controller.signal : controller.signal;
140
+ if (options.signal) {
141
+ options.signal.addEventListener("abort", () => controller.abort(), { once: true });
142
+ }
143
+ try {
144
+ const res = await fetch("/api/dsh-chat-translate/translate", {
145
+ method: "POST",
146
+ headers: {
147
+ "Content-Type": "application/json"
148
+ },
149
+ body: JSON.stringify({ texts: validTexts }),
150
+ signal: effectiveSignal
151
+ });
152
+ if (res.ok) {
153
+ const data = await res.json();
154
+ if (data.ok && Array.isArray(data.results) && data.results.length > 0) {
155
+ return data.results;
156
+ } else if (data.error) {
157
+ console.warn(`[dsh-chat-translate] \u7FFB\u8BD1\u63A5\u53E3\u9519\u8BEF: ${data.error}`);
158
+ }
159
+ } else {
160
+ console.warn(`[dsh-chat-translate] \u7FFB\u8BD1\u8BF7\u6C42\u5931\u8D25 (HTTP ${res.status})`);
161
+ }
162
+ } catch (err) {
163
+ if (err?.name === "AbortError") {
164
+ }
165
+ } finally {
166
+ if (timeoutId !== null) {
167
+ clearTimeout(timeoutId);
168
+ }
169
+ }
170
+ return validTexts.map((t) => ({
171
+ original: t,
172
+ translated: t,
173
+ channel: "fallback-client",
174
+ cached: false
175
+ }));
176
+ }
177
+ async function fetchServerConfig() {
178
+ try {
179
+ const res = await fetch("/api/dsh-chat-translate/config");
180
+ if (!res.ok) return null;
181
+ const json = await res.json();
182
+ return json.config;
183
+ } catch {
184
+ return null;
185
+ }
186
+ }
187
+ async function updateServerConfig(updates) {
188
+ try {
189
+ const res = await fetch("/api/dsh-chat-translate/config", {
190
+ method: "POST",
191
+ headers: { "Content-Type": "application/json" },
192
+ body: JSON.stringify(updates)
193
+ });
194
+ if (!res.ok) return null;
195
+ const json = await res.json();
196
+ return json.config;
197
+ } catch {
198
+ return null;
199
+ }
200
+ }
201
+ async function testServerChannel(channel) {
202
+ try {
203
+ const res = await fetch("/api/dsh-chat-translate/test-channel", {
204
+ method: "POST",
205
+ headers: { "Content-Type": "application/json" },
206
+ body: JSON.stringify({ channel })
207
+ });
208
+ return await res.json();
209
+ } catch (err) {
210
+ return { ok: false, latencyMs: 0, error: err?.message || String(err) };
211
+ }
212
+ }
213
+ async function saveCredentials(apiKey) {
214
+ try {
215
+ const res = await fetch("/api/dsh-chat-translate/credentials", {
216
+ method: "POST",
217
+ headers: { "Content-Type": "application/json" },
218
+ body: JSON.stringify({ apiKey })
219
+ });
220
+ return await res.json();
221
+ } catch (err) {
222
+ return { ok: false, error: err?.message || String(err) };
223
+ }
224
+ }
225
+
226
+ // src/client/translate/mount.ts
227
+ var CLASS_ORIGINAL_HIDDEN = "dsh-tidy-original-hidden";
228
+ var CLASS_ORIGINAL_SHOWN = "dsh-tidy-original-shown";
229
+ var CLASS_TRANSLATED_BLOCK = "dsh-tidy-translated-block";
230
+ var NonDestructiveTranslationMount = class {
231
+ /**
232
+ * Mounts a translated string onto the target element non-destructively.
233
+ */
234
+ static mount(element, translated, options = {}) {
235
+ if (!element || !element.ownerDocument) return;
236
+ const doc = element.ownerDocument;
237
+ let transWrapper = element.querySelector(`:scope > .${CLASS_TRANSLATED_BLOCK}`);
238
+ let origWrapper = element.querySelector(
239
+ `:scope > .${CLASS_ORIGINAL_HIDDEN}, :scope > .${CLASS_ORIGINAL_SHOWN}`
240
+ );
241
+ if (transWrapper && origWrapper) {
242
+ transWrapper.textContent = translated;
243
+ element.dataset.tidyTranslated = "true";
244
+ if (options.isThink) element.dataset.tidyThink = "true";
245
+ if (options.originalText) element.dataset.original = options.originalText;
246
+ return;
247
+ }
248
+ const originalText = options.originalText ?? this.extractVisibleText(element);
249
+ origWrapper = doc.createElement("span");
250
+ origWrapper.className = CLASS_ORIGINAL_HIDDEN;
251
+ origWrapper.style.display = "none";
252
+ while (element.firstChild) {
253
+ origWrapper.appendChild(element.firstChild);
254
+ }
255
+ transWrapper = doc.createElement("span");
256
+ transWrapper.className = CLASS_TRANSLATED_BLOCK;
257
+ transWrapper.textContent = translated;
258
+ const interactive = options.interactive !== false;
259
+ if (interactive) {
260
+ const showOriginal = (e) => {
261
+ e.stopPropagation();
262
+ if (!origWrapper || !transWrapper) return;
263
+ origWrapper.style.display = "inline";
264
+ origWrapper.className = CLASS_ORIGINAL_SHOWN;
265
+ transWrapper.style.display = "none";
266
+ };
267
+ const showTranslated = (e) => {
268
+ e.stopPropagation();
269
+ if (!origWrapper || !transWrapper) return;
270
+ origWrapper.style.display = "none";
271
+ origWrapper.className = CLASS_ORIGINAL_HIDDEN;
272
+ transWrapper.style.display = "inline";
273
+ };
274
+ transWrapper.addEventListener("click", showOriginal);
275
+ origWrapper.addEventListener("click", showTranslated);
276
+ }
277
+ element.appendChild(transWrapper);
278
+ element.appendChild(origWrapper);
279
+ element.dataset.tidyTranslated = "true";
280
+ element.dataset.original = originalText;
281
+ if (options.isThink) {
282
+ element.dataset.tidyThink = "true";
283
+ }
284
+ }
285
+ /**
286
+ * Unmounts translation and restores original DOM nodes completely.
287
+ */
288
+ static unmount(element) {
289
+ if (!element) return;
290
+ const origWrapper = element.querySelector(
291
+ `:scope > .${CLASS_ORIGINAL_HIDDEN}, :scope > .${CLASS_ORIGINAL_SHOWN}`
292
+ );
293
+ const transWrapper = element.querySelector(`:scope > .${CLASS_TRANSLATED_BLOCK}`);
294
+ if (origWrapper) {
295
+ while (origWrapper.firstChild) {
296
+ element.insertBefore(origWrapper.firstChild, origWrapper);
297
+ }
298
+ origWrapper.remove();
299
+ }
300
+ if (transWrapper) {
301
+ transWrapper.remove();
302
+ }
303
+ if (!origWrapper && element.dataset.original) {
304
+ element.textContent = element.dataset.original;
305
+ }
306
+ delete element.dataset.tidyTranslated;
307
+ delete element.dataset.original;
308
+ delete element.dataset.tidyThink;
309
+ }
310
+ /**
311
+ * Checks if an element has non-destructive translation mounted.
312
+ */
313
+ static isMounted(element) {
314
+ return element.dataset.tidyTranslated === "true" && !!element.querySelector(`:scope > .${CLASS_TRANSLATED_BLOCK}`);
315
+ }
316
+ /**
317
+ * Gets the original text recorded on the element or contained in origWrapper.
318
+ */
319
+ static getOriginal(element) {
320
+ if (element.dataset.original) return element.dataset.original;
321
+ const origWrapper = element.querySelector(
322
+ `:scope > .${CLASS_ORIGINAL_HIDDEN}, :scope > .${CLASS_ORIGINAL_SHOWN}`
323
+ );
324
+ return origWrapper ? origWrapper.textContent?.trim() : void 0;
325
+ }
326
+ /**
327
+ * Extracts text content excluding our own translation wrappers.
328
+ */
329
+ static extractVisibleText(element) {
330
+ const origWrapper = element.querySelector(
331
+ `:scope > .${CLASS_ORIGINAL_HIDDEN}, :scope > .${CLASS_ORIGINAL_SHOWN}`
332
+ );
333
+ if (origWrapper) {
334
+ return origWrapper.textContent?.trim() || "";
335
+ }
336
+ const transWrapper = element.querySelector(`:scope > .${CLASS_TRANSLATED_BLOCK}`);
337
+ if (transWrapper) {
338
+ return transWrapper.textContent?.trim() || "";
339
+ }
340
+ return element.textContent?.trim() || "";
341
+ }
342
+ };
343
+
344
+ // src/client/translate/viewport-observer.ts
345
+ var StreamDebounceViewportObserver = class {
346
+ intersectionObserver = null;
347
+ streamingTimers = /* @__PURE__ */ new WeakMap();
348
+ pendingQueue = [];
349
+ batchFlushTimer = null;
350
+ options;
351
+ constructor(options) {
352
+ this.options = {
353
+ rootMargin: options.rootMargin ?? "150px 0px",
354
+ debounceMs: options.debounceMs ?? 400,
355
+ onVisibleBatch: options.onVisibleBatch
356
+ };
357
+ this.initIntersectionObserver();
358
+ }
359
+ initIntersectionObserver() {
360
+ if (typeof window === "undefined" || typeof IntersectionObserver === "undefined") {
361
+ return;
362
+ }
363
+ this.intersectionObserver = new IntersectionObserver(
364
+ (entries) => {
365
+ for (const entry of entries) {
366
+ if (entry.isIntersecting && entry.target instanceof HTMLElement) {
367
+ const el = entry.target;
368
+ this.intersectionObserver?.unobserve(el);
369
+ const text = el.dataset.tidyPendingText || el.textContent?.trim() || "";
370
+ const isThink = el.dataset.tidyPendingThink === "true";
371
+ if (text) {
372
+ delete el.dataset.tidyPendingText;
373
+ delete el.dataset.tidyPendingThink;
374
+ this.enqueueBatch(el, text, isThink);
375
+ }
376
+ }
377
+ }
378
+ },
379
+ {
380
+ root: null,
381
+ // viewport
382
+ rootMargin: this.options.rootMargin,
383
+ threshold: 0
384
+ }
385
+ );
386
+ }
387
+ /**
388
+ * Observe an element with streaming debounce.
389
+ * If streaming updates characterData repeatedly within debounceMs, the timer resets.
390
+ */
391
+ observeWithDebounce(element, text, immediate = false, isThink = false) {
392
+ if (!element || !text) return;
393
+ const existingTimer = this.streamingTimers.get(element);
394
+ if (existingTimer !== void 0) {
395
+ clearTimeout(existingTimer);
396
+ this.streamingTimers.delete(element);
397
+ }
398
+ if (immediate || this.options.debounceMs <= 0) {
399
+ this.registerForViewport(element, text, isThink);
400
+ return;
401
+ }
402
+ const timer = window.setTimeout(() => {
403
+ this.streamingTimers.delete(element);
404
+ if (element.isConnected) {
405
+ const latestText = element.textContent?.trim() || text;
406
+ this.registerForViewport(element, latestText, isThink);
407
+ }
408
+ }, this.options.debounceMs);
409
+ this.streamingTimers.set(element, timer);
410
+ }
411
+ registerForViewport(element, text, isThink = false) {
412
+ if (!element.isConnected) return;
413
+ if (!this.intersectionObserver) {
414
+ this.enqueueBatch(element, text, isThink);
415
+ return;
416
+ }
417
+ element.dataset.tidyPendingText = text;
418
+ if (isThink) element.dataset.tidyPendingThink = "true";
419
+ this.intersectionObserver.observe(element);
420
+ }
421
+ enqueueBatch(element, text, isThink = false) {
422
+ this.pendingQueue.push({ element, text, isThink });
423
+ if (this.batchFlushTimer === null && typeof window !== "undefined") {
424
+ this.batchFlushTimer = window.setTimeout(() => {
425
+ this.batchFlushTimer = null;
426
+ this.flushQueue();
427
+ }, 50);
428
+ }
429
+ }
430
+ flushQueue() {
431
+ if (this.pendingQueue.length === 0) return;
432
+ const batch = [...this.pendingQueue];
433
+ this.pendingQueue = [];
434
+ this.options.onVisibleBatch(batch);
435
+ }
436
+ unobserve(element) {
437
+ const timer = this.streamingTimers.get(element);
438
+ if (timer !== void 0) {
439
+ clearTimeout(timer);
440
+ this.streamingTimers.delete(element);
441
+ }
442
+ delete element.dataset.tidyPendingText;
443
+ delete element.dataset.tidyPendingThink;
444
+ if (this.intersectionObserver) {
445
+ this.intersectionObserver.unobserve(element);
446
+ }
447
+ }
448
+ disconnect() {
449
+ if (this.batchFlushTimer !== null) {
450
+ clearTimeout(this.batchFlushTimer);
451
+ this.batchFlushTimer = null;
452
+ }
453
+ this.pendingQueue = [];
454
+ if (this.intersectionObserver) {
455
+ this.intersectionObserver.disconnect();
456
+ this.initIntersectionObserver();
457
+ }
458
+ }
459
+ };
460
+
461
+ // src/client/translate/lazy.ts
462
+ var LazyTranslationQueue = class {
463
+ enabled = true;
464
+ viewportObserver;
465
+ constructor() {
466
+ this.viewportObserver = new StreamDebounceViewportObserver({
467
+ rootMargin: "150px 0px",
468
+ debounceMs: 400,
469
+ onVisibleBatch: (items) => this.handleVisibleBatch(items)
470
+ });
471
+ }
472
+ setEnabled(enabled) {
473
+ this.enabled = enabled;
474
+ if (!enabled) {
475
+ this.viewportObserver.disconnect();
476
+ }
477
+ }
478
+ observe(element, text, immediate = false, isThink = false) {
479
+ if (!this.enabled || !element.isConnected) return;
480
+ const cached = clientCache.get(text);
481
+ if (cached) {
482
+ this.applyTranslation(element, cached, text, isThink);
483
+ return;
484
+ }
485
+ this.viewportObserver.observeWithDebounce(element, text, immediate, isThink);
486
+ }
487
+ async handleVisibleBatch(items) {
488
+ if (!this.enabled || items.length === 0) return;
489
+ const sorted = [...items].sort((a, b) => {
490
+ if (a.element === b.element) return 0;
491
+ const pos = a.element.compareDocumentPosition(b.element);
492
+ return pos & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
493
+ });
494
+ const textMap = /* @__PURE__ */ new Map();
495
+ for (const item of sorted) {
496
+ if (!item.element.isConnected) continue;
497
+ const cached = clientCache.get(item.text);
498
+ if (cached) {
499
+ this.applyTranslation(item.element, cached, item.text, item.isThink);
500
+ continue;
501
+ }
502
+ const list = textMap.get(item.text) || [];
503
+ list.push({ element: item.element, isThink: item.isThink });
504
+ textMap.set(item.text, list);
505
+ }
506
+ const uniqueTexts = Array.from(textMap.keys());
507
+ if (uniqueTexts.length === 0) return;
508
+ const results = await requestTranslateBatch(uniqueTexts);
509
+ for (const res of results) {
510
+ if (res.translated && res.translated.trim() && res.channel !== "fallback" && res.channel !== "fallback-client" && res.translated.trim() !== res.original.trim()) {
511
+ clientCache.set(res.original, res.translated);
512
+ const entries = textMap.get(res.original) || [];
513
+ for (const entry of entries) {
514
+ if (entry.element.isConnected && this.enabled) {
515
+ this.applyTranslation(entry.element, res.translated, res.original, entry.isThink);
516
+ }
517
+ }
518
+ } else if (res.channel === "fallback" || res.channel === "fallback-client") {
519
+ console.debug(`[dsh-chat-translate] \u7FFB\u8BD1\u672A\u6210\u529F (\u964D\u7EA7\u4FDD\u7559\u539F\u6587): "${res.original.slice(0, 40)}"`);
520
+ }
521
+ }
522
+ }
523
+ applyTranslation(element, translated, original, isThink) {
524
+ if (!element.isConnected || !this.enabled) return;
525
+ NonDestructiveTranslationMount.mount(element, translated, {
526
+ originalText: original,
527
+ isThink: isThink || element.dataset.tidyThink === "true"
528
+ });
529
+ }
530
+ disconnect() {
531
+ this.viewportObserver.disconnect();
532
+ }
533
+ };
534
+ var lazyQueue = new LazyTranslationQueue();
535
+
536
+ // src/client/translate/observer.ts
537
+ var TOOL_TITLE_SELECTOR = '[class*="summary"]';
538
+ var SESSION_ROOT_SELECTOR = "[data-conversation-scroll], [data-chat-flow]";
539
+ var ROOT_CHECK_INTERVAL_MS = 3e3;
540
+ function isToolSummarySpan(span) {
541
+ if (!span || span.nodeType !== 1) return false;
542
+ if (span.hasAttribute("aria-hidden")) return false;
543
+ if (span.querySelector?.('[class*="title"], [class*="leading"], [class*="chevron"], [class*="sep"], [class*="summary"]')) {
544
+ return false;
545
+ }
546
+ const cls = span.className || "";
547
+ if (/title|leading|icon|badge|chevron|separator|sep\b|row\b|root\b|card\b/i.test(cls)) return false;
548
+ const rawToggle = (span.textContent || "").trim();
549
+ if (rawToggle.length <= 12 && /^(展开|收起|展开全部|收起全部|Expand|Collapse|Show more|Show less|Think|思考)$/i.test(rawToggle)) {
550
+ return false;
551
+ }
552
+ if (rawToggle === "Think" || rawToggle === "\u601D\u8003") return false;
553
+ if (span.closest('button, [role="button"]') && rawToggle.length <= 12 && /展开|收起|Expand|Collapse|Think|思考/i.test(rawToggle)) {
554
+ return false;
555
+ }
556
+ if (span.closest(
557
+ '[data-chat-call-id], [data-slot="tool.call.toolview"], [data-sample], [data-variant], [data-tool], [data-disclosure-row]'
558
+ )) {
559
+ return true;
560
+ }
561
+ return false;
562
+ }
563
+ var ChatTranslateObserver = class {
564
+ observer = null;
565
+ rootElement = null;
566
+ rootCheckTimer = null;
567
+ isEnabled = true;
568
+ constructor() {
569
+ this.handleMutations = this.handleMutations.bind(this);
570
+ }
571
+ setEnabled(enabled) {
572
+ this.isEnabled = enabled;
573
+ if (enabled) {
574
+ lazyQueue.setEnabled(true);
575
+ this.start();
576
+ } else {
577
+ this.restoreOriginals();
578
+ this.disconnect();
579
+ lazyQueue.setEnabled(false);
580
+ }
581
+ }
582
+ restoreOriginals() {
583
+ const scope = this.rootElement ?? document;
584
+ const spans = scope.querySelectorAll('[data-tidy-translated="true"]');
585
+ for (const span of spans) {
586
+ NonDestructiveTranslationMount.unmount(span);
587
+ }
588
+ }
589
+ /**
590
+ * The active session's scroll container. Only the currently-viewed session
591
+ * is translated; switching sessions replaces this subtree and the observer
592
+ * naturally follows the new content.
593
+ */
594
+ isVisible(el) {
595
+ if (el.hasAttribute("hidden")) return false;
596
+ if (el.style.display === "none") return false;
597
+ try {
598
+ return el.getClientRects().length > 0;
599
+ } catch {
600
+ return true;
601
+ }
602
+ }
603
+ findRoot(documentRef) {
604
+ if (!documentRef.body) {
605
+ return documentRef.documentElement;
606
+ }
607
+ const candidates = documentRef.querySelectorAll(SESSION_ROOT_SELECTOR);
608
+ for (const el of candidates) {
609
+ if (this.isVisible(el)) {
610
+ return el;
611
+ }
612
+ }
613
+ return documentRef.body;
614
+ }
615
+ start(documentRef = document) {
616
+ if (!this.isEnabled || typeof window === "undefined") {
617
+ return () => {
618
+ };
619
+ }
620
+ const findAndObserveRoot = () => {
621
+ const root = this.findRoot(documentRef);
622
+ this.rootElement = root;
623
+ this.scanContainer(root);
624
+ if (!this.observer) {
625
+ this.observer = new MutationObserver(this.handleMutations);
626
+ this.observer.observe(root, {
627
+ childList: true,
628
+ subtree: true,
629
+ attributes: true,
630
+ characterData: true,
631
+ attributeFilter: ["data-state", "data-tool", "data-variant", "data-sample", "aria-expanded"]
632
+ });
633
+ }
634
+ };
635
+ findAndObserveRoot();
636
+ this.scheduleRootCheck();
637
+ return () => {
638
+ this.disconnect();
639
+ };
640
+ }
641
+ scheduleRootCheck() {
642
+ if (this.rootCheckTimer !== null || typeof window === "undefined") return;
643
+ this.rootCheckTimer = window.setInterval(() => {
644
+ if (this.rootElement && this.rootElement.isConnected && this.isVisible(this.rootElement)) {
645
+ return;
646
+ }
647
+ this.restart();
648
+ }, ROOT_CHECK_INTERVAL_MS);
649
+ }
650
+ restart() {
651
+ if (typeof window === "undefined") return;
652
+ const wasEnabled = this.isEnabled;
653
+ this.disconnect();
654
+ if (wasEnabled) {
655
+ this.start();
656
+ }
657
+ }
658
+ handleMutations(mutations) {
659
+ if (!this.isEnabled) return;
660
+ for (const mutation of mutations) {
661
+ if (mutation.type === "childList") {
662
+ for (let i = 0; i < mutation.addedNodes.length; i++) {
663
+ const node = mutation.addedNodes[i];
664
+ if (node instanceof HTMLElement) {
665
+ if (node.classList?.contains("dsh-tidy-translated-block") || node.classList?.contains("dsh-tidy-original-hidden") || node.classList?.contains("dsh-tidy-original-shown")) {
666
+ continue;
667
+ }
668
+ this.scanNode(node);
669
+ }
670
+ }
671
+ } else if (mutation.type === "attributes") {
672
+ const target = mutation.target;
673
+ if (target instanceof HTMLElement) {
674
+ if (target.classList?.contains("dsh-tidy-translated-block") || target.classList?.contains("dsh-tidy-original-hidden") || target.classList?.contains("dsh-tidy-original-shown")) {
675
+ continue;
676
+ }
677
+ this.scanNode(target);
678
+ }
679
+ } else if (mutation.type === "characterData") {
680
+ const parent = mutation.target.parentElement;
681
+ if (parent instanceof HTMLElement) {
682
+ if (parent.classList?.contains("dsh-tidy-translated-block") || parent.classList?.contains("dsh-tidy-original-hidden") || parent.classList?.contains("dsh-tidy-original-shown")) {
683
+ continue;
684
+ }
685
+ this.scanNode(parent);
686
+ }
687
+ }
688
+ }
689
+ }
690
+ scanContainer(container) {
691
+ const spans = container.querySelectorAll(TOOL_TITLE_SELECTOR);
692
+ spans.forEach((span) => {
693
+ if (isToolSummarySpan(span)) {
694
+ this.processSpan(span);
695
+ }
696
+ });
697
+ }
698
+ scanNode(node) {
699
+ if (node.matches?.(TOOL_TITLE_SELECTOR) && isToolSummarySpan(node)) {
700
+ this.processSpan(node);
701
+ }
702
+ const spans = node.querySelectorAll(TOOL_TITLE_SELECTOR);
703
+ spans.forEach((span) => {
704
+ if (isToolSummarySpan(span)) {
705
+ this.processSpan(span);
706
+ }
707
+ });
708
+ }
709
+ processSpan(span) {
710
+ if (NonDestructiveTranslationMount.isMounted(span)) {
711
+ const original = NonDestructiveTranslationMount.getOriginal(span);
712
+ if (original) {
713
+ const cached2 = clientCache.get(original);
714
+ if (cached2) return;
715
+ }
716
+ return;
717
+ }
718
+ const text = NonDestructiveTranslationMount.extractVisibleText(span);
719
+ if (!text) return;
720
+ const cached = clientCache.get(text);
721
+ if (cached) {
722
+ NonDestructiveTranslationMount.mount(span, cached, {
723
+ originalText: text
724
+ });
725
+ return;
726
+ }
727
+ lazyQueue.observe(span, text);
728
+ }
729
+ disconnect() {
730
+ if (this.rootCheckTimer !== null) {
731
+ clearInterval(this.rootCheckTimer);
732
+ this.rootCheckTimer = null;
733
+ }
734
+ if (this.observer) {
735
+ this.observer.disconnect();
736
+ this.observer = null;
737
+ }
738
+ lazyQueue.disconnect();
739
+ this.rootElement = null;
740
+ }
741
+ };
742
+ var chatTranslateObserver = new ChatTranslateObserver();
743
+
744
+ // src/client/settings/ui.tsx
745
+ var import_react = require("react");
746
+
747
+ // src/client/settings/store.ts
748
+ var LS_PREFIX = "dsh-chat-translate:";
749
+ var LS_ENABLED = `${LS_PREFIX}enabled`;
750
+ var LS_CONCURRENCY = `${LS_PREFIX}concurrency`;
751
+ var LS_AI_ENABLED = `${LS_PREFIX}aiEnabled`;
752
+ var LS_BING_ENABLED = `${LS_PREFIX}bingEnabled`;
753
+ var LS_BASE_URL = `${LS_PREFIX}baseUrl`;
754
+ var LS_MODEL = `${LS_PREFIX}model`;
755
+ var DEFAULT_STATE = {
756
+ enabled: true,
757
+ concurrency: 3,
758
+ aiEnabled: true,
759
+ bingEnabled: true,
760
+ baseUrl: "",
761
+ model: "",
762
+ aiConfigured: false
763
+ };
764
+ var SettingsStore = class {
765
+ state = { ...DEFAULT_STATE };
766
+ listeners = /* @__PURE__ */ new Set();
767
+ storageListener = null;
768
+ pushTimer = null;
769
+ pushSeq = 0;
770
+ userTouched = false;
771
+ constructor() {
772
+ this.loadFromLocalStorage();
773
+ this.initStorageListener();
774
+ this.syncFromServer();
775
+ }
776
+ loadFromLocalStorage() {
777
+ if (typeof localStorage === "undefined") return;
778
+ try {
779
+ const enabledRaw = localStorage.getItem(LS_ENABLED);
780
+ if (enabledRaw !== null) {
781
+ this.state.enabled = enabledRaw === "true";
782
+ }
783
+ const concurrencyRaw = localStorage.getItem(LS_CONCURRENCY);
784
+ if (concurrencyRaw !== null) {
785
+ const c = parseInt(concurrencyRaw, 10);
786
+ if (Number.isFinite(c) && c >= 1 && c <= 100) {
787
+ this.state.concurrency = c;
788
+ }
789
+ }
790
+ const aiRaw = localStorage.getItem(LS_AI_ENABLED);
791
+ if (aiRaw !== null) {
792
+ this.state.aiEnabled = aiRaw === "true";
793
+ }
794
+ const bingRaw = localStorage.getItem(LS_BING_ENABLED);
795
+ if (bingRaw !== null) {
796
+ this.state.bingEnabled = bingRaw === "true";
797
+ }
798
+ const baseUrlRaw = localStorage.getItem(LS_BASE_URL);
799
+ if (baseUrlRaw !== null) {
800
+ this.state.baseUrl = baseUrlRaw;
801
+ }
802
+ const modelRaw = localStorage.getItem(LS_MODEL);
803
+ if (modelRaw !== null) {
804
+ this.state.model = modelRaw;
805
+ }
806
+ } catch {
807
+ }
808
+ try {
809
+ chatTranslateObserver.setEnabled(this.state.enabled);
810
+ } catch {
811
+ }
812
+ }
813
+ initStorageListener() {
814
+ if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
815
+ this.storageListener = (e) => {
816
+ if (!e.key || !e.key.startsWith(LS_PREFIX)) return;
817
+ let changed = false;
818
+ if (e.key === LS_ENABLED && e.newValue !== null) {
819
+ const val = e.newValue === "true";
820
+ if (this.state.enabled !== val) {
821
+ this.state.enabled = val;
822
+ chatTranslateObserver.setEnabled(val);
823
+ changed = true;
824
+ }
825
+ } else if (e.key === LS_CONCURRENCY && e.newValue !== null) {
826
+ const c = parseInt(e.newValue, 10);
827
+ if (Number.isFinite(c) && c >= 1 && c <= 100 && this.state.concurrency !== c) {
828
+ this.state.concurrency = c;
829
+ changed = true;
830
+ }
831
+ } else if (e.key === LS_AI_ENABLED && e.newValue !== null) {
832
+ const val = e.newValue === "true";
833
+ if (this.state.aiEnabled !== val) {
834
+ this.state.aiEnabled = val;
835
+ changed = true;
836
+ }
837
+ } else if (e.key === LS_BING_ENABLED && e.newValue !== null) {
838
+ const val = e.newValue === "true";
839
+ if (this.state.bingEnabled !== val) {
840
+ this.state.bingEnabled = val;
841
+ changed = true;
842
+ }
843
+ } else if (e.key === LS_BASE_URL && e.newValue !== null && this.state.baseUrl !== e.newValue) {
844
+ this.state.baseUrl = e.newValue;
845
+ changed = true;
846
+ } else if (e.key === LS_MODEL && e.newValue !== null && this.state.model !== e.newValue) {
847
+ this.state.model = e.newValue;
848
+ changed = true;
849
+ }
850
+ if (changed) {
851
+ this.notify();
852
+ this.schedulePush();
853
+ }
854
+ };
855
+ window.addEventListener("storage", this.storageListener);
856
+ }
857
+ async syncFromServer() {
858
+ try {
859
+ const config = await fetchServerConfig();
860
+ if (config) {
861
+ const touched = this.userTouched;
862
+ this.state = {
863
+ enabled: !touched && typeof config.enabled === "boolean" ? config.enabled : this.state.enabled,
864
+ concurrency: !touched && typeof config.concurrency === "number" && Number.isFinite(config.concurrency) ? config.concurrency : this.state.concurrency,
865
+ aiEnabled: !touched && typeof config.aiEnabled === "boolean" ? config.aiEnabled : this.state.aiEnabled,
866
+ bingEnabled: !touched && typeof config.bingEnabled === "boolean" ? config.bingEnabled : this.state.bingEnabled,
867
+ baseUrl: !touched && typeof config.baseUrl === "string" ? config.baseUrl : this.state.baseUrl,
868
+ model: !touched && typeof config.model === "string" ? config.model : this.state.model,
869
+ aiConfigured: typeof config.aiConfigured === "boolean" ? config.aiConfigured : this.state.aiConfigured
870
+ };
871
+ chatTranslateObserver.setEnabled(this.state.enabled);
872
+ this.notify();
873
+ }
874
+ } catch {
875
+ }
876
+ }
877
+ getState() {
878
+ return { ...this.state };
879
+ }
880
+ subscribe(listener) {
881
+ this.listeners.add(listener);
882
+ return () => {
883
+ this.listeners.delete(listener);
884
+ };
885
+ }
886
+ notify() {
887
+ this.listeners.forEach((l) => {
888
+ try {
889
+ l();
890
+ } catch {
891
+ }
892
+ });
893
+ }
894
+ /**
895
+ * Debounced server push: fast typing in the baseUrl/model/concurrency inputs
896
+ * collapses into a single POST (300ms trailing). A monotonic sequence number
897
+ * ensures an out-of-order older response never overwrites newer state.
898
+ */
899
+ schedulePush() {
900
+ if (this.pushTimer !== null || typeof window === "undefined") return;
901
+ this.pushTimer = window.setTimeout(() => {
902
+ this.pushTimer = null;
903
+ this.pushToServer();
904
+ }, 300);
905
+ }
906
+ async pushToServer() {
907
+ const seq = ++this.pushSeq;
908
+ const sent = {
909
+ enabled: this.state.enabled,
910
+ concurrency: this.state.concurrency,
911
+ aiEnabled: this.state.aiEnabled,
912
+ bingEnabled: this.state.bingEnabled,
913
+ baseUrl: this.state.baseUrl,
914
+ model: this.state.model
915
+ };
916
+ try {
917
+ const updated = await updateServerConfig(sent);
918
+ if (!updated || seq !== this.pushSeq) return;
919
+ this.state = {
920
+ ...this.state,
921
+ enabled: this.state.enabled === sent.enabled && typeof updated.enabled === "boolean" ? updated.enabled : this.state.enabled,
922
+ concurrency: this.state.concurrency === sent.concurrency && typeof updated.concurrency === "number" ? updated.concurrency : this.state.concurrency,
923
+ aiEnabled: this.state.aiEnabled === sent.aiEnabled && typeof updated.aiEnabled === "boolean" ? updated.aiEnabled : this.state.aiEnabled,
924
+ bingEnabled: this.state.bingEnabled === sent.bingEnabled && typeof updated.bingEnabled === "boolean" ? updated.bingEnabled : this.state.bingEnabled,
925
+ baseUrl: this.state.baseUrl === sent.baseUrl && typeof updated.baseUrl === "string" ? updated.baseUrl : this.state.baseUrl,
926
+ model: this.state.model === sent.model && typeof updated.model === "string" ? updated.model : this.state.model,
927
+ aiConfigured: typeof updated.aiConfigured === "boolean" ? updated.aiConfigured : this.state.aiConfigured
928
+ };
929
+ this.notify();
930
+ } catch {
931
+ }
932
+ }
933
+ async update(partial) {
934
+ this.userTouched = true;
935
+ let sanitizedConcurrency = this.state.concurrency;
936
+ if (typeof partial.concurrency === "number") {
937
+ if (Number.isFinite(partial.concurrency)) {
938
+ sanitizedConcurrency = Math.min(Math.max(Math.round(partial.concurrency), 1), 100);
939
+ }
940
+ }
941
+ this.state = {
942
+ ...this.state,
943
+ ...partial,
944
+ concurrency: sanitizedConcurrency
945
+ };
946
+ if (typeof partial.enabled === "boolean") {
947
+ try {
948
+ localStorage.setItem(LS_ENABLED, String(partial.enabled));
949
+ } catch {
950
+ }
951
+ chatTranslateObserver.setEnabled(partial.enabled);
952
+ }
953
+ if (typeof partial.concurrency === "number" && Number.isFinite(partial.concurrency)) {
954
+ try {
955
+ localStorage.setItem(LS_CONCURRENCY, String(sanitizedConcurrency));
956
+ } catch {
957
+ }
958
+ }
959
+ if (typeof partial.aiEnabled === "boolean") {
960
+ try {
961
+ localStorage.setItem(LS_AI_ENABLED, String(partial.aiEnabled));
962
+ } catch {
963
+ }
964
+ }
965
+ if (typeof partial.bingEnabled === "boolean") {
966
+ try {
967
+ localStorage.setItem(LS_BING_ENABLED, String(partial.bingEnabled));
968
+ } catch {
969
+ }
970
+ }
971
+ if (typeof partial.baseUrl === "string") {
972
+ try {
973
+ localStorage.setItem(LS_BASE_URL, partial.baseUrl);
974
+ } catch {
975
+ }
976
+ }
977
+ if (typeof partial.model === "string") {
978
+ try {
979
+ localStorage.setItem(LS_MODEL, partial.model);
980
+ } catch {
981
+ }
982
+ }
983
+ this.notify();
984
+ this.schedulePush();
985
+ }
986
+ async testChannel(channel) {
987
+ return testServerChannel(channel);
988
+ }
989
+ /** Persist the API key via the host and refresh the server-derived status. */
990
+ async saveApiKey(apiKey) {
991
+ const res = await saveCredentials(apiKey);
992
+ if (res.ok) {
993
+ await this.syncFromServer();
994
+ }
995
+ return { ok: Boolean(res.ok), error: res.error };
996
+ }
997
+ /** Re-pull the server config (e.g. after the API key changed). */
998
+ async refreshFromServer() {
999
+ await this.syncFromServer();
1000
+ }
1001
+ dispose() {
1002
+ if (this.pushTimer !== null && typeof window !== "undefined") {
1003
+ clearTimeout(this.pushTimer);
1004
+ this.pushTimer = null;
1005
+ }
1006
+ if (this.storageListener && typeof window !== "undefined") {
1007
+ window.removeEventListener("storage", this.storageListener);
1008
+ this.storageListener = null;
1009
+ }
1010
+ this.listeners.clear();
1011
+ }
1012
+ };
1013
+ var settingsStore = new SettingsStore();
1014
+
1015
+ // src/client/settings/styles.ts
1016
+ var SETTINGS_CSS = String.raw`
1017
+ .dsh-tidy-settings {
1018
+ display: flex;
1019
+ flex-direction: column;
1020
+ gap: 16px;
1021
+ max-width: 580px;
1022
+ padding-bottom: 32px;
1023
+ color: var(--dsw-alias-label-primary, inherit);
1024
+ font-family: inherit;
1025
+ }
1026
+
1027
+ .dsh-tidy-card {
1028
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.2));
1029
+ border-radius: 12px;
1030
+ padding: 16px 18px;
1031
+ background: var(--dsw-alias-bg-card, rgba(128, 128, 128, 0.05));
1032
+ display: flex;
1033
+ flex-direction: column;
1034
+ gap: 12px;
1035
+ }
1036
+
1037
+ .dsh-tidy-title {
1038
+ font-size: 14px;
1039
+ font-weight: 600;
1040
+ color: var(--dsw-alias-label-primary, inherit);
1041
+ display: flex;
1042
+ align-items: center;
1043
+ justify-content: space-between;
1044
+ }
1045
+
1046
+ .dsh-tidy-desc {
1047
+ font-size: 12px;
1048
+ line-height: 1.5;
1049
+ color: var(--dsw-alias-label-secondary, rgba(128, 128, 128, 0.8));
1050
+ }
1051
+
1052
+ .dsh-tidy-row {
1053
+ display: flex;
1054
+ align-items: center;
1055
+ justify-content: space-between;
1056
+ gap: 14px;
1057
+ padding: 8px 0;
1058
+ }
1059
+
1060
+ .dsh-tidy-row + .dsh-tidy-row {
1061
+ border-top: 1px solid var(--dsw-alias-border-l3, rgba(128, 128, 128, 0.1));
1062
+ }
1063
+
1064
+ .dsh-tidy-row-info {
1065
+ flex: 1;
1066
+ min-width: 0;
1067
+ }
1068
+
1069
+ .dsh-tidy-row-title {
1070
+ font-size: 13px;
1071
+ font-weight: 500;
1072
+ }
1073
+
1074
+ .dsh-tidy-row-desc {
1075
+ font-size: 11px;
1076
+ color: var(--dsw-alias-label-secondary, rgba(128, 128, 128, 0.7));
1077
+ margin-top: 2px;
1078
+ line-height: 1.4;
1079
+ }
1080
+
1081
+ /* Switch */
1082
+ .dsh-tidy-switch {
1083
+ position: relative;
1084
+ width: 38px;
1085
+ height: 22px;
1086
+ flex: none;
1087
+ cursor: pointer;
1088
+ border-radius: 999px;
1089
+ border: none;
1090
+ background: rgba(128, 128, 128, 0.3);
1091
+ transition: background 0.15s ease;
1092
+ padding: 0;
1093
+ outline: none;
1094
+ }
1095
+
1096
+ .dsh-tidy-switch[aria-checked="true"] {
1097
+ background: #3b82f6;
1098
+ }
1099
+
1100
+ .dsh-tidy-switch::after {
1101
+ content: "";
1102
+ position: absolute;
1103
+ top: 2px;
1104
+ left: 2px;
1105
+ width: 18px;
1106
+ height: 18px;
1107
+ border-radius: 50%;
1108
+ background: #ffffff;
1109
+ transition: transform 0.15s ease;
1110
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
1111
+ }
1112
+
1113
+ .dsh-tidy-switch[aria-checked="true"]::after {
1114
+ transform: translateX(16px);
1115
+ }
1116
+
1117
+ /* Inputs */
1118
+ .dsh-tidy-input-group {
1119
+ display: flex;
1120
+ flex-direction: column;
1121
+ gap: 6px;
1122
+ }
1123
+
1124
+ .dsh-tidy-label {
1125
+ font-size: 12px;
1126
+ font-weight: 500;
1127
+ display: flex;
1128
+ align-items: center;
1129
+ justify-content: space-between;
1130
+ }
1131
+
1132
+ .dsh-tidy-input-row {
1133
+ display: flex;
1134
+ gap: 8px;
1135
+ }
1136
+
1137
+ .dsh-tidy-input {
1138
+ flex: 1;
1139
+ min-width: 0;
1140
+ padding: 8px 12px;
1141
+ border-radius: 8px;
1142
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.25));
1143
+ background: var(--dsw-alias-bg-input, rgba(0, 0, 0, 0.05));
1144
+ color: inherit;
1145
+ font-size: 12px;
1146
+ outline: none;
1147
+ transition: border-color 0.15s;
1148
+ }
1149
+
1150
+ .dsh-tidy-input:focus {
1151
+ border-color: #3b82f6;
1152
+ }
1153
+
1154
+ .dsh-tidy-btn {
1155
+ padding: 6px 14px;
1156
+ border-radius: 8px;
1157
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.3));
1158
+ background: transparent;
1159
+ color: inherit;
1160
+ font-size: 12px;
1161
+ cursor: pointer;
1162
+ transition: all 0.15s ease;
1163
+ white-space: nowrap;
1164
+ }
1165
+
1166
+ .dsh-tidy-btn:hover {
1167
+ background: rgba(128, 128, 128, 0.1);
1168
+ border-color: rgba(128, 128, 128, 0.5);
1169
+ }
1170
+
1171
+ .dsh-tidy-btn:disabled {
1172
+ opacity: 0.5;
1173
+ cursor: not-allowed;
1174
+ }
1175
+
1176
+ .dsh-tidy-badge {
1177
+ font-size: 10px;
1178
+ padding: 2px 6px;
1179
+ border-radius: 4px;
1180
+ font-weight: 500;
1181
+ }
1182
+
1183
+ .dsh-tidy-badge-ok {
1184
+ background: rgba(34, 197, 94, 0.15);
1185
+ color: #22c55e;
1186
+ }
1187
+
1188
+ .dsh-tidy-badge-none {
1189
+ background: rgba(156, 163, 175, 0.15);
1190
+ color: #9ca3af;
1191
+ }
1192
+
1193
+ .dsh-tidy-badge-warn {
1194
+ background: rgba(245, 158, 11, 0.15);
1195
+ color: #f59e0b;
1196
+ }
1197
+
1198
+ .dsh-tidy-test-result {
1199
+ font-size: 12px;
1200
+ margin-left: 10px;
1201
+ }
1202
+
1203
+ .dsh-tidy-test-result.ok {
1204
+ color: #22c55e;
1205
+ }
1206
+
1207
+ .dsh-tidy-test-result.fail {
1208
+ color: #ef4444;
1209
+ }
1210
+
1211
+ .dsh-tidy-behavior-list {
1212
+ padding-left: 18px;
1213
+ margin: 4px 0 0;
1214
+ display: flex;
1215
+ flex-direction: column;
1216
+ gap: 4px;
1217
+ }
1218
+
1219
+ /* Priority list */
1220
+ .dsh-tidy-priority-list {
1221
+ display: flex;
1222
+ flex-direction: column;
1223
+ gap: 8px;
1224
+ }
1225
+
1226
+ .dsh-tidy-priority-item {
1227
+ display: flex;
1228
+ align-items: center;
1229
+ justify-content: space-between;
1230
+ padding: 8px 12px;
1231
+ border-radius: 8px;
1232
+ background: rgba(128, 128, 128, 0.06);
1233
+ border: 1px solid var(--dsw-alias-border-l3, rgba(128, 128, 128, 0.12));
1234
+ }
1235
+
1236
+ .dsh-tidy-priority-name {
1237
+ font-size: 12px;
1238
+ font-weight: 500;
1239
+ display: flex;
1240
+ align-items: center;
1241
+ gap: 8px;
1242
+ }
1243
+
1244
+ .dsh-tidy-order-badge {
1245
+ display: inline-flex;
1246
+ align-items: center;
1247
+ justify-content: center;
1248
+ width: 18px;
1249
+ height: 18px;
1250
+ border-radius: 50%;
1251
+ background: #3b82f6;
1252
+ color: #ffffff;
1253
+ font-size: 11px;
1254
+ font-weight: 600;
1255
+ }
1256
+
1257
+ .dsh-tidy-btn-group {
1258
+ display: flex;
1259
+ gap: 4px;
1260
+ }
1261
+
1262
+ .dsh-tidy-icon-btn {
1263
+ width: 26px;
1264
+ height: 26px;
1265
+ padding: 0;
1266
+ display: flex;
1267
+ align-items: center;
1268
+ justify-content: center;
1269
+ border-radius: 6px;
1270
+ border: 1px solid rgba(128, 128, 128, 0.2);
1271
+ background: transparent;
1272
+ color: inherit;
1273
+ cursor: pointer;
1274
+ font-size: 12px;
1275
+ }
1276
+
1277
+ .dsh-tidy-icon-btn:hover:not(:disabled) {
1278
+ background: rgba(128, 128, 128, 0.15);
1279
+ }
1280
+
1281
+ .dsh-tidy-icon-btn:disabled {
1282
+ opacity: 0.3;
1283
+ cursor: default;
1284
+ }
1285
+
1286
+ /* Slider / Select */
1287
+ .dsh-tidy-select {
1288
+ padding: 6px 10px;
1289
+ border-radius: 8px;
1290
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.3));
1291
+ background: var(--dsw-alias-bg-input, rgba(0, 0, 0, 0.05));
1292
+ color: inherit;
1293
+ font-size: 12px;
1294
+ outline: none;
1295
+ }
1296
+ `;
1297
+
1298
+ // src/client/settings/ui.tsx
1299
+ var import_jsx_runtime = require("react/jsx-runtime");
1300
+ var stylesInjected = false;
1301
+ function ensureSettingsStyles() {
1302
+ if (stylesInjected || typeof document === "undefined") return;
1303
+ const el = document.createElement("style");
1304
+ el.dataset.tidySettings = "true";
1305
+ el.textContent = SETTINGS_CSS;
1306
+ document.head.appendChild(el);
1307
+ stylesInjected = true;
1308
+ }
1309
+ function Switch(props) {
1310
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1311
+ "button",
1312
+ {
1313
+ type: "button",
1314
+ className: "dsh-tidy-switch",
1315
+ role: "switch",
1316
+ "aria-checked": props.checked,
1317
+ onClick: props.onChange,
1318
+ "aria-label": props.label
1319
+ }
1320
+ );
1321
+ }
1322
+ function TidySettingsPanel() {
1323
+ ensureSettingsStyles();
1324
+ const [state, setState] = (0, import_react.useState)(() => settingsStore.getState());
1325
+ const [testing, setTesting] = (0, import_react.useState)(null);
1326
+ const [apiKeyInput, setApiKeyInput] = (0, import_react.useState)("");
1327
+ const [savingKey, setSavingKey] = (0, import_react.useState)(false);
1328
+ const [keyMsg, setKeyMsg] = (0, import_react.useState)(null);
1329
+ (0, import_react.useEffect)(() => {
1330
+ return settingsStore.subscribe(() => {
1331
+ setState(settingsStore.getState());
1332
+ });
1333
+ }, []);
1334
+ const runTest = async (channel) => {
1335
+ setTesting({ channel, running: true });
1336
+ const res = await settingsStore.testChannel(channel);
1337
+ const message = res.ok ? `\u8FDE\u63A5\u6B63\u5E38\uFF0C\u5EF6\u8FDF ${res.latencyMs}ms` : `\u5931\u8D25\uFF1A${res.error || "\u672A\u77E5\u9519\u8BEF"}`;
1338
+ setTesting({ channel, running: false, ok: res.ok, message });
1339
+ };
1340
+ const handleSaveKey = async () => {
1341
+ setSavingKey(true);
1342
+ setKeyMsg(null);
1343
+ const res = await settingsStore.saveApiKey(apiKeyInput);
1344
+ setSavingKey(false);
1345
+ if (res.ok) {
1346
+ const cleared = !apiKeyInput.trim();
1347
+ setKeyMsg({
1348
+ ok: true,
1349
+ text: cleared ? "\u5DF2\u6E05\u9664 API Key\uFF08AI \u901A\u9053\u5C06\u4E0D\u53EF\u7528\uFF0CBing \u515C\u5E95\uFF09" : "\u5DF2\u4FDD\u5B58\u5230 ~/.dsh/.credentials.yaml\uFF0C\u7ACB\u5373\u751F\u6548"
1350
+ });
1351
+ setApiKeyInput("");
1352
+ } else {
1353
+ setKeyMsg({ ok: false, text: `\u4FDD\u5B58\u5931\u8D25\uFF1A${res.error || "\u672A\u77E5\u9519\u8BEF"}` });
1354
+ }
1355
+ };
1356
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-settings", children: [
1357
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-card", children: [
1358
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-title", children: [
1359
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "\u5DE5\u5177\u8C03\u7528\u4E0E\u601D\u8003\u6458\u8981\u7FFB\u8BD1" }),
1360
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1361
+ Switch,
1362
+ {
1363
+ checked: state.enabled,
1364
+ onChange: () => settingsStore.update({ enabled: !state.enabled }),
1365
+ label: "\u542F\u7528\u5DE5\u5177\u8C03\u7528\u4E0E\u6458\u8981\u7FFB\u8BD1"
1366
+ }
1367
+ )
1368
+ ] }),
1369
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-desc", children: [
1370
+ "\u81EA\u52A8\u5C06\u5F53\u524D\u4F1A\u8BDD\u4E2D\u5DE5\u5177\u8C03\u7528\u6807\u9898\u4E0E\u601D\u8003\u6298\u53E0\u6458\u8981\uFF08\u5982 ",
1371
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "Locate DSH home directory structure" }),
1372
+ "\uFF09\u7FFB\u8BD1\u4E3A\u4E2D\u6587\uFF0C \u70B9\u51FB\u8BD1\u6587\u53EF\u539F\u5730\u5207\u6362\u539F\u6587/\u8BD1\u6587\u3002\u4EC5\u4F5C\u7528\u4E8E\u5F53\u524D\u67E5\u770B\u7684\u4F1A\u8BDD\uFF0C\u5BF9\u8BDD\u6B63\u6587\u6C38\u4E0D\u7FFB\u8BD1\u3002"
1373
+ ] })
1374
+ ] }),
1375
+ state.enabled && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1376
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-card", children: [
1377
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-title", children: [
1378
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "AI \u7FFB\u8BD1\uFF08OpenAI \u517C\u5BB9\u534F\u8BAE\uFF09" }),
1379
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1380
+ Switch,
1381
+ {
1382
+ checked: state.aiEnabled,
1383
+ onChange: () => settingsStore.update({ aiEnabled: !state.aiEnabled }),
1384
+ label: "\u542F\u7528 AI \u7FFB\u8BD1\u901A\u9053"
1385
+ }
1386
+ )
1387
+ ] }),
1388
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-desc", children: [
1389
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `dsh-tidy-badge ${state.aiConfigured ? "dsh-tidy-badge-ok" : "dsh-tidy-badge-warn"}`, children: state.aiConfigured ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E" }),
1390
+ " ",
1391
+ "\u672A\u914D\u7F6E\u65F6 AI \u901A\u9053\u81EA\u52A8\u8DF3\u8FC7\uFF0C\u7531 Bing \u515C\u5E95\u3002"
1392
+ ] }),
1393
+ state.aiEnabled && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1394
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
1395
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
1396
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: "API Key" }),
1397
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-desc", children: [
1398
+ "\u4FDD\u5B58\u81F3 ",
1399
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "~/.dsh/.credentials.yaml" }),
1400
+ " \u7684 ",
1401
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "TRANSLATE_API_KEY" }),
1402
+ "\uFF0C\u4FDD\u5B58\u540E\u7ACB\u5373\u751F\u6548\uFF1B\u7559\u7A7A\u4FDD\u5B58 = \u6E05\u9664"
1403
+ ] })
1404
+ ] }),
1405
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-input-row", children: [
1406
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1407
+ "input",
1408
+ {
1409
+ type: "password",
1410
+ className: "dsh-tidy-input",
1411
+ placeholder: state.aiConfigured ? "\u5DF2\u914D\u7F6E\uFF08\u5982\u9700\u66F4\u6362\u8BF7\u76F4\u63A5\u8F93\u5165\uFF09" : "sk-...",
1412
+ value: apiKeyInput,
1413
+ onChange: (e) => setApiKeyInput(e.target.value),
1414
+ style: { width: "260px" },
1415
+ "aria-label": "API Key"
1416
+ }
1417
+ ),
1418
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "dsh-tidy-btn", disabled: savingKey, onClick: handleSaveKey, children: savingKey ? "\u4FDD\u5B58\u4E2D\u2026" : "\u4FDD\u5B58" })
1419
+ ] })
1420
+ ] }),
1421
+ keyMsg && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `dsh-tidy-test-result ${keyMsg.ok ? "ok" : "fail"}`, children: keyMsg.text }),
1422
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
1423
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
1424
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: "Base URL" }),
1425
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-desc", children: [
1426
+ "\u4EFB\u610F OpenAI \u517C\u5BB9\u670D\u52A1\u7AEF\u70B9\uFF0C\u5982 ",
1427
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "https://api.openai.com/v1" }),
1428
+ "\u3001",
1429
+ " ",
1430
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "https://api.deepseek.com/v1" })
1431
+ ] })
1432
+ ] }),
1433
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1434
+ "input",
1435
+ {
1436
+ type: "text",
1437
+ className: "dsh-tidy-input",
1438
+ placeholder: "https://api.openai.com/v1",
1439
+ value: state.baseUrl,
1440
+ onChange: (e) => settingsStore.update({ baseUrl: e.target.value }),
1441
+ style: { width: "260px" },
1442
+ "aria-label": "AI Base URL"
1443
+ }
1444
+ )
1445
+ ] }),
1446
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
1447
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
1448
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: "\u6A21\u578B" }),
1449
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-desc", children: [
1450
+ "\u5982 ",
1451
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "gpt-4o-mini" }),
1452
+ "\u3001",
1453
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "deepseek-chat" }),
1454
+ "\uFF1B\u7559\u7A7A\u89C6\u4E3A\u672A\u914D\u7F6E"
1455
+ ] })
1456
+ ] }),
1457
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1458
+ "input",
1459
+ {
1460
+ type: "text",
1461
+ className: "dsh-tidy-input",
1462
+ placeholder: "gpt-4o-mini",
1463
+ value: state.model,
1464
+ onChange: (e) => settingsStore.update({ model: e.target.value }),
1465
+ style: { width: "260px" },
1466
+ "aria-label": "AI \u6A21\u578B"
1467
+ }
1468
+ )
1469
+ ] }),
1470
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
1471
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1472
+ "button",
1473
+ {
1474
+ type: "button",
1475
+ className: "dsh-tidy-btn",
1476
+ disabled: testing?.running,
1477
+ onClick: () => runTest("openai"),
1478
+ children: testing?.running ? "\u6D4B\u8BD5\u4E2D\u2026" : "\u6D4B\u8BD5 AI \u901A\u9053"
1479
+ }
1480
+ ),
1481
+ testing?.channel === "openai" && !testing.running && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `dsh-tidy-test-result ${testing.ok ? "ok" : "fail"}`, children: testing.message })
1482
+ ] })
1483
+ ] })
1484
+ ] }),
1485
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-card", children: [
1486
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-title", children: [
1487
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Bing \u7F51\u9875\u7FFB\u8BD1\uFF08\u514D Key \u515C\u5E95\uFF09" }),
1488
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1489
+ Switch,
1490
+ {
1491
+ checked: state.bingEnabled,
1492
+ onChange: () => settingsStore.update({ bingEnabled: !state.bingEnabled }),
1493
+ label: "\u542F\u7528 Bing \u7FFB\u8BD1\u901A\u9053"
1494
+ }
1495
+ )
1496
+ ] }),
1497
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-desc", children: "\u5185\u7F6E\u514D Key \u7FFB\u8BD1\u901A\u9053\uFF0C\u65E0\u9700\u4EFB\u4F55\u914D\u7F6E\u3002AI \u672A\u914D\u7F6E\u6216\u8BF7\u6C42\u5931\u8D25\u65F6\u81EA\u52A8\u515C\u5E95\uFF1BAI \u4E0E Bing \u540C\u65F6\u5173\u95ED\u5219\u4E0D\u7FFB\u8BD1\u3002" })
1498
+ ] }),
1499
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-card", children: [
1500
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: "\u901A\u9053\u884C\u4E3A" }),
1501
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("ul", { className: "dsh-tidy-desc dsh-tidy-behavior-list", children: [
1502
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: "AI \u5F00\u542F\u4E14\u5DF2\u914D\u7F6E \u2192 AI \u4F18\u5148\u7FFB\u8BD1\uFF0C\u5931\u8D25\u81EA\u52A8\u964D\u7EA7 Bing" }),
1503
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: "AI \u5F00\u542F\u4F46\u672A\u914D\u7F6E + Bing \u5F00\u542F \u2192 \u7531 Bing \u7FFB\u8BD1" }),
1504
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: "AI \u5F00\u542F\u4F46\u672A\u914D\u7F6E + Bing \u5173\u95ED \u2192 \u4E0D\u7FFB\u8BD1" }),
1505
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: "AI \u5173\u95ED + Bing \u5F00\u542F \u2192 \u76F4\u63A5\u4F7F\u7528 Bing" }),
1506
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: "AI \u5173\u95ED + Bing \u5173\u95ED \u2192 \u4E0D\u7FFB\u8BD1" })
1507
+ ] })
1508
+ ] }),
1509
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-card", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
1510
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
1511
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: "\u6700\u5927\u7FFB\u8BD1\u5E76\u53D1\u6570" }),
1512
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: "\u63A7\u5236\u89C6\u53E3\u6EDA\u52A8\u4E0E\u591A\u5DE5\u5177\u5361\u7247\u65F6\u7684\u6700\u5927\u5E76\u884C\u8BF7\u6C42\u6570\uFF08\u8303\u56F4 1-100\uFF0C\u63A8\u8350 3\uFF09\u3002" })
1513
+ ] }),
1514
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1515
+ "input",
1516
+ {
1517
+ type: "number",
1518
+ className: "dsh-tidy-input",
1519
+ min: 1,
1520
+ max: 100,
1521
+ step: 1,
1522
+ value: state.concurrency,
1523
+ onChange: (e) => {
1524
+ const val = parseInt(e.target.value, 10);
1525
+ if (!Number.isFinite(val)) return;
1526
+ settingsStore.update({ concurrency: Math.min(Math.max(val, 1), 100) });
1527
+ },
1528
+ style: { width: "88px" },
1529
+ "aria-label": "\u6700\u5927\u7FFB\u8BD1\u5E76\u53D1\u6570"
1530
+ }
1531
+ )
1532
+ ] }) })
1533
+ ] })
1534
+ ] });
1535
+ }
1536
+ function setupSettingsUi(ctx) {
1537
+ if (typeof window === "undefined") return;
1538
+ try {
1539
+ const slots = ctx?.slots || (ctx?.get ? ctx.get("slots") : null);
1540
+ if (!slots || typeof slots.inject !== "function") return;
1541
+ slots.inject("settings.section", () => {
1542
+ return slots.register(
1543
+ {
1544
+ name: "settings.section",
1545
+ id: "dsh-chat-translate",
1546
+ order: 5,
1547
+ label: () => "\u804A\u5929\u7FFB\u8BD1"
1548
+ },
1549
+ TidySettingsPanel
1550
+ );
1551
+ });
1552
+ } catch (err) {
1553
+ console.warn("[dsh-chat-translate] Failed to inject settings section:", err);
1554
+ }
1555
+ }
1556
+
1557
+ // src/client/index.ts
1558
+ var name = "dsh-chat-translate";
1559
+ var inject = ["slots"];
1560
+ function apply(ctx) {
1561
+ ctx.effect(() => chatTranslateObserver.start(document), "dsh-chat-translate: title translate observer");
1562
+ ctx.effect(() => setupSettingsUi(ctx), "dsh-chat-translate: settings section");
1563
+ }
1564
+ return module.exports; } });
1565
+ //# sourceMappingURL=client.js.map