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/CHANGELOG.md +26 -0
- package/README.md +52 -18
- package/lib/brave.js +1 -0
- package/lib/client.js +563 -89
- package/lib/index.js +99 -4
- package/lib/shared.js +1 -1
- package/lib/tavily.js +10 -2
- package/lib/usage.js +352 -0
- package/package.json +2 -2
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).
|
|
103
|
+
countryHint: "ISO 2-letter code (e.g. cn).",
|
|
77
104
|
searchLang: "Search language",
|
|
78
|
-
searchLangHint: "e.g. zh-hans or en.
|
|
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
|
-
|
|
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
|
|
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
|
|
195
|
+
countryHint: "ISO 两位码(如 cn)。",
|
|
144
196
|
searchLang: "搜索语言",
|
|
145
|
-
searchLangHint: "如 zh-hans、en
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
354
|
-
|
|
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
|
-
|
|
358
|
-
|
|
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
|
|
496
|
-
const fieldRow = (label, hint, children) => react.createElement("div", {
|
|
497
|
-
react.createElement("
|
|
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
|
-
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
(
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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",
|