dsh-web-search-plugin 0.2.0 → 0.2.1

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 CHANGED
@@ -38,11 +38,39 @@ window.__ModuleLoader__.load({
38
38
  /** Form field the DeepSeek write-only credential control stages under. */
39
39
  const DEEPSEEK_API_KEY_FIELD = "deepseekApiKey";
40
40
  const SECRET_FIELDS = new Set([TAVILY_API_KEY_FIELD, BRAVE_API_KEY_FIELD, DEEPSEEK_API_KEY_FIELD]);
41
+ const DRAFT_KEY = "dsh-web-search-plugin:settings-draft";
42
+
43
+ function readDraft() {
44
+ try {
45
+ const raw = sessionStorage.getItem(DRAFT_KEY);
46
+ if (!raw) return null;
47
+ const parsed = JSON.parse(raw);
48
+ if (parsed === null || typeof parsed !== "object" || parsed.staged === null || typeof parsed.staged !== "object") return null;
49
+ return parsed;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+ function writeDraft(form) {
55
+ try {
56
+ const staged = {};
57
+ for (const [field, edit] of form.staged) {
58
+ if (SECRET_FIELDS.has(field)) continue;
59
+ staged[field] = { text: String(edit?.text ?? ""), clear: edit?.clear === true };
60
+ }
61
+ if (Object.keys(staged).length === 0 && form.failed !== true) {
62
+ sessionStorage.removeItem(DRAFT_KEY);
63
+ return;
64
+ }
65
+ sessionStorage.setItem(DRAFT_KEY, JSON.stringify({ staged, failed: form.failed === true }));
66
+ } catch {
67
+ /* private mode / quota */
68
+ }
69
+ }
41
70
 
42
71
  /** Locale bundles for the card. */
43
72
  const en = {
44
73
  nav: "Web search",
45
- description: "Tavily (keyless by default) or Brave Search. Switching engines here is enough — web.searchProvider stays dsh-web-search.",
46
74
  provider: "Search engine",
47
75
  providerTavily: "Tavily",
48
76
  providerBrave: "Brave Search",
@@ -63,7 +91,6 @@ window.__ModuleLoader__.load({
63
91
  braveBaseURL: "Endpoint",
64
92
  braveBaseURLHint: "Leave blank to use the Brave Search API default.",
65
93
  maxResults: "Max results",
66
- maxResultsHint: "Sources returned per search (1-20).",
67
94
  searchDepth: "Search depth",
68
95
  searchDepthBasic: "Basic (faster)",
69
96
  searchDepthAdvanced: "Advanced (slower, deeper)",
@@ -73,9 +100,9 @@ window.__ModuleLoader__.load({
73
100
  topicGeneral: "General",
74
101
  topicNews: "News",
75
102
  country: "Country",
76
- countryHint: "ISO 2-letter code (e.g. cn). Leave blank for Brave's default.",
103
+ countryHint: "ISO 2-letter code (e.g. cn).",
77
104
  searchLang: "Search language",
78
- searchLangHint: "e.g. zh-hans or en. Leave blank for Brave's default.",
105
+ searchLangHint: "e.g. zh-hans or en.",
79
106
  freshness: "Freshness",
80
107
  freshnessAny: "Any time",
81
108
  freshnessDay: "Past day",
@@ -83,7 +110,7 @@ window.__ModuleLoader__.load({
83
110
  freshnessMonth: "Past month",
84
111
  freshnessYear: "Past year",
85
112
  proxy: "Proxy",
86
- proxyHint: "HTTP(S) proxy URL. Leave blank to use HTTPS_PROXY / HTTP_PROXY.",
113
+ proxyPlaceholder: "Leave blank to use HTTPS_PROXY / HTTP_PROXY.",
87
114
  deepseekApiKeyEnv: "Credential reference",
88
115
  deepseekApiKeyEnvHint: "Name of the credential/env var resolved for DeepSeek search.",
89
116
  deepseekBaseURL: "Endpoint",
@@ -102,14 +129,40 @@ window.__ModuleLoader__.load({
102
129
  saving: "Saving…",
103
130
  discard: "Discard",
104
131
  unsaved: "Unsaved",
132
+ overridden: "Overridden",
133
+ reset: "Restore default",
105
134
  saveFailed: "The deployment did not accept these values; they were left for you to correct.",
135
+ savedToast: "✓ Settings saved and applied",
136
+ nothingToSave: "No changes to save",
137
+ invalidToast: "Fix the highlighted fields before saving.",
106
138
  readOnly: "This deployment stores settings read-only.",
107
- invalidNumber: "Enter a number, or leave blank to use the default."
139
+ invalidNumber: "Enter a whole number of at least 1, or leave blank for the default.",
140
+ quotaTitle: "Quota",
141
+ quotaPlan: "Plan {plan}",
142
+ quotaUsed: "Used {used} / {limit}",
143
+ quotaUsedOnly: "Used {used}",
144
+ quotaRemaining: "{remaining} left",
145
+ quotaUnlimited: "No published limit",
146
+ quotaResets: "Resets {time}",
147
+ quotaCycle: "Resets with the billing cycle",
148
+ quotaBurst: "Burst {remaining}/{limit}",
149
+ quotaBraveCapacity: "Capacity",
150
+ quotaBraveRps: "{limit} requests per second",
151
+ quotaBraveRpsWindow: "Per-second window",
152
+ braveDefaultPlaceholder: "Leave blank for Brave's default.",
153
+ quotaEmpty: "Usage appears after the next search.",
154
+ quotaKeyedHint: "Keyed Tavily: local deductions vs periodic /usage, whichever is higher.",
155
+ quotaBraveUnlimited: "No monthly request cap",
156
+ quotaRefresh: "Refresh usage",
157
+ quotaRefreshing: "Refreshing…",
158
+ quotaError: "Usage unavailable: {error}",
159
+ quotaWarn: "Remaining quota is low.",
160
+ quotaDanger: "Remaining quota is critical.",
161
+ quotaPaygo: "Pay-as-you-go {used}/{limit}"
108
162
  };
109
163
  /** Simplified Chinese copy. */
110
164
  const zh = {
111
165
  nav: "网页搜索",
112
- description: "Tavily(默认 keyless)或 Brave Search。在此切换引擎即可,不必改 web.searchProvider。",
113
166
  provider: "搜索引擎",
114
167
  providerTavily: "Tavily",
115
168
  providerBrave: "Brave Search",
@@ -130,7 +183,6 @@ window.__ModuleLoader__.load({
130
183
  braveBaseURL: "接口地址",
131
184
  braveBaseURLHint: "留空则使用 Brave Search API 默认地址。",
132
185
  maxResults: "结果数量",
133
- maxResultsHint: "每次搜索返回的源数量(1-20)。",
134
186
  searchDepth: "搜索深度",
135
187
  searchDepthBasic: "Basic(较快)",
136
188
  searchDepthAdvanced: "Advanced(较慢、更深)",
@@ -140,9 +192,9 @@ window.__ModuleLoader__.load({
140
192
  topicGeneral: "通用",
141
193
  topicNews: "新闻",
142
194
  country: "国家/地区",
143
- countryHint: "ISO 两位码(如 cn)。留空使用 Brave 默认。",
195
+ countryHint: "ISO 两位码(如 cn)。",
144
196
  searchLang: "搜索语言",
145
- searchLangHint: "如 zh-hans、en。留空使用 Brave 默认。",
197
+ searchLangHint: "如 zh-hans、en。",
146
198
  freshness: "时效",
147
199
  freshnessAny: "不限",
148
200
  freshnessDay: "过去一天",
@@ -150,7 +202,7 @@ window.__ModuleLoader__.load({
150
202
  freshnessMonth: "过去一月",
151
203
  freshnessYear: "过去一年",
152
204
  proxy: "代理",
153
- proxyHint: "HTTP(S) 代理 URL。留空则使用 HTTPS_PROXY / HTTP_PROXY。",
205
+ proxyPlaceholder: "留空使用 HTTPS_PROXY / HTTP_PROXY环境变量",
154
206
  deepseekApiKeyEnv: "凭据引用名",
155
207
  deepseekApiKeyEnvHint: "DeepSeek 搜索解析的凭据/环境变量名。",
156
208
  deepseekBaseURL: "接口地址",
@@ -169,21 +221,53 @@ window.__ModuleLoader__.load({
169
221
  saving: "保存中…",
170
222
  discard: "放弃修改",
171
223
  unsaved: "未保存",
224
+ overridden: "已覆盖",
225
+ reset: "恢复默认",
172
226
  saveFailed: "本部署没有接受这些值,已保留供你修改。",
227
+ savedToast: "✓ 设置已保存并立即生效",
228
+ nothingToSave: "没有需要保存的更改",
229
+ invalidToast: "请先修正标红的字段再保存。",
173
230
  readOnly: "本部署的设置为只读。",
174
- invalidNumber: "请填数字;留空表示使用默认值。"
231
+ invalidNumber: "请输入不小于 1 的整数;留空表示使用默认值。",
232
+ quotaTitle: "额度",
233
+ quotaPlan: "计划 {plan}",
234
+ quotaUsed: "已用 {used} / {limit}",
235
+ quotaUsedOnly: "已用 {used}",
236
+ quotaRemaining: "剩余 {remaining}",
237
+ quotaUnlimited: "未公布限额",
238
+ quotaResets: "{time} 重置",
239
+ quotaCycle: "按计费周期重置",
240
+ quotaBurst: "突发 {remaining}/{limit}",
241
+ quotaBraveCapacity: "容量",
242
+ quotaBraveRps: "{limit} 次/秒",
243
+ quotaBraveRpsWindow: "每秒窗口",
244
+ braveDefaultPlaceholder: "留空使用 Brave 默认。",
245
+ quotaEmpty: "下次搜索后显示用量。",
246
+ quotaKeyedHint: "Tavily keyed:本地扣费与定时 /usage 取较高值。",
247
+ quotaBraveUnlimited: "月请求不限额",
248
+ quotaRefresh: "刷新额度",
249
+ quotaRefreshing: "刷新中…",
250
+ quotaError: "额度不可用:{error}",
251
+ quotaWarn: "剩余额度偏低。",
252
+ quotaDanger: "剩余额度告急。",
253
+ quotaPaygo: "按量 {used}/{limit}"
175
254
  };
176
255
 
177
- /** A whole-number field spec: empty draft clears, any other invalid draft blocks save. */
178
- function numberField(field) {
256
+ const MAX_RESULTS_CAP = 20;
257
+ /** A whole-number field spec: empty draft clears; non-digits block save; max is clamped. */
258
+ function numberField(field, bounds) {
179
259
  return {
180
260
  field,
181
261
  format: (value) => typeof value === "number" ? String(value) : "",
182
262
  parse: (text) => {
183
263
  const trimmed = text.trim();
184
264
  if (trimmed === "") return { kind: "clear" };
185
- const parsed = Number(trimmed);
186
- return Number.isFinite(parsed) ? { kind: "set", value: parsed } : void 0;
265
+ if (!/^[0-9]+$/u.test(trimmed)) return void 0;
266
+ let parsed = Number(trimmed);
267
+ if (!Number.isInteger(parsed) || parsed < 1) return void 0;
268
+ if (bounds?.min != null && parsed < bounds.min) parsed = bounds.min;
269
+ if (bounds?.max != null && parsed > bounds.max) parsed = bounds.max;
270
+ return { kind: "set", value: parsed };
187
271
  }
188
272
  };
189
273
  }
@@ -227,7 +311,7 @@ window.__ModuleLoader__.load({
227
311
  this.specs = new Map([
228
312
  choiceField("provider"),
229
313
  choiceField("mode"),
230
- numberField("maxResults"),
314
+ numberField("maxResults", { min: 1, max: MAX_RESULTS_CAP }),
231
315
  textField("apiKeyEnv"),
232
316
  textField("baseURL"),
233
317
  choiceField("searchDepth"),
@@ -243,14 +327,24 @@ window.__ModuleLoader__.load({
243
327
  textField("deepseekBaseURL"),
244
328
  textField("model"),
245
329
  textField("apiVersion"),
246
- numberField("maxTokens"),
247
- numberField("maxUses")
330
+ numberField("maxTokens", { min: 1 }),
331
+ numberField("maxUses", { min: 1 })
248
332
  ].map((spec) => [spec.field, spec]));
249
333
  this.staged = new Map();
250
334
  this.saving = false;
251
335
  this.failed = false;
252
336
  this.credential = { ref: "", configured: false, writable: true };
337
+ this.toastKey = null;
338
+ this.toastTimer = 0;
253
339
  this.listeners = new Set();
340
+ const draft = readDraft();
341
+ if (draft?.staged) {
342
+ for (const [field, edit] of Object.entries(draft.staged)) {
343
+ if (!this.specs.has(field) || edit === null || typeof edit !== "object") continue;
344
+ this.staged.set(field, { text: String(edit.text ?? ""), clear: edit.clear === true });
345
+ }
346
+ if (draft.failed === true) this.failed = true;
347
+ }
254
348
  scope.subscribe(() => {
255
349
  this.publish();
256
350
  });
@@ -272,7 +366,8 @@ window.__ModuleLoader__.load({
272
366
  dirty: plan.length > 0,
273
367
  invalid: plan.some((item) => item.run === void 0),
274
368
  saving: this.saving,
275
- failed: this.failed
369
+ failed: this.failed,
370
+ toastKey: this.toastKey
276
371
  };
277
372
  }
278
373
  field(field) {
@@ -316,19 +411,41 @@ window.__ModuleLoader__.load({
316
411
  }
317
412
  return plan;
318
413
  }
414
+ showToast(key) {
415
+ this.toastKey = key;
416
+ if (this.toastTimer) clearTimeout(this.toastTimer);
417
+ this.toastTimer = setTimeout(() => {
418
+ this.toastKey = null;
419
+ this.toastTimer = 0;
420
+ this.publish();
421
+ }, 2500);
422
+ this.publish();
423
+ }
319
424
  async save() {
320
425
  const plan = this.plan();
321
426
  const writes = plan.flatMap((item) => item.run === void 0 ? [] : [item.run]);
322
- if (plan.length === 0 || this.saving || writes.length !== plan.length) return;
427
+ if (this.saving) return;
428
+ if (plan.length === 0) {
429
+ this.showToast("nothing");
430
+ return;
431
+ }
432
+ if (writes.length !== plan.length) {
433
+ this.showToast("invalid");
434
+ return;
435
+ }
323
436
  this.saving = true;
324
437
  this.failed = false;
325
438
  this.publish();
326
439
  let landed = true;
327
- for (const write of writes) landed = await write() && landed;
440
+ try {
441
+ for (const write of writes) landed = await write() && landed;
442
+ } catch (_saveFailure) {
443
+ landed = false;
444
+ }
328
445
  if (landed) this.staged.clear();
329
446
  this.saving = false;
330
447
  this.failed = !landed;
331
- this.publish();
448
+ this.showToast(landed ? "saved" : "failed");
332
449
  }
333
450
  actions() {
334
451
  return {
@@ -350,18 +467,26 @@ window.__ModuleLoader__.load({
350
467
  };
351
468
  }
352
469
  async clear(field) {
353
- await this.scope.unset(field);
354
- return !this.stored(field);
470
+ try {
471
+ await this.scope.unset(field);
472
+ return true;
473
+ } catch (_clearFailure) {
474
+ return false;
475
+ }
355
476
  }
356
477
  async store(field, value) {
357
- await this.scope.set(field, value);
358
- return this.userLayer()?.[field] === value;
478
+ try {
479
+ await this.scope.set(field, value);
480
+ return true;
481
+ } catch (_storeFailure) {
482
+ return false;
483
+ }
359
484
  }
360
485
  stage(field, edit) {
361
486
  this.staged.set(field, edit);
362
487
  this.failed = false;
363
488
  this.publish();
364
- if (field === "provider" || field === "apiKeyEnv" || field === "braveApiKeyEnv") this.readCredential();
489
+ if (field === "provider" || field === "apiKeyEnv" || field === "braveApiKeyEnv" || field === "deepseekApiKeyEnv") this.readCredential();
365
490
  }
366
491
  snapshotOf() {
367
492
  return this.scope.getSnapshot();
@@ -380,6 +505,7 @@ window.__ModuleLoader__.load({
380
505
  return user !== void 0 && Object.hasOwn(user, field);
381
506
  }
382
507
  publish() {
508
+ writeDraft(this);
383
509
  for (const listener of this.listeners) listener();
384
510
  }
385
511
  /** Draft-aware engine: staged `provider` wins over the saved section. */
@@ -481,7 +607,8 @@ window.__ModuleLoader__.load({
481
607
  maxTokens: this.form.field("maxTokens"),
482
608
  maxUses: this.form.field("maxUses"),
483
609
  apiKeyConfigured: this.form.credential.configured,
484
- apiKeyWritable: this.form.credential.writable
610
+ apiKeyWritable: this.form.credential.writable,
611
+ toastKey: this.form.toastKey
485
612
  };
486
613
  }
487
614
  inject() {
@@ -492,79 +619,398 @@ window.__ModuleLoader__.load({
492
619
  }
493
620
  };
494
621
 
495
- /** Row style: one labelled field with label, hint, and staged input. */
496
- const fieldRow = (label, hint, children) => react.createElement("div", { style: { display: "flex", flexDirection: "column", gap: "4px", padding: "12px 0", borderTop: "1px solid var(--dsw-alias-border-l2)" } },
497
- react.createElement("label", { style: { color: "var(--dsw-alias-label-primary)", fontSize: "13px", fontWeight: 500, lineHeight: "1.5" } }, label),
622
+ /** Row style: one labelled field with optional override badge and restore-default. */
623
+ const fieldRow = (label, hint, children, meta) => react.createElement("div", { className: "dshws_field" },
624
+ react.createElement("div", { className: "dshws_field_head" },
625
+ react.createElement("label", { className: "dshws_field_label" }, label),
626
+ meta?.overridden ? react.createElement("span", { className: "dshws_field_badges" },
627
+ react.createElement("span", { className: "dshws_badge" }, meta.t("overridden")),
628
+ react.createElement("button", { type: "button", className: "dshws_reset", disabled: meta.disabled === true, onClick: meta.onReset }, meta.t("reset"))) : null),
498
629
  children,
499
- hint ? react.createElement("p", { style: { color: "var(--dsw-alias-label-tertiary)", margin: 0, fontSize: "12px", lineHeight: "1.5" } }, hint) : null);
500
- const textInput = (props) => react.createElement("input", { style: { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", height: "34px", font: "inherit", color: "var(--dsw-alias-label-primary)", borderRadius: "8px", padding: "0 12px", fontSize: "13px", lineHeight: "1.5" }, type: "text", ...props.numeric ? { inputMode: "numeric" } : {}, ...props });
501
- const secretInput = (props) => react.createElement("input", { style: { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", height: "34px", font: "inherit", color: "var(--dsw-alias-label-primary)", borderRadius: "8px", padding: "0 12px", fontSize: "13px", lineHeight: "1.5" }, type: "password", autoComplete: "off", ...props });
502
- const selectInput = (props) => react.createElement("select", { style: { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", height: "34px", font: "inherit", color: "var(--dsw-alias-label-primary)", borderRadius: "8px", padding: "0 12px", fontSize: "13px", lineHeight: "1.5" }, ...props });
630
+ meta?.invalid ? react.createElement("p", { className: "dshws_field_error" }, meta.t("invalidNumber")) : hint ? react.createElement("p", { className: "dshws_field_hint" }, hint) : null);
631
+ const pair = (left, right) => {
632
+ if (!left && !right) return null;
633
+ if (!left) return right;
634
+ if (!right) return left;
635
+ return react.createElement("div", { className: "dshws_pair" }, left, right);
636
+ };
637
+ const controlStyle = { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", height: "34px", font: "inherit", color: "var(--dsw-alias-label-primary)", borderRadius: "8px", padding: "0 12px", fontSize: "13px", lineHeight: "1.5", width: "100%", boxSizing: "border-box" };
638
+ const textInput = (props) => react.createElement("input", { style: controlStyle, type: "text", ...props });
639
+ const numberInput = (props) => react.createElement("input", {
640
+ style: controlStyle,
641
+ type: "text",
642
+ inputMode: "numeric",
643
+ pattern: "[0-9]*",
644
+ autoComplete: "off",
645
+ ...props,
646
+ onChange: (event) => {
647
+ props.onChange?.({ target: { value: String(event.target.value ?? "").replace(/\D/g, "") } });
648
+ }
649
+ });
650
+ const secretInput = (props) => react.createElement("input", { style: controlStyle, type: "password", autoComplete: "off", ...props });
651
+ const selectInput = (props) => react.createElement("select", { style: controlStyle, ...props });
503
652
  const checkboxInput = (props) => react.createElement("input", { type: "checkbox", style: { width: "16px", height: "16px", accentColor: "var(--dsw-alias-brand-primary)" }, ...props });
504
653
 
654
+ const CSS_ID = "dsh-web-search-plugin/styles.css#maxresults";
655
+ const GLOBE_MASK = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14zM2.1 8.75h2.55c.12 1.58.48 3 1 4.12A6.02 6.02 0 0 1 2.1 8.75zm8.25 4.12c.52-1.12.88-2.54 1-4.12H13.9a6.02 6.02 0 0 1-3.55 4.12zM13.9 7.25h-2.55c-.12-1.58-.48-3-1-4.12A6.02 6.02 0 0 1 13.9 7.25zM5.65 3.13c-.52 1.12-.88 2.54-1 4.12H2.1a6.02 6.02 0 0 1 3.55-4.12zM8 2.2c.82 1.12 1.36 2.82 1.5 4.95H6.5C6.64 5.02 7.18 3.32 8 2.2zm0 11.6c-.82-1.12-1.36-2.82-1.5-4.95h3C9.36 10.98 8.82 12.68 8 13.8z'/%3E%3C/svg%3E\") 50%/contain no-repeat";
656
+ if (typeof document !== "undefined") {
657
+ for (const old of document.querySelectorAll('style[data-plugin="dsh-web-search-plugin"]')) old.remove();
658
+ const tag = document.createElement("style");
659
+ tag.dataset.plugin = "dsh-web-search-plugin";
660
+ tag.dataset.pluginCss = CSS_ID;
661
+ tag.textContent = [
662
+ "@keyframes dshws-toast-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}",
663
+ ".dshws_quota{border:1px solid var(--dsw-alias-border-l3,rgba(128,128,128,0.12));background:var(--dsw-alias-bg-layer-2,rgba(128,128,128,0.04));border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:6px;margin:4px 0 8px}",
664
+ ".dshws_quota_head{display:flex;align-items:baseline;justify-content:space-between;gap:8px}",
665
+ ".dshws_quota_title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary)}",
666
+ ".dshws_quota_pct{font-variant-numeric:tabular-nums;font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);background:transparent;border:none;padding:0;font:inherit;font-weight:600;cursor:pointer}",
667
+ ".dshws_quota_pct:disabled{cursor:wait;opacity:.7}",
668
+ ".dshws_quota_track{height:6px;border-radius:999px;background:var(--dsw-alias-bg-layer-1,rgba(128,128,128,0.14));overflow:hidden}",
669
+ ".dshws_quota_fill{height:100%;border-radius:999px;background:var(--dsw-alias-state-success-primary,#10b981);transition:width .2s ease}",
670
+ ".dshws_quota_fill_warning{background:var(--dsw-alias-state-warn-primary,var(--dsw-alias-state-warning-primary,#f59e0b))}",
671
+ ".dshws_quota_fill_danger{background:var(--dsw-alias-state-error-primary,#ef4444)}",
672
+ ".dshws_quota_meta{display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap;font-size:11px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
673
+ ".dshws_quota_hint{font-size:11px;color:var(--dsw-alias-label-tertiary);line-height:1.45;margin:0}",
674
+ ".dshws_quota_alert{font-size:11.5px;margin:0;line-height:1.45}",
675
+ ".dshws_quota_alert_warning{color:var(--dsw-alias-state-warn-primary,#f59e0b)}",
676
+ ".dshws_quota_alert_danger{color:var(--dsw-alias-state-error-primary,#ef4444)}",
677
+ ".dshws_btn{appearance:none;font:inherit;cursor:pointer;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5;display:inline-flex;align-items:center;justify-content:center;transition:filter .15s ease,opacity .15s ease}",
678
+ ".dshws_btn:disabled{cursor:not-allowed;opacity:.45}",
679
+ ".dshws_btn_primary{border:1px solid transparent;color:var(--dsw-alias-label-on-brand,#fff);background:var(--dsw-alias-bg-brand-solid,var(--dsw-alias-brand-primary,#3b82f6));font-weight:600}",
680
+ ".dshws_btn_primary:hover:not(:disabled){filter:brightness(1.08)}",
681
+ ".dshws_btn_outline{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:transparent}",
682
+ ".dshws_toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--dsw-alias-state-success-primary,#10b981);color:#fff;padding:8px 18px;border-radius:999px;box-shadow:var(--dsw-shadow-lv3,0 8px 24px rgba(0,0,0,.3));font-size:12.5px;font-weight:500;z-index:100000;animation:dshws-toast-in .2s ease-out}",
683
+ ".dshws_toast_error{background:var(--dsw-alias-state-error-primary,#ef4444)}",
684
+ ".dshws_toast_info{background:var(--dsw-alias-label-secondary,#6b7280)}",
685
+ ".dshws_card_header{display:flex;align-items:flex-start;gap:12px;padding:6px 0 2px}",
686
+ ".dshws_card_headText{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}",
687
+ ".dshws_card_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}",
688
+ ".dshws_card_desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}",
689
+ ".dshws_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform,rgba(128,128,128,0.12));color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}",
690
+ ".dshws_form{display:flex;flex-direction:column;gap:0}",
691
+ ".dshws_field{display:flex;flex-direction:column;gap:4px;padding:10px 0;border-top:1px solid var(--dsw-alias-border-l2);min-width:0}",
692
+ ".dshws_field_head{display:flex;align-items:center;gap:8px}",
693
+ ".dshws_field_label{min-width:0;flex:1;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:1.5}",
694
+ ".dshws_field_badges{display:inline-flex;align-items:center;gap:8px;flex:none}",
695
+ ".dshws_field_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.45}",
696
+ ".dshws_field_error{color:var(--dsw-alias-state-error-primary,#ef4444)!important;margin:0;font-size:12px;line-height:1.45;font-weight:500}",
697
+ ".dshws_pair{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);column-gap:12px;border-top:1px solid var(--dsw-alias-border-l2)}",
698
+ ".dshws_pair>.dshws_field{border-top:none;padding:10px 0}",
699
+ ".dshws_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform,rgba(128,128,128,0.12));color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}",
700
+ ".dshws_reset{appearance:none;font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:transparent;border:none;padding:0;font-size:12px;line-height:1.5}",
701
+ ".dshws_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}",
702
+ ".dshws_reset:disabled{cursor:default;opacity:.45}",
703
+ ".dshws_input_invalid{border-color:var(--dsw-alias-state-error-primary,#ef4444)!important}",
704
+ "[data-dshws-nav]>[class*='_navIcon']{display:none}",
705
+ "[data-dshws-nav]:before{content:'';background:currentColor;flex:none;width:16px;height:16px;-webkit-mask:" + GLOBE_MASK + ";mask:" + GLOBE_MASK + "}"
706
+ ].join("\n");
707
+ document.head.appendChild(tag);
708
+ }
709
+
710
+ function interpolate(template, vars) {
711
+ return String(template ?? "").replace(/\{(\w+)\}/g, (_, key) => vars[key] == null ? "" : String(vars[key]));
712
+ }
713
+ function tFill(t, key, vars) {
714
+ return interpolate(t(key), vars);
715
+ }
716
+ function formatResetTime(ms) {
717
+ if (!Number.isFinite(ms) || ms <= 0) return "—";
718
+ const date = new Date(ms);
719
+ return Number.isNaN(date.getTime()) ? "—" : date.toLocaleString();
720
+ }
721
+ function usedPercent(used, limit) {
722
+ if (!Number.isFinite(limit) || limit <= 0 || !Number.isFinite(used)) return 0;
723
+ return Math.max(0, Math.min(100, (100 * used) / limit));
724
+ }
725
+ function remainingLevel(used, limit) {
726
+ if (!Number.isFinite(limit) || limit <= 0) return "unknown";
727
+ const remainingPct = Math.max(0, (100 * (limit - used)) / limit);
728
+ if (remainingPct <= 10) return "danger";
729
+ if (remainingPct <= 20) return "warning";
730
+ return "success";
731
+ }
732
+
733
+ const USAGE_POLL_MS = 15000;
734
+ function createUsageClientStore() {
735
+ let snap = { status: "idle", payload: null, refreshing: false };
736
+ const listeners = new Set();
737
+ const publish = () => {
738
+ for (const listener of listeners) listener();
739
+ };
740
+ const load = async (force) => {
741
+ if (force) {
742
+ snap = { ...snap, refreshing: true };
743
+ publish();
744
+ }
745
+ try {
746
+ const response = await fetch(force ? "/dsh-web-search/usage?force=1" : "/dsh-web-search/usage", { cache: "no-store" });
747
+ const payload = await response.json();
748
+ snap = { status: "ok", payload, refreshing: false };
749
+ } catch (error) {
750
+ snap = { status: "error", payload: snap.payload, refreshing: false, error: String(error?.message ?? error) };
751
+ }
752
+ publish();
753
+ return snap;
754
+ };
755
+ return {
756
+ subscribe(listener) {
757
+ listeners.add(listener);
758
+ return () => listeners.delete(listener);
759
+ },
760
+ getSnapshot() {
761
+ return snap;
762
+ },
763
+ refresh: () => load(true),
764
+ start() {
765
+ void load(true);
766
+ const timer = setInterval(() => {
767
+ void load(false);
768
+ }, USAGE_POLL_MS);
769
+ return () => clearInterval(timer);
770
+ }
771
+ };
772
+ }
773
+ const usageStore = createUsageClientStore();
774
+
775
+ function QuotaBar(props) {
776
+ const { name, used, limit, remainingText, resetText, level, onRefresh, refreshing, t, extra } = props;
777
+ const pct = usedPercent(used, limit);
778
+ const hasLimit = Number.isFinite(limit) && limit > 0;
779
+ const fillClass = "dshws_quota_fill" + (level === "danger" ? " dshws_quota_fill_danger" : level === "warning" ? " dshws_quota_fill_warning" : "");
780
+ return react.createElement("div", { className: "dshws_quota" },
781
+ react.createElement("div", { className: "dshws_quota_head" },
782
+ react.createElement("span", { className: "dshws_quota_title" }, name),
783
+ react.createElement("button", {
784
+ type: "button",
785
+ className: "dshws_quota_pct",
786
+ disabled: refreshing,
787
+ title: refreshing ? t("quotaRefreshing") : t("quotaRefresh"),
788
+ onClick: onRefresh
789
+ }, hasLimit ? Math.round(pct) + "%" : (Number.isFinite(used) && used > 0 ? String(used) : "—"))),
790
+ react.createElement("div", { className: "dshws_quota_track" },
791
+ react.createElement("div", { className: fillClass, style: { width: (hasLimit ? pct : 0) + "%" } })),
792
+ react.createElement("div", { className: "dshws_quota_meta" },
793
+ react.createElement("span", null, remainingText),
794
+ react.createElement("span", null, resetText)),
795
+ level === "warning" || level === "danger"
796
+ ? react.createElement("p", { className: "dshws_quota_alert dshws_quota_alert_" + level }, t(level === "danger" ? "quotaDanger" : "quotaWarn"))
797
+ : null,
798
+ extra);
799
+ }
800
+
801
+ function QuotaCard(props) {
802
+ const { t, provider, keyed } = props;
803
+ const usage = react.useSyncExternalStore(usageStore.subscribe, usageStore.getSnapshot, usageStore.getSnapshot);
804
+ const showTavily = provider === "tavily" && keyed;
805
+ const showBrave = provider === "brave";
806
+ if (!showTavily && !showBrave) return null;
807
+ const payload = usage.payload;
808
+ const refreshing = usage.refreshing === true;
809
+ const onRefresh = (event) => {
810
+ event.preventDefault();
811
+ void usageStore.refresh();
812
+ };
813
+ if (showTavily) {
814
+ const info = payload?.tavily;
815
+ const error = info?.error || (usage.status === "error" ? usage.error : null);
816
+ if (!info || info.fetchedAt === 0 && info.used === 0 && !error) {
817
+ return react.createElement("div", { className: "dshws_quota" },
818
+ react.createElement("div", { className: "dshws_quota_head" },
819
+ react.createElement("span", { className: "dshws_quota_title" }, t("quotaTitle")),
820
+ react.createElement("button", { type: "button", className: "dshws_quota_pct", disabled: refreshing, onClick: onRefresh }, refreshing ? t("quotaRefreshing") : t("quotaRefresh"))),
821
+ react.createElement("p", { className: "dshws_quota_hint" }, t("quotaEmpty")),
822
+ react.createElement("p", { className: "dshws_quota_hint" }, t("quotaKeyedHint")));
823
+ }
824
+ const used = Number(info.used) || 0;
825
+ const limit = info.limit;
826
+ const remaining = info.remaining;
827
+ const level = remainingLevel(used, limit);
828
+ const name = info.plan ? tFill(t, "quotaPlan", { plan: info.plan }) : t("quotaTitle");
829
+ const remainingText = Number.isFinite(limit) && limit > 0
830
+ ? tFill(t, "quotaUsed", { used, limit }) + " · " + tFill(t, "quotaRemaining", { remaining: remaining ?? Math.max(0, limit - used) })
831
+ : used > 0
832
+ ? tFill(t, "quotaUsedOnly", { used }) + " · " + t("quotaUnlimited")
833
+ : t("quotaUnlimited");
834
+ return react.createElement(QuotaBar, {
835
+ name,
836
+ used,
837
+ limit,
838
+ remainingText,
839
+ resetText: t("quotaCycle"),
840
+ level,
841
+ onRefresh,
842
+ refreshing,
843
+ t,
844
+ extra: [
845
+ info.paygoUsed > 0 ? react.createElement("p", { className: "dshws_quota_hint", key: "paygo" }, tFill(t, "quotaPaygo", { used: info.paygoUsed, limit: info.paygoLimit ?? "—" })) : null,
846
+ error ? react.createElement("p", { className: "dshws_quota_hint", key: "err" }, tFill(t, "quotaError", { error })) : react.createElement("p", { className: "dshws_quota_hint", key: "hint" }, t("quotaKeyedHint"))
847
+ ]
848
+ });
849
+ }
850
+ const brave = payload?.brave;
851
+ const monthly = brave?.monthly;
852
+ const burst = brave?.burst;
853
+ if (!monthly && !burst) {
854
+ return react.createElement("div", { className: "dshws_quota" },
855
+ react.createElement("span", { className: "dshws_quota_title" }, t("quotaTitle")),
856
+ react.createElement("p", { className: "dshws_quota_hint" }, t("quotaEmpty")));
857
+ }
858
+ const monthlyCapped = Number.isFinite(monthly?.limit) && monthly.limit > 0;
859
+ const primary = monthlyCapped ? monthly : (burst ?? monthly);
860
+ const used = Number(primary.used) || 0;
861
+ const limit = primary.limit;
862
+ const remaining = primary.remaining;
863
+ const level = remainingLevel(used, limit);
864
+ return react.createElement(QuotaBar, {
865
+ name: monthlyCapped ? t("quotaTitle") : t("quotaBraveCapacity"),
866
+ used,
867
+ limit,
868
+ remainingText: monthlyCapped
869
+ ? tFill(t, "quotaUsed", { used, limit }) + " · " + tFill(t, "quotaRemaining", { remaining: remaining ?? Math.max(0, limit - used) })
870
+ : tFill(t, "quotaBraveRps", { limit: limit ?? 0 }) + " · " + tFill(t, "quotaRemaining", { remaining: remaining ?? 0 }),
871
+ resetText: monthlyCapped
872
+ ? tFill(t, "quotaResets", { time: formatResetTime(monthly.resetAt) })
873
+ : t("quotaBraveUnlimited") + (monthly?.resetAt ? " · " + tFill(t, "quotaResets", { time: formatResetTime(monthly.resetAt) }) : ""),
874
+ level,
875
+ onRefresh,
876
+ refreshing,
877
+ t,
878
+ extra: monthlyCapped && burst
879
+ ? react.createElement("p", { className: "dshws_quota_hint", key: "burst" }, tFill(t, "quotaBurst", { remaining: burst.remaining, limit: burst.limit }))
880
+ : null
881
+ });
882
+ }
883
+
884
+ function toastCopy(t, key) {
885
+ if (key === "saved") return t("savedToast");
886
+ if (key === "nothing") return t("nothingToSave");
887
+ if (key === "invalid") return t("invalidToast");
888
+ if (key === "failed") return t("saveFailed");
889
+ return "";
890
+ }
891
+
505
892
  /** Render the provider selector plus the backend-specific staged form. */
506
893
  function WebSearchCard(props) {
507
894
  const { t, state, actions } = props;
508
895
  if (!state.available) return null;
509
896
  const disabled = !state.writable;
510
- const blocked = !state.dirty || state.invalid || state.saving;
511
897
  const provider = state.provider.text === "brave" ? "brave" : state.provider.text === "deepseek-official" ? "deepseek-official" : "tavily";
512
898
  const brave = provider === "brave";
513
899
  const deepseek = provider === "deepseek-official";
514
900
  const keyed = state.mode.text === "keyed";
515
- return react.createElement("div", { style: { display: "flex", flexDirection: "column", gap: "4px" } },
516
- !state.writable ? react.createElement("p", { style: { color: "var(--dsw-alias-label-tertiary)", margin: "0", fontSize: "12px", lineHeight: "1.5" } }, t("readOnly")) : null,
517
- fieldRow(t("provider"), null, selectInput({ id: "ws-provider", value: provider, disabled, onChange: (e) => actions.edit("provider", e.target.value), children: [
518
- react.createElement("option", { value: "tavily", key: "tavily" }, t("providerTavily")),
519
- react.createElement("option", { value: "brave", key: "brave" }, t("providerBrave")),
520
- react.createElement("option", { value: "deepseek-official", key: "deepseek-official" }, t("providerDeepseek"))
521
- ] })),
522
- deepseek ? null : brave ? null : fieldRow(t("mode"), null, selectInput({ id: "ws-mode", value: state.mode.text, disabled, onChange: (e) => actions.edit("mode", e.target.value), children: [
523
- react.createElement("option", { value: "keyless", key: "keyless" }, t("modeKeyless")),
524
- react.createElement("option", { value: "keyed", key: "keyed" }, t("modeKeyed"))
525
- ] })),
526
- // API key + credential reference for the visible backend.
527
- deepseek ? fieldRow(t("apiKey"), t("apiKeyHint"), secretInput({ id: "ws-deepseek-key", value: state.deepseekApiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(DEEPSEEK_API_KEY_FIELD, e.target.value) })) : null,
528
- deepseek ? fieldRow(t("deepseekApiKeyEnv"), t("deepseekApiKeyEnvHint"), textInput({ id: "ws-deepseek-keyref", value: state.deepseekApiKeyEnv.text, disabled, onChange: (e) => actions.edit("deepseekApiKeyEnv", e.target.value), onReset: () => actions.resetField("deepseekApiKeyEnv") })) : null,
529
- brave ? fieldRow(t("apiKey"), t("apiKeyHint"), secretInput({ id: "ws-brave-key", value: state.braveApiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(BRAVE_API_KEY_FIELD, e.target.value) })) : null,
530
- brave ? fieldRow(t("braveApiKeyEnv"), t("braveApiKeyEnvHint"), textInput({ id: "ws-brave-keyref", value: state.braveApiKeyEnv.text, disabled, onChange: (e) => actions.edit("braveApiKeyEnv", e.target.value), onReset: () => actions.resetField("braveApiKeyEnv") })) : null,
531
- (!brave && !deepseek && keyed) ? fieldRow(t("apiKey"), t("apiKeyHint"), secretInput({ id: "ws-tavily-key", value: state.apiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(TAVILY_API_KEY_FIELD, e.target.value) })) : null,
532
- (!brave && !deepseek && keyed) ? fieldRow(t("apiKeyEnv"), t("apiKeyEnvHint"), textInput({ id: "ws-tavily-keyref", value: state.apiKeyEnv.text, disabled, onChange: (e) => actions.edit("apiKeyEnv", e.target.value), onReset: () => actions.resetField("apiKeyEnv") })) : null,
533
- // Endpoint.
534
- deepseek ? fieldRow(t("deepseekBaseURL"), t("deepseekBaseURLHint"), textInput({ id: "ws-deepseek-base", value: state.deepseekBaseURL.text, disabled, onChange: (e) => actions.edit("deepseekBaseURL", e.target.value), onReset: () => actions.resetField("deepseekBaseURL") })) : null,
535
- brave ? fieldRow(t("braveBaseURL"), t("braveBaseURLHint"), textInput({ id: "ws-brave-base", value: state.braveBaseURL.text, disabled, onChange: (e) => actions.edit("braveBaseURL", e.target.value), onReset: () => actions.resetField("braveBaseURL") })) : null,
536
- (!brave && !deepseek) ? fieldRow(t("baseURL"), t("baseURLHint"), textInput({ id: "ws-tavily-base", value: state.baseURL.text, disabled, onChange: (e) => actions.edit("baseURL", e.target.value), onReset: () => actions.resetField("baseURL") })) : null,
537
- // DeepSeek options.
538
- deepseek ? fieldRow(t("model"), t("modelHint"), textInput({ id: "ws-model", value: state.model.text, disabled, onChange: (e) => actions.edit("model", e.target.value), onReset: () => actions.resetField("model") })) : null,
539
- deepseek ? fieldRow(t("apiVersion"), t("apiVersionHint"), textInput({ id: "ws-apiversion", value: state.apiVersion.text, disabled, onChange: (e) => actions.edit("apiVersion", e.target.value), onReset: () => actions.resetField("apiVersion") })) : null,
540
- deepseek ? fieldRow(t("maxTokens"), t("maxTokensHint"), textInput({ id: "ws-maxtokens", numeric: true, value: state.maxTokens.text, disabled, onChange: (e) => actions.edit("maxTokens", e.target.value), onReset: () => actions.resetField("maxTokens") })) : null,
541
- deepseek ? fieldRow(t("maxUses"), t("maxUsesHint"), textInput({ id: "ws-maxuses", numeric: true, value: state.maxUses.text, disabled, onChange: (e) => actions.edit("maxUses", e.target.value), onReset: () => actions.resetField("maxUses") })) : null,
542
- // Tavily options.
543
- (!brave && !deepseek) ? fieldRow(t("searchDepth"), null, selectInput({ id: "ws-depth", value: state.searchDepth.text, disabled, onChange: (e) => actions.edit("searchDepth", e.target.value), children: [
544
- react.createElement("option", { value: "basic", key: "basic" }, t("searchDepthBasic")),
545
- react.createElement("option", { value: "advanced", key: "advanced" }, t("searchDepthAdvanced"))
546
- ] })) : null,
547
- (!brave && !deepseek) ? fieldRow(t("topic"), null, selectInput({ id: "ws-topic", value: state.topic.text, disabled, onChange: (e) => actions.edit("topic", e.target.value), children: [
548
- react.createElement("option", { value: "general", key: "general" }, t("topicGeneral")),
549
- react.createElement("option", { value: "news", key: "news" }, t("topicNews"))
550
- ] })) : null,
551
- (!brave && !deepseek) ? fieldRow(t("includeAnswer"), t("includeAnswerHint"), checkboxInput({ id: "ws-answer", checked: state.includeAnswer.text === "true", disabled, onChange: (e) => actions.edit("includeAnswer", e.target.checked ? "true" : "") })) : null,
552
- // Brave options.
553
- brave ? fieldRow(t("country"), t("countryHint"), textInput({ id: "ws-country", value: state.country.text, disabled, onChange: (e) => actions.edit("country", e.target.value), onReset: () => actions.resetField("country") })) : null,
554
- brave ? fieldRow(t("searchLang"), t("searchLangHint"), textInput({ id: "ws-lang", value: state.searchLang.text, disabled, onChange: (e) => actions.edit("searchLang", e.target.value), onReset: () => actions.resetField("searchLang") })) : null,
555
- brave ? fieldRow(t("freshness"), null, selectInput({ id: "ws-freshness", value: state.freshness.text, disabled, onChange: (e) => actions.edit("freshness", e.target.value), children: [
556
- react.createElement("option", { value: "", key: "any" }, t("freshnessAny")),
557
- react.createElement("option", { value: "pd", key: "pd" }, t("freshnessDay")),
558
- react.createElement("option", { value: "pw", key: "pw" }, t("freshnessWeek")),
559
- react.createElement("option", { value: "pm", key: "pm" }, t("freshnessMonth")),
560
- react.createElement("option", { value: "py", key: "py" }, t("freshnessYear"))
561
- ] })) : null,
562
- brave ? fieldRow(t("proxy"), t("proxyHint"), textInput({ id: "ws-proxy", value: state.proxy.text, disabled, onChange: (e) => actions.edit("proxy", e.target.value), onReset: () => actions.resetField("proxy") })) : null,
563
- fieldRow(t("maxResults"), t("maxResultsHint"), textInput({ id: "ws-max", numeric: true, value: state.maxResults.text, disabled, onChange: (e) => actions.edit("maxResults", e.target.value), onReset: () => actions.resetField("maxResults") })),
901
+ const toastText = toastCopy(t, state.toastKey);
902
+ const toastTone = state.toastKey === "failed" || state.toastKey === "invalid" ? "error" : state.toastKey === "nothing" ? "info" : "success";
903
+ const row = (field, label, hint, children) => fieldRow(label, hint, children, {
904
+ t,
905
+ overridden: state[field]?.overridden === true,
906
+ invalid: state[field]?.invalid === true,
907
+ disabled,
908
+ onReset: () => actions.resetField(field)
909
+ });
910
+ const maxResultsWrite = numberField("maxResults", { min: 1, max: MAX_RESULTS_CAP }).parse(String(state.maxResults.text ?? "").trim());
911
+ const maxResultsValue = maxResultsWrite?.kind === "set" ? String(maxResultsWrite.value) : "8";
912
+ const maxResultsRow = row("maxResults", t("maxResults"), null, selectInput({
913
+ id: "ws-max",
914
+ value: maxResultsValue,
915
+ disabled,
916
+ className: state.maxResults.invalid ? "dshws_input_invalid" : void 0,
917
+ onChange: (e) => actions.edit("maxResults", e.target.value),
918
+ children: Array.from({ length: MAX_RESULTS_CAP }, (_, index) => {
919
+ const n = String(index + 1);
920
+ return react.createElement("option", { value: n, key: n }, n);
921
+ })
922
+ }));
923
+ return react.createElement("div", { className: "dshws_form" },
924
+ react.createElement("div", { className: "dshws_card_header" },
925
+ react.createElement("div", { className: "dshws_card_headText" },
926
+ react.createElement("span", { className: "dshws_card_name" }, t("nav"))),
927
+ state.dirty || state.failed ? react.createElement("span", { className: "dshws_pending" }, t("unsaved")) : null),
928
+ !state.writable ? react.createElement("p", { className: "dshws_field_hint", style: { padding: "8px 0 0" } }, t("readOnly")) : null,
929
+ pair(
930
+ row("provider", t("provider"), null, selectInput({ id: "ws-provider", value: provider, disabled, onChange: (e) => actions.edit("provider", e.target.value), children: [
931
+ react.createElement("option", { value: "tavily", key: "tavily" }, t("providerTavily")),
932
+ react.createElement("option", { value: "brave", key: "brave" }, t("providerBrave")),
933
+ react.createElement("option", { value: "deepseek-official", key: "deepseek-official" }, t("providerDeepseek"))
934
+ ] })),
935
+ deepseek || brave ? null : row("mode", t("mode"), null, selectInput({ id: "ws-mode", value: state.mode.text, disabled, onChange: (e) => actions.edit("mode", e.target.value), children: [
936
+ react.createElement("option", { value: "keyless", key: "keyless" }, t("modeKeyless")),
937
+ react.createElement("option", { value: "keyed", key: "keyed" }, t("modeKeyed"))
938
+ ] }))
939
+ ),
940
+ react.createElement(QuotaCard, { t, provider, keyed }),
941
+ deepseek ? pair(
942
+ fieldRow(t("apiKey"), t("apiKeyHint"), secretInput({ id: "ws-deepseek-key", value: state.deepseekApiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(DEEPSEEK_API_KEY_FIELD, e.target.value) })),
943
+ row("deepseekApiKeyEnv", t("deepseekApiKeyEnv"), t("deepseekApiKeyEnvHint"), textInput({ id: "ws-deepseek-keyref", value: state.deepseekApiKeyEnv.text, disabled, onChange: (e) => actions.edit("deepseekApiKeyEnv", e.target.value) }))
944
+ ) : null,
945
+ brave ? pair(
946
+ fieldRow(t("apiKey"), t("apiKeyHint"), secretInput({ id: "ws-brave-key", value: state.braveApiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(BRAVE_API_KEY_FIELD, e.target.value) })),
947
+ row("braveApiKeyEnv", t("braveApiKeyEnv"), t("braveApiKeyEnvHint"), textInput({ id: "ws-brave-keyref", value: state.braveApiKeyEnv.text, disabled, onChange: (e) => actions.edit("braveApiKeyEnv", e.target.value) }))
948
+ ) : null,
949
+ (!brave && !deepseek && keyed) ? pair(
950
+ fieldRow(t("apiKey"), t("apiKeyHint"), secretInput({ id: "ws-tavily-key", value: state.apiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(TAVILY_API_KEY_FIELD, e.target.value) })),
951
+ row("apiKeyEnv", t("apiKeyEnv"), t("apiKeyEnvHint"), textInput({ id: "ws-tavily-keyref", value: state.apiKeyEnv.text, disabled, onChange: (e) => actions.edit("apiKeyEnv", e.target.value) }))
952
+ ) : null,
953
+ deepseek ? row("deepseekBaseURL", t("deepseekBaseURL"), t("deepseekBaseURLHint"), textInput({ id: "ws-deepseek-base", value: state.deepseekBaseURL.text, disabled, onChange: (e) => actions.edit("deepseekBaseURL", e.target.value) })) : null,
954
+ brave ? row("braveBaseURL", t("braveBaseURL"), t("braveBaseURLHint"), textInput({ id: "ws-brave-base", value: state.braveBaseURL.text, disabled, onChange: (e) => actions.edit("braveBaseURL", e.target.value) })) : null,
955
+ (!brave && !deepseek) ? row("baseURL", t("baseURL"), t("baseURLHint"), textInput({ id: "ws-tavily-base", value: state.baseURL.text, disabled, onChange: (e) => actions.edit("baseURL", e.target.value) })) : null,
956
+ deepseek ? pair(
957
+ row("model", t("model"), t("modelHint"), textInput({ id: "ws-model", value: state.model.text, disabled, onChange: (e) => actions.edit("model", e.target.value) })),
958
+ row("apiVersion", t("apiVersion"), t("apiVersionHint"), textInput({ id: "ws-apiversion", value: state.apiVersion.text, disabled, onChange: (e) => actions.edit("apiVersion", e.target.value) }))
959
+ ) : null,
960
+ deepseek ? pair(
961
+ row("maxTokens", t("maxTokens"), t("maxTokensHint"), numberInput({ id: "ws-maxtokens", value: state.maxTokens.text, disabled, onChange: (e) => actions.edit("maxTokens", e.target.value), className: state.maxTokens.invalid ? "dshws_input_invalid" : void 0 })),
962
+ row("maxUses", t("maxUses"), t("maxUsesHint"), numberInput({ id: "ws-maxuses", value: state.maxUses.text, disabled, onChange: (e) => actions.edit("maxUses", e.target.value), className: state.maxUses.invalid ? "dshws_input_invalid" : void 0 }))
963
+ ) : null,
964
+ (!brave && !deepseek) ? pair(
965
+ row("searchDepth", t("searchDepth"), null, selectInput({ id: "ws-depth", value: state.searchDepth.text, disabled, onChange: (e) => actions.edit("searchDepth", e.target.value), children: [
966
+ react.createElement("option", { value: "basic", key: "basic" }, t("searchDepthBasic")),
967
+ react.createElement("option", { value: "advanced", key: "advanced" }, t("searchDepthAdvanced"))
968
+ ] })),
969
+ row("topic", t("topic"), null, selectInput({ id: "ws-topic", value: state.topic.text, disabled, onChange: (e) => actions.edit("topic", e.target.value), children: [
970
+ react.createElement("option", { value: "general", key: "general" }, t("topicGeneral")),
971
+ react.createElement("option", { value: "news", key: "news" }, t("topicNews"))
972
+ ] }))
973
+ ) : null,
974
+ (!brave && !deepseek) ? pair(
975
+ row("includeAnswer", t("includeAnswer"), t("includeAnswerHint"), checkboxInput({ id: "ws-answer", checked: state.includeAnswer.text === "true", disabled, onChange: (e) => actions.edit("includeAnswer", e.target.checked ? "true" : "") })),
976
+ maxResultsRow
977
+ ) : null,
978
+ brave ? pair(
979
+ row("country", t("country"), t("countryHint"), textInput({ id: "ws-country", value: state.country.text, disabled, placeholder: t("braveDefaultPlaceholder"), onChange: (e) => actions.edit("country", e.target.value) })),
980
+ row("searchLang", t("searchLang"), t("searchLangHint"), textInput({ id: "ws-lang", value: state.searchLang.text, disabled, placeholder: t("braveDefaultPlaceholder"), onChange: (e) => actions.edit("searchLang", e.target.value) }))
981
+ ) : null,
982
+ brave ? pair(
983
+ row("freshness", t("freshness"), null, selectInput({ id: "ws-freshness", value: state.freshness.text, disabled, onChange: (e) => actions.edit("freshness", e.target.value), children: [
984
+ react.createElement("option", { value: "", key: "any" }, t("freshnessAny")),
985
+ react.createElement("option", { value: "pd", key: "pd" }, t("freshnessDay")),
986
+ react.createElement("option", { value: "pw", key: "pw" }, t("freshnessWeek")),
987
+ react.createElement("option", { value: "pm", key: "pm" }, t("freshnessMonth")),
988
+ react.createElement("option", { value: "py", key: "py" }, t("freshnessYear"))
989
+ ] })),
990
+ maxResultsRow
991
+ ) : null,
992
+ brave ? row("proxy", t("proxy"), null, textInput({ id: "ws-proxy", value: state.proxy.text, disabled, placeholder: t("proxyPlaceholder"), onChange: (e) => actions.edit("proxy", e.target.value) })) : null,
993
+ deepseek ? maxResultsRow : null,
564
994
  react.createElement("div", { style: { borderTop: "1px solid var(--dsw-alias-border-l2)", justifyContent: "flex-end", alignItems: "center", gap: "8px", padding: "12px 0 4px", display: "flex" } },
565
- state.failed ? react.createElement("p", { role: "status", style: { minWidth: 0, color: "var(--dsw-alias-label-error)", flex: 1, margin: 0, fontSize: "12px", lineHeight: "1.5" } }, t("saveFailed")) : null,
566
- react.createElement("button", { type: "button", disabled: !state.dirty || state.saving, onClick: actions.discard, style: { appearance: "none", font: "inherit", cursor: "pointer", border: "1px solid var(--dsw-alias-border-l2)", borderRadius: "8px", padding: "5px 14px", fontSize: "13px", lineHeight: "1.5", color: "var(--dsw-alias-label-secondary)", background: "0 0" } }, t("discard")),
567
- react.createElement("button", { type: "button", disabled: blocked, onClick: actions.save, style: { appearance: "none", font: "inherit", cursor: "pointer", border: "1px solid transparent", borderRadius: "8px", padding: "5px 14px", fontSize: "13px", lineHeight: "1.5", color: "var(--dsw-alias-label-on-brand)", background: "var(--dsw-alias-bg-brand-solid)" } }, t(state.saving ? "saving" : "save"))));
995
+ react.createElement("button", {
996
+ type: "button",
997
+ className: "dshws_btn dshws_btn_outline",
998
+ disabled: (!state.dirty && !state.failed) || state.saving,
999
+ onClick: (event) => {
1000
+ event.preventDefault();
1001
+ actions.discard();
1002
+ }
1003
+ }, t("discard")),
1004
+ react.createElement("button", {
1005
+ type: "button",
1006
+ className: "dshws_btn dshws_btn_primary",
1007
+ disabled: state.saving || !state.writable,
1008
+ onClick: (event) => {
1009
+ event.preventDefault();
1010
+ void actions.save();
1011
+ }
1012
+ }, t(state.saving ? "saving" : "save"))),
1013
+ toastText ? react.createElement("div", { className: "dshws_toast" + (toastTone === "error" ? " dshws_toast_error" : toastTone === "info" ? " dshws_toast_info" : ""), role: "status" }, toastText) : null);
568
1014
  }
569
1015
 
570
1016
  /** Section component: the "Web search" page body, driven by useSyncExternalStore. */
@@ -579,6 +1025,32 @@ window.__ModuleLoader__.load({
579
1025
  };
580
1026
  }
581
1027
 
1028
+ function syncSettingsNavIcon() {
1029
+ if (typeof document === "undefined" || typeof document.querySelectorAll !== "function") return;
1030
+ const lists = document.querySelectorAll("[class*='_navList']");
1031
+ for (const list of lists) {
1032
+ const cells = list.querySelectorAll(":scope > [class*='_navCell']");
1033
+ for (const cell of cells) {
1034
+ const text = String(cell.textContent ?? "").replace(/\s+/g, " ").trim();
1035
+ if (text === "网页搜索" || text === "Web search") cell.setAttribute("data-dshws-nav", "web-search");
1036
+ else cell.removeAttribute("data-dshws-nav");
1037
+ }
1038
+ }
1039
+ }
1040
+ function startSettingsNavIconSync() {
1041
+ if (typeof document === "undefined") return () => {};
1042
+ const run = () => {
1043
+ try { syncSettingsNavIcon(); } catch { /* settings sheet not mounted yet */ }
1044
+ };
1045
+ run();
1046
+ if (typeof MutationObserver !== "function") return () => {};
1047
+ const root = document.body || document.documentElement;
1048
+ if (!root) return () => {};
1049
+ const observer = new MutationObserver(run);
1050
+ observer.observe(root, { childList: true, subtree: true, characterData: true });
1051
+ return () => observer.disconnect();
1052
+ }
1053
+
582
1054
  /** Mount the top-level "Web search" settings section. */
583
1055
  function apply(ctx) {
584
1056
  const { api } = ctx.get("connection");
@@ -588,6 +1060,8 @@ window.__ModuleLoader__.load({
588
1060
  ctx.effect(() => ctx.remote.$on("credentials/updated", (ref) => {
589
1061
  controller.form.refreshCredential(ref);
590
1062
  }), "dsh-web-search-plugin: credential invalidations");
1063
+ ctx.effect(() => usageStore.start(), "dsh-web-search-plugin: usage poll");
1064
+ ctx.effect(() => startSettingsNavIconSync(), "dsh-web-search-plugin: settings nav icon");
591
1065
  ctx.slots.inject("settings.section", () => ctx.slots.register({
592
1066
  name: "settings.section",
593
1067
  id: "web-search",