sqs-dsh-better-input 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +27 -0
- package/README.md +218 -0
- package/assets/banner.png +0 -0
- package/cordis.patch.yml +6 -0
- package/lib/about.d.ts +65 -0
- package/lib/client/MicrophoneButton.d.ts +64 -0
- package/lib/client/OptimizeButton.d.ts +33 -0
- package/lib/client/VoiceRecognitionBar.d.ts +14 -0
- package/lib/client/index.d.ts +14 -0
- package/lib/client/settings-controller.d.ts +80 -0
- package/lib/client/settings.d.ts +15 -0
- package/lib/client/strings.d.ts +113 -0
- package/lib/client/templates-controller.d.ts +45 -0
- package/lib/client/templates-section.d.ts +17 -0
- package/lib/client/templates-source.d.ts +16 -0
- package/lib/client/voice-session.d.ts +35 -0
- package/lib/client/web-speech.d.ts +35 -0
- package/lib/client.d.ts +1 -0
- package/lib/client.js +2925 -0
- package/lib/client.js.map +1 -0
- package/lib/config.d.ts +100 -0
- package/lib/identity-CzaR1Taa.js +18 -0
- package/lib/identity-CzaR1Taa.js.map +1 -0
- package/lib/identity.d.ts +24 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +920 -0
- package/lib/index.js.map +1 -0
- package/lib/polish/prompts.d.ts +37 -0
- package/lib/polish/service.d.ts +45 -0
- package/lib/remote-contract.d.ts +166 -0
- package/lib/remote.d.ts +56 -0
- package/lib/remote.js +226 -0
- package/lib/remote.js.map +1 -0
- package/lib/settings/store.d.ts +41 -0
- package/lib/templates/model.d.ts +30 -0
- package/lib/templates/store.d.ts +27 -0
- package/lib/typert.d.ts +107 -0
- package/lib/typert.js +468 -0
- package/lib/typert.js.map +1 -0
- package/package.json +116 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,2925 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "sqs-dsh-better-input",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
let react_dom = require("react-dom");
|
|
10
|
+
//#region src/identity.ts
|
|
11
|
+
/**
|
|
12
|
+
* Single source of truth for this package's identity.
|
|
13
|
+
*
|
|
14
|
+
* Every externally visible name (the Typert package key, the npm package name
|
|
15
|
+
* used by the update check, the settings namespace) derives from here so a
|
|
16
|
+
* rename never leaves a stale literal behind.
|
|
17
|
+
*/
|
|
18
|
+
/** Stable identifier for this fork. Used as the Typert package key. */
|
|
19
|
+
const PACKAGE_NAME = "sqs-dsh-better-input";
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/remote.ts
|
|
22
|
+
/**
|
|
23
|
+
* Build one strict codec for the CLIENT face.
|
|
24
|
+
*
|
|
25
|
+
* The client face exists only to describe the wire shape to
|
|
26
|
+
* `ctx.remote.$mount`; the actual byte validation happens on the Host. This is
|
|
27
|
+
* not an assumption — it is how dsh 0.1.7 is wired:
|
|
28
|
+
*
|
|
29
|
+
* - `@deepseek-ai/dsh-api-gateway/lib/index.js` (Host) is the ONLY place that
|
|
30
|
+
* runs `codec.create().parse(value)`. Validation lives in `decode()`.
|
|
31
|
+
* - `…/lib/client.js` contains ZERO `.create()` calls. Its `$mount` path only
|
|
32
|
+
* calls `requireStrictCodecs`, which inspects `codec.mode === 'strict'` and
|
|
33
|
+
* never reads `typeSymbol` or invokes the factory. `prepareInvocation()`
|
|
34
|
+
* forwards raw argument values straight to the RPC carrier.
|
|
35
|
+
*
|
|
36
|
+
* So materializing a real schema in the browser is pure dead weight: it pulls
|
|
37
|
+
* all of zod (~280 KB) into `lib/client.js` for a factory that is never called.
|
|
38
|
+
* The `create()` here is therefore lazy AND schema-free — it never imports zod.
|
|
39
|
+
*
|
|
40
|
+
* The eager `import { …Schema } from './remote-contract.js'` that used to sit
|
|
41
|
+
* at the top of this file was the single value-reachability edge that dragged
|
|
42
|
+
* zod into the browser bundle, because `src/client/index.ts` needs
|
|
43
|
+
* `TYPERT_REMOTE` as a value.
|
|
44
|
+
*
|
|
45
|
+
* The placeholder satisfies `TypertSchema` ({ parse(value) }) structurally, so
|
|
46
|
+
* the client face still typechecks. It is unreachable at runtime; if a future
|
|
47
|
+
* dsh ever validates on the client, `parse` throws loudly instead of silently
|
|
48
|
+
* accepting bad bytes. The Host manifest in `typert.ts` keeps the real zod
|
|
49
|
+
* schemas.
|
|
50
|
+
*/
|
|
51
|
+
function codec(typeSymbol) {
|
|
52
|
+
return {
|
|
53
|
+
mode: "strict",
|
|
54
|
+
typeSymbol,
|
|
55
|
+
create: () => ({ parse(value) {
|
|
56
|
+
throw new Error(`sqs-dsh-better-input: client-face codec ${typeSymbol} was materialized. The client must never validate wire bytes (the Host does). If this fires, the client-face descriptor is being used for validation it should not own.`);
|
|
57
|
+
} })
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Type symbol for one of this plugin's own wire types. */
|
|
61
|
+
const symbol = (name) => `${PACKAGE_NAME}#${name}`;
|
|
62
|
+
/** The client-face Typert manifest; mirrors the Host face in `typert.ts`. */
|
|
63
|
+
const TYPERT_REMOTE = {
|
|
64
|
+
package: PACKAGE_NAME,
|
|
65
|
+
descriptors: [
|
|
66
|
+
{
|
|
67
|
+
id: `${PACKAGE_NAME}#betterInput/getSettings`,
|
|
68
|
+
service: "BetterInputPolish",
|
|
69
|
+
namespace: "betterInput",
|
|
70
|
+
method: "getSettings",
|
|
71
|
+
invocation: { kind: "direct" },
|
|
72
|
+
parameters: [],
|
|
73
|
+
result: codec(symbol("BetterInputSettingsView"))
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: `${PACKAGE_NAME}#betterInput/updateSettings`,
|
|
77
|
+
service: "BetterInputPolish",
|
|
78
|
+
namespace: "betterInput",
|
|
79
|
+
method: "updateSettings",
|
|
80
|
+
invocation: { kind: "direct" },
|
|
81
|
+
parameters: [{
|
|
82
|
+
name: "patch",
|
|
83
|
+
wire: "patch",
|
|
84
|
+
source: "json",
|
|
85
|
+
codec: codec(symbol("BetterInputSettingsPatch"))
|
|
86
|
+
}],
|
|
87
|
+
cancellation: { parameter: "signal" },
|
|
88
|
+
result: codec(symbol("BetterInputSettingsView"))
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: `${PACKAGE_NAME}#betterInput/listRoutes`,
|
|
92
|
+
service: "BetterInputPolish",
|
|
93
|
+
namespace: "betterInput",
|
|
94
|
+
method: "listRoutes",
|
|
95
|
+
invocation: { kind: "direct" },
|
|
96
|
+
parameters: [],
|
|
97
|
+
result: codec(symbol("PolishRoute[]"))
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: `${PACKAGE_NAME}#betterInput/resolveModelEfforts`,
|
|
101
|
+
service: "BetterInputPolish",
|
|
102
|
+
namespace: "betterInput",
|
|
103
|
+
method: "resolveModelEfforts",
|
|
104
|
+
invocation: { kind: "direct" },
|
|
105
|
+
parameters: [{
|
|
106
|
+
name: "provider",
|
|
107
|
+
wire: "provider",
|
|
108
|
+
source: "json",
|
|
109
|
+
codec: codec("string")
|
|
110
|
+
}, {
|
|
111
|
+
name: "model",
|
|
112
|
+
wire: "model",
|
|
113
|
+
source: "json",
|
|
114
|
+
codec: codec("string")
|
|
115
|
+
}],
|
|
116
|
+
result: codec(symbol("ResolveModelEffortsResult"))
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: `${PACKAGE_NAME}#betterInput/getAbout`,
|
|
120
|
+
service: "BetterInputPolish",
|
|
121
|
+
namespace: "betterInput",
|
|
122
|
+
method: "getAbout",
|
|
123
|
+
invocation: { kind: "direct" },
|
|
124
|
+
parameters: [],
|
|
125
|
+
result: codec(symbol("AboutInfo"))
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: `${PACKAGE_NAME}#betterInput/checkForUpdate`,
|
|
129
|
+
service: "BetterInputPolish",
|
|
130
|
+
namespace: "betterInput",
|
|
131
|
+
method: "checkForUpdate",
|
|
132
|
+
invocation: { kind: "direct" },
|
|
133
|
+
parameters: [],
|
|
134
|
+
cancellation: { parameter: "signal" },
|
|
135
|
+
result: codec(symbol("UpdateCheckResult"))
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: `${PACKAGE_NAME}#betterInput/polish`,
|
|
139
|
+
service: "BetterInputPolish",
|
|
140
|
+
namespace: "betterInput",
|
|
141
|
+
method: "polish",
|
|
142
|
+
invocation: { kind: "direct" },
|
|
143
|
+
parameters: [
|
|
144
|
+
{
|
|
145
|
+
name: "transcript",
|
|
146
|
+
wire: "transcript",
|
|
147
|
+
source: "json",
|
|
148
|
+
codec: codec("string")
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "provider",
|
|
152
|
+
wire: "provider",
|
|
153
|
+
source: "json",
|
|
154
|
+
codec: codec("string")
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: "model",
|
|
158
|
+
wire: "model",
|
|
159
|
+
source: "json",
|
|
160
|
+
codec: codec("string")
|
|
161
|
+
}
|
|
162
|
+
],
|
|
163
|
+
cancellation: { parameter: "signal" },
|
|
164
|
+
result: codec("string")
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: `${PACKAGE_NAME}#betterInput/optimize`,
|
|
168
|
+
service: "BetterInputPolish",
|
|
169
|
+
namespace: "betterInput",
|
|
170
|
+
method: "optimize",
|
|
171
|
+
invocation: { kind: "direct" },
|
|
172
|
+
parameters: [
|
|
173
|
+
{
|
|
174
|
+
name: "text",
|
|
175
|
+
wire: "text",
|
|
176
|
+
source: "json",
|
|
177
|
+
codec: codec("string")
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: "provider",
|
|
181
|
+
wire: "provider",
|
|
182
|
+
source: "json",
|
|
183
|
+
codec: codec("string")
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "model",
|
|
187
|
+
wire: "model",
|
|
188
|
+
source: "json",
|
|
189
|
+
codec: codec("string")
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: "context",
|
|
193
|
+
wire: "context",
|
|
194
|
+
source: "json",
|
|
195
|
+
codec: codec("string")
|
|
196
|
+
}
|
|
197
|
+
],
|
|
198
|
+
cancellation: { parameter: "signal" },
|
|
199
|
+
result: codec("string")
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: `${PACKAGE_NAME}#betterInput/templatesList`,
|
|
203
|
+
service: "BetterInputPolish",
|
|
204
|
+
namespace: "betterInput",
|
|
205
|
+
method: "templatesList",
|
|
206
|
+
invocation: { kind: "direct" },
|
|
207
|
+
parameters: [],
|
|
208
|
+
result: codec(symbol("TemplateListResult"))
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: `${PACKAGE_NAME}#betterInput/templatesSave`,
|
|
212
|
+
service: "BetterInputPolish",
|
|
213
|
+
namespace: "betterInput",
|
|
214
|
+
method: "templatesSave",
|
|
215
|
+
invocation: { kind: "direct" },
|
|
216
|
+
parameters: [{
|
|
217
|
+
name: "template",
|
|
218
|
+
wire: "template",
|
|
219
|
+
source: "json",
|
|
220
|
+
codec: codec(symbol("TemplateInput"))
|
|
221
|
+
}],
|
|
222
|
+
cancellation: { parameter: "signal" },
|
|
223
|
+
result: codec(symbol("TemplateSaveResult"))
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: `${PACKAGE_NAME}#betterInput/templatesRemove`,
|
|
227
|
+
service: "BetterInputPolish",
|
|
228
|
+
namespace: "betterInput",
|
|
229
|
+
method: "templatesRemove",
|
|
230
|
+
invocation: { kind: "direct" },
|
|
231
|
+
parameters: [{
|
|
232
|
+
name: "id",
|
|
233
|
+
wire: "id",
|
|
234
|
+
source: "json",
|
|
235
|
+
codec: codec("string")
|
|
236
|
+
}],
|
|
237
|
+
cancellation: { parameter: "signal" },
|
|
238
|
+
result: codec(symbol("TemplateRemoveResult"))
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
};
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region src/client/strings.ts
|
|
244
|
+
const zh = {
|
|
245
|
+
voiceStart: "语音输入",
|
|
246
|
+
voiceStop: "停止语音输入",
|
|
247
|
+
voiceBusy: "正在处理…",
|
|
248
|
+
voiceUnavailable: "此浏览器不支持语音识别",
|
|
249
|
+
listening: "正在聆听…",
|
|
250
|
+
transcribing: "正在转写…",
|
|
251
|
+
polishing: "正在润色…",
|
|
252
|
+
voiceFailed: "语音输入失败",
|
|
253
|
+
polishNotConfigured: "未配置润色模型,请在设置页选择",
|
|
254
|
+
polishFailedKeepOriginal: "润色失败,已保留原文",
|
|
255
|
+
settingsTitle: "BetterInput 设置",
|
|
256
|
+
settingsDescription: "配置语音识别与 AI 润色。润色复用你在 dsh 设置里已配置的模型,无需额外 API key。",
|
|
257
|
+
loading: "加载中…",
|
|
258
|
+
saveFailed: "保存失败,请重试",
|
|
259
|
+
languageLabel: "识别语言",
|
|
260
|
+
languageHint: "留空时跟随浏览器语言。",
|
|
261
|
+
languagePlaceholder: "例如 zh-CN 或 en-US",
|
|
262
|
+
recordingLimitLabel: "单次录音上限(秒)",
|
|
263
|
+
recordingLimitHint: "1–600 秒。",
|
|
264
|
+
polishLabel: "AI 润色",
|
|
265
|
+
polishHint: "识别完成后用大模型清理转写文本(去口头禅、修正同音错字、加标点)。",
|
|
266
|
+
on: "开",
|
|
267
|
+
off: "关",
|
|
268
|
+
polishModelLabel: "润色模型",
|
|
269
|
+
polishModelHint: "选择 dsh 中已配置的模型路由。",
|
|
270
|
+
polishModelNone: "(未选择)",
|
|
271
|
+
polishEffortLabel: "润色思考强度",
|
|
272
|
+
polishEffortHint: "控制大模型的推理深度。默认即适配器最低档,适合大多数场景。",
|
|
273
|
+
polishPromptLabel: "自定义润色提示词",
|
|
274
|
+
polishPromptHint: "留空使用内置提示词。自定义提示词总是追加输出契约保护。",
|
|
275
|
+
polishPromptPlaceholder: "可选:粘贴自定义提示词…",
|
|
276
|
+
showDefaultPrompt: "查看内置提示词",
|
|
277
|
+
hideDefaultPrompt: "收起内置提示词",
|
|
278
|
+
defaultPromptLabel: "内置提示词",
|
|
279
|
+
effortDefaultLabel: "默认(关闭思考)",
|
|
280
|
+
effortLoadingLabel: "加载思考强度选项…",
|
|
281
|
+
routesStatus: "可用模型路由",
|
|
282
|
+
routesUnavailable: "不可用",
|
|
283
|
+
optimizeButton: "优化提示词",
|
|
284
|
+
optimizeBusy: "优化中…",
|
|
285
|
+
optimizeFailed: "优化失败,请重试",
|
|
286
|
+
optimizeEmpty: "输入框为空,无需优化",
|
|
287
|
+
optimizePanelTitle: "提示词优化结果",
|
|
288
|
+
optimizeOriginalLabel: "原文",
|
|
289
|
+
optimizeOptimizedLabel: "优化后",
|
|
290
|
+
optimizeAdopt: "采纳",
|
|
291
|
+
optimizeCancel: "取消",
|
|
292
|
+
optimizeNotConfigured: "未配置优化模型,请在设置页开启",
|
|
293
|
+
optimizeSectionLabel: "提示词优化",
|
|
294
|
+
optimizeModelLabel: "优化模型",
|
|
295
|
+
optimizeModelHint: "选择 dsh 中已配置的模型路由。",
|
|
296
|
+
optimizeEffortLabel: "优化思考强度",
|
|
297
|
+
optimizeEffortHint: "控制大模型的推理深度。默认即适配器最低档,适合大多数场景。",
|
|
298
|
+
optimizePromptLabel: "自定义优化提示词",
|
|
299
|
+
optimizePromptHint: "留空使用内置提示词。自定义提示词总是追加输出契约保护。",
|
|
300
|
+
optimizePromptPlaceholder: "可选:粘贴自定义提示词…",
|
|
301
|
+
contextTurnsLabel: "上下文引用轮数",
|
|
302
|
+
contextTurnsHint: "优化时引用最近 N 轮对话作为上下文,0 为禁用。默认 3 轮。",
|
|
303
|
+
aboutTitle: "关于与更新",
|
|
304
|
+
aboutVersionLabel: "当前版本",
|
|
305
|
+
aboutRepositoryLabel: "项目地址",
|
|
306
|
+
aboutChangelogLabel: "更新日志",
|
|
307
|
+
aboutLicenseLabel: "许可证",
|
|
308
|
+
checkUpdateButton: "检查更新",
|
|
309
|
+
checkingUpdate: "检查中…",
|
|
310
|
+
updateUpToDate: "当前已是最新版本。",
|
|
311
|
+
updateAvailable: "发现新版本",
|
|
312
|
+
updateUnpublished: "该版本未在 npm 上公开发布。",
|
|
313
|
+
updateCheckFailed: "检查更新失败",
|
|
314
|
+
updateCommandLabel: "已全局安装 dsh CLI,执行",
|
|
315
|
+
updateCommandNpxLabel: "未全局安装,改用 npx 执行",
|
|
316
|
+
updateCommandPick: "按你的安装方式二选一即可",
|
|
317
|
+
voiceSectionLabel: "语音识别",
|
|
318
|
+
polishSectionLabel: "提示词润色",
|
|
319
|
+
templatesTitle: "提示词模板",
|
|
320
|
+
templatesDescription: "把常用提示词存成模板,在输入框键入 / 即可搜索并插入。",
|
|
321
|
+
templatesNew: "新建模板",
|
|
322
|
+
templatesEdit: "编辑",
|
|
323
|
+
templatesSave: "保存",
|
|
324
|
+
templatesCancel: "取消",
|
|
325
|
+
templatesDelete: "删除",
|
|
326
|
+
templatesDeleteConfirm: "再次点击确认删除",
|
|
327
|
+
templatesEmpty: "还没有模板,点击「新建模板」创建第一个。",
|
|
328
|
+
templatesLoadFailed: "模板加载失败",
|
|
329
|
+
templatesRetry: "重试",
|
|
330
|
+
templatesActionFailed: "操作失败,请重试",
|
|
331
|
+
templatesNameLabel: "名称",
|
|
332
|
+
templatesNamePlaceholder: "例如:代码评审助手",
|
|
333
|
+
templatesDescriptionLabel: "描述",
|
|
334
|
+
templatesDescriptionPlaceholder: "可选:这个模板用来做什么",
|
|
335
|
+
templatesContentLabel: "内容",
|
|
336
|
+
templatesContentPlaceholder: "插入输入框的提示词正文…",
|
|
337
|
+
templatesTagsLabel: "标签",
|
|
338
|
+
templatesTagsHint: "逗号分隔,最多 8 个,用于菜单搜索。"
|
|
339
|
+
};
|
|
340
|
+
const en = {
|
|
341
|
+
voiceStart: "Voice input",
|
|
342
|
+
voiceStop: "Stop voice input",
|
|
343
|
+
voiceBusy: "Processing…",
|
|
344
|
+
voiceUnavailable: "Speech recognition is not supported in this browser",
|
|
345
|
+
listening: "Listening…",
|
|
346
|
+
transcribing: "Transcribing…",
|
|
347
|
+
polishing: "Polishing…",
|
|
348
|
+
voiceFailed: "Voice input failed",
|
|
349
|
+
polishNotConfigured: "No polish model configured, please choose one in Settings",
|
|
350
|
+
polishFailedKeepOriginal: "Polishing failed, original kept",
|
|
351
|
+
settingsTitle: "BetterInput Settings",
|
|
352
|
+
settingsDescription: "Configure voice recognition and AI polishing. Polishing reuses the models already configured in dsh — no extra API key needed.",
|
|
353
|
+
loading: "Loading…",
|
|
354
|
+
saveFailed: "Failed to save, please retry",
|
|
355
|
+
languageLabel: "Recognition language",
|
|
356
|
+
languageHint: "Empty follows the browser language.",
|
|
357
|
+
languagePlaceholder: "e.g. zh-CN or en-US",
|
|
358
|
+
recordingLimitLabel: "Recording limit (seconds)",
|
|
359
|
+
recordingLimitHint: "1–600 seconds.",
|
|
360
|
+
polishLabel: "AI polishing",
|
|
361
|
+
polishHint: "Clean the transcript with an LLM after recognition (fillers, homophone fixes, punctuation).",
|
|
362
|
+
on: "On",
|
|
363
|
+
off: "Off",
|
|
364
|
+
polishModelLabel: "Polish model",
|
|
365
|
+
polishModelHint: "Pick a model route already configured in dsh.",
|
|
366
|
+
polishModelNone: "(none)",
|
|
367
|
+
polishEffortLabel: "Polishing thinking effort",
|
|
368
|
+
polishEffortHint: "Controls the model inference depth. Default uses the adapter baseline (lightest tier).",
|
|
369
|
+
polishPromptLabel: "Custom polish prompt",
|
|
370
|
+
polishPromptHint: "Empty uses the built-in prompt. A custom prompt always keeps the output-contract guard.",
|
|
371
|
+
polishPromptPlaceholder: "Optional: paste a custom prompt…",
|
|
372
|
+
showDefaultPrompt: "Show the built-in prompt",
|
|
373
|
+
hideDefaultPrompt: "Hide the built-in prompt",
|
|
374
|
+
defaultPromptLabel: "Built-in prompt",
|
|
375
|
+
effortDefaultLabel: "Default (thinking off)",
|
|
376
|
+
effortLoadingLabel: "Loading reasoning effort options…",
|
|
377
|
+
routesStatus: "Available model routes",
|
|
378
|
+
routesUnavailable: "unavailable",
|
|
379
|
+
optimizeButton: "Optimize prompt",
|
|
380
|
+
optimizeBusy: "Optimizing…",
|
|
381
|
+
optimizeFailed: "Optimization failed, please retry",
|
|
382
|
+
optimizeEmpty: "Input is empty, nothing to optimize",
|
|
383
|
+
optimizePanelTitle: "Prompt optimization result",
|
|
384
|
+
optimizeOriginalLabel: "Original",
|
|
385
|
+
optimizeOptimizedLabel: "Optimized",
|
|
386
|
+
optimizeAdopt: "Adopt",
|
|
387
|
+
optimizeCancel: "Cancel",
|
|
388
|
+
optimizeNotConfigured: "No optimize model configured, enable it in Settings",
|
|
389
|
+
optimizeSectionLabel: "Prompt optimization",
|
|
390
|
+
optimizeModelLabel: "Optimize model",
|
|
391
|
+
optimizeModelHint: "Pick a model route already configured in dsh.",
|
|
392
|
+
optimizeEffortLabel: "Optimize thinking effort",
|
|
393
|
+
optimizeEffortHint: "Controls the model inference depth. Default uses the adapter baseline (lightest tier).",
|
|
394
|
+
optimizePromptLabel: "Custom optimize prompt",
|
|
395
|
+
optimizePromptHint: "Empty uses the built-in prompt. A custom prompt always keeps the output-contract guard.",
|
|
396
|
+
optimizePromptPlaceholder: "Optional: paste a custom prompt…",
|
|
397
|
+
contextTurnsLabel: "Context turns",
|
|
398
|
+
contextTurnsHint: "Include recent N turns as context for optimization. 0 = disabled. Default 3.",
|
|
399
|
+
aboutTitle: "About & Updates",
|
|
400
|
+
aboutVersionLabel: "Installed version",
|
|
401
|
+
aboutRepositoryLabel: "Repository",
|
|
402
|
+
aboutChangelogLabel: "Changelog",
|
|
403
|
+
aboutLicenseLabel: "License",
|
|
404
|
+
checkUpdateButton: "Check for updates",
|
|
405
|
+
checkingUpdate: "Checking…",
|
|
406
|
+
updateUpToDate: "You are up to date.",
|
|
407
|
+
updateAvailable: "A new version is available",
|
|
408
|
+
updateUnpublished: "This version is not published on npm.",
|
|
409
|
+
updateCheckFailed: "Update check failed",
|
|
410
|
+
updateCommandLabel: "With a global dsh CLI, run",
|
|
411
|
+
updateCommandNpxLabel: "Without a global dsh CLI, run via npx",
|
|
412
|
+
updateCommandPick: "Use either one depending on how you installed DSH",
|
|
413
|
+
voiceSectionLabel: "Voice Recognition",
|
|
414
|
+
polishSectionLabel: "Prompt Polishing",
|
|
415
|
+
templatesTitle: "Prompt Templates",
|
|
416
|
+
templatesDescription: "Save frequently used prompts as templates, then type / in the input box to search and insert.",
|
|
417
|
+
templatesNew: "New template",
|
|
418
|
+
templatesEdit: "Edit",
|
|
419
|
+
templatesSave: "Save",
|
|
420
|
+
templatesCancel: "Cancel",
|
|
421
|
+
templatesDelete: "Delete",
|
|
422
|
+
templatesDeleteConfirm: "Click again to confirm",
|
|
423
|
+
templatesEmpty: "No templates yet. Create your first one with \"New template\".",
|
|
424
|
+
templatesLoadFailed: "Failed to load templates",
|
|
425
|
+
templatesRetry: "Retry",
|
|
426
|
+
templatesActionFailed: "Action failed, please retry",
|
|
427
|
+
templatesNameLabel: "Name",
|
|
428
|
+
templatesNamePlaceholder: "e.g. Code review assistant",
|
|
429
|
+
templatesDescriptionLabel: "Description",
|
|
430
|
+
templatesDescriptionPlaceholder: "Optional: what this template is for",
|
|
431
|
+
templatesContentLabel: "Content",
|
|
432
|
+
templatesContentPlaceholder: "Prompt body inserted into the input box…",
|
|
433
|
+
templatesTagsLabel: "Tags",
|
|
434
|
+
templatesTagsHint: "Comma separated, up to 8, used for menu search."
|
|
435
|
+
};
|
|
436
|
+
/** Namespace owning every BetterInput surface string. Registered into the DSH
|
|
437
|
+
* locale runtime; slots declaring this namespace receive the typed `t`. */
|
|
438
|
+
const BETTER_INPUT_NS = "better-input";
|
|
439
|
+
/**
|
|
440
|
+
* Out-of-the-box defaults: every toggle ON so new users get the full
|
|
441
|
+
* experience immediately; reasoning effort left empty, which the Host
|
|
442
|
+
* translates to "thinking off" (the model's `off` tier when it exposes
|
|
443
|
+
* one, otherwise the adapter's own default).
|
|
444
|
+
* Provider/model stay empty and get auto-filled on first settings page
|
|
445
|
+
* load via SettingsController (first route returned by listRoutes).
|
|
446
|
+
*/
|
|
447
|
+
const DEFAULT_SETTINGS = Object.freeze({
|
|
448
|
+
language: "",
|
|
449
|
+
maxRecordingSeconds: 120,
|
|
450
|
+
polishingEnabled: true,
|
|
451
|
+
polishProvider: "",
|
|
452
|
+
polishModel: "",
|
|
453
|
+
polishReasoningEffort: "",
|
|
454
|
+
polishPrompt: "",
|
|
455
|
+
optimizeEnabled: true,
|
|
456
|
+
optimizeProvider: "",
|
|
457
|
+
optimizeModel: "",
|
|
458
|
+
optimizeReasoningEffort: "",
|
|
459
|
+
optimizePrompt: "",
|
|
460
|
+
contextTurns: 3
|
|
461
|
+
});
|
|
462
|
+
/**
|
|
463
|
+
* Recognition language derived from the browser locale. An empty stored
|
|
464
|
+
* language follows the UI.
|
|
465
|
+
*/
|
|
466
|
+
function recognitionLanguageFromBrowser() {
|
|
467
|
+
if (typeof navigator === "undefined") return "en-US";
|
|
468
|
+
return (navigator.language ?? "").toLowerCase().startsWith("zh") ? "zh-CN" : "en-US";
|
|
469
|
+
}
|
|
470
|
+
function effectiveRecognitionLanguage(stored) {
|
|
471
|
+
const value = stored.trim();
|
|
472
|
+
return value === "" ? recognitionLanguageFromBrowser() : value;
|
|
473
|
+
}
|
|
474
|
+
function isValidRecordingLimit(value) {
|
|
475
|
+
return Number.isSafeInteger(value) && value >= 1 && value <= 600;
|
|
476
|
+
}
|
|
477
|
+
/** Resolve the effective recording cap from stored settings. */
|
|
478
|
+
function effectiveRecordingSeconds(settings) {
|
|
479
|
+
return isValidRecordingLimit(settings.maxRecordingSeconds) ? settings.maxRecordingSeconds : 120;
|
|
480
|
+
}
|
|
481
|
+
//#endregion
|
|
482
|
+
//#region src/client/web-speech.ts
|
|
483
|
+
var WebSpeechSession = class {
|
|
484
|
+
recognition;
|
|
485
|
+
options;
|
|
486
|
+
active = false;
|
|
487
|
+
stopping = false;
|
|
488
|
+
finalText = "";
|
|
489
|
+
lastHeard = "";
|
|
490
|
+
ended = false;
|
|
491
|
+
silent = false;
|
|
492
|
+
constructor(options) {
|
|
493
|
+
const Recognition = getSpeechRecognitionConstructor();
|
|
494
|
+
if (Recognition === void 0) throw new Error("Speech recognition is unavailable in this browser");
|
|
495
|
+
this.options = options;
|
|
496
|
+
this.recognition = new Recognition();
|
|
497
|
+
this.recognition.lang = options.language;
|
|
498
|
+
this.recognition.continuous = true;
|
|
499
|
+
this.recognition.interimResults = true;
|
|
500
|
+
this.recognition.maxAlternatives = 1;
|
|
501
|
+
this.recognition.onstart = () => options.onStart?.();
|
|
502
|
+
this.recognition.onresult = (event) => this.handleResult(event);
|
|
503
|
+
this.recognition.onerror = (event) => this.handleError(event);
|
|
504
|
+
this.recognition.onend = () => this.handleEnd();
|
|
505
|
+
}
|
|
506
|
+
start() {
|
|
507
|
+
if (this.active || this.ended || this.silent) return;
|
|
508
|
+
this.active = true;
|
|
509
|
+
this.stopping = false;
|
|
510
|
+
try {
|
|
511
|
+
this.recognition.start();
|
|
512
|
+
} catch (error) {
|
|
513
|
+
this.fail(error instanceof Error ? error : new Error(String(error)));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
stop() {
|
|
517
|
+
if (!this.active || this.ended || this.silent) return;
|
|
518
|
+
this.stopping = true;
|
|
519
|
+
this.active = false;
|
|
520
|
+
try {
|
|
521
|
+
this.recognition.stop();
|
|
522
|
+
} catch {
|
|
523
|
+
this.endOnce();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
abort() {
|
|
527
|
+
if (this.ended || this.silent) return;
|
|
528
|
+
this.silent = true;
|
|
529
|
+
this.stopping = true;
|
|
530
|
+
this.active = false;
|
|
531
|
+
this.recognition.onstart = null;
|
|
532
|
+
this.recognition.onresult = null;
|
|
533
|
+
this.recognition.onerror = null;
|
|
534
|
+
this.recognition.onend = null;
|
|
535
|
+
try {
|
|
536
|
+
this.recognition.abort();
|
|
537
|
+
} catch {}
|
|
538
|
+
this.ended = true;
|
|
539
|
+
}
|
|
540
|
+
handleResult(event) {
|
|
541
|
+
if (this.silent || this.ended) return;
|
|
542
|
+
let finalChunk = "";
|
|
543
|
+
let interimChunk = "";
|
|
544
|
+
for (let index = event.resultIndex; index < event.results.length; index += 1) {
|
|
545
|
+
const result = event.results[index];
|
|
546
|
+
const transcript = result?.[0]?.transcript ?? "";
|
|
547
|
+
if (result?.isFinal === true) finalChunk = appendSpeech(finalChunk, transcript);
|
|
548
|
+
else interimChunk = appendSpeech(interimChunk, transcript);
|
|
549
|
+
}
|
|
550
|
+
if (finalChunk !== "") {
|
|
551
|
+
this.finalText = appendSpeech(this.finalText, finalChunk);
|
|
552
|
+
this.lastHeard = this.finalText;
|
|
553
|
+
this.options.onFinal(this.finalText);
|
|
554
|
+
}
|
|
555
|
+
const heard = appendSpeech(this.finalText, interimChunk);
|
|
556
|
+
if (heard !== "") this.lastHeard = heard;
|
|
557
|
+
this.options.onInterim(heard);
|
|
558
|
+
}
|
|
559
|
+
handleError(event) {
|
|
560
|
+
if (this.stopping || this.silent || this.ended) return;
|
|
561
|
+
const code = event.error ?? "unknown";
|
|
562
|
+
const detail = event.message === void 0 ? "" : `: ${event.message}`;
|
|
563
|
+
this.fail(/* @__PURE__ */ new Error(`Speech recognition failed (${code})${detail}`));
|
|
564
|
+
}
|
|
565
|
+
handleEnd() {
|
|
566
|
+
if (this.silent || this.ended) return;
|
|
567
|
+
if (this.active && !this.stopping) try {
|
|
568
|
+
this.recognition.start();
|
|
569
|
+
return;
|
|
570
|
+
} catch (error) {
|
|
571
|
+
this.fail(error instanceof Error ? error : new Error(String(error)));
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
this.endOnce();
|
|
575
|
+
}
|
|
576
|
+
fail(error) {
|
|
577
|
+
if (this.stopping || this.silent || this.ended) return;
|
|
578
|
+
this.active = false;
|
|
579
|
+
this.stopping = true;
|
|
580
|
+
this.options.onError(error);
|
|
581
|
+
try {
|
|
582
|
+
this.recognition.abort();
|
|
583
|
+
} catch {}
|
|
584
|
+
this.endOnce();
|
|
585
|
+
}
|
|
586
|
+
endOnce() {
|
|
587
|
+
if (this.ended || this.silent) return;
|
|
588
|
+
this.ended = true;
|
|
589
|
+
this.options.onEnd(this.finalText !== "" ? this.finalText : this.lastHeard);
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
function appendSpeech(current, next) {
|
|
593
|
+
if (next === "") return current;
|
|
594
|
+
if (current === "") return next;
|
|
595
|
+
if (/\s$/.test(current) || /^\s/.test(next)) return current + next;
|
|
596
|
+
return `${current} ${next}`;
|
|
597
|
+
}
|
|
598
|
+
function getSpeechRecognitionConstructor() {
|
|
599
|
+
if (typeof window === "undefined") return void 0;
|
|
600
|
+
const speechWindow = window;
|
|
601
|
+
return speechWindow.SpeechRecognition ?? speechWindow.webkitSpeechRecognition;
|
|
602
|
+
}
|
|
603
|
+
//#endregion
|
|
604
|
+
//#region src/client/voice-session.ts
|
|
605
|
+
const NOTICE_STATES = /* @__PURE__ */ new Set(["error", "polish-error"]);
|
|
606
|
+
const VOICE_ERROR_DISMISS_MS = 2600;
|
|
607
|
+
/**
|
|
608
|
+
* Shared voice-input state for one session, written from scratch for
|
|
609
|
+
* sqs-dsh-better-input. The microphone button and the recognition bar both
|
|
610
|
+
* subscribe; the bar can request stop/cancel through the same instance.
|
|
611
|
+
*/
|
|
612
|
+
var VoiceInputSession = class {
|
|
613
|
+
snapshot = {
|
|
614
|
+
state: "idle",
|
|
615
|
+
detail: ""
|
|
616
|
+
};
|
|
617
|
+
listeners = /* @__PURE__ */ new Set();
|
|
618
|
+
stopListeners = /* @__PURE__ */ new Set();
|
|
619
|
+
cancelListeners = /* @__PURE__ */ new Set();
|
|
620
|
+
epoch = 0;
|
|
621
|
+
errorTimer;
|
|
622
|
+
captureEpoch() {
|
|
623
|
+
return this.epoch;
|
|
624
|
+
}
|
|
625
|
+
isCurrentEpoch(epoch) {
|
|
626
|
+
return this.epoch === epoch;
|
|
627
|
+
}
|
|
628
|
+
getSnapshot = () => this.snapshot;
|
|
629
|
+
subscribe = (listener) => {
|
|
630
|
+
this.listeners.add(listener);
|
|
631
|
+
return () => this.listeners.delete(listener);
|
|
632
|
+
};
|
|
633
|
+
onStopRequested = (listener) => {
|
|
634
|
+
this.stopListeners.add(listener);
|
|
635
|
+
return () => this.stopListeners.delete(listener);
|
|
636
|
+
};
|
|
637
|
+
onCancelRequested = (listener) => {
|
|
638
|
+
this.cancelListeners.add(listener);
|
|
639
|
+
return () => this.cancelListeners.delete(listener);
|
|
640
|
+
};
|
|
641
|
+
setState(state, detail = "") {
|
|
642
|
+
const nextDetail = NOTICE_STATES.has(state) ? detail : "";
|
|
643
|
+
if (this.snapshot.state === state && this.snapshot.detail === nextDetail) return;
|
|
644
|
+
this.clearErrorTimer();
|
|
645
|
+
this.snapshot = {
|
|
646
|
+
state,
|
|
647
|
+
detail: nextDetail
|
|
648
|
+
};
|
|
649
|
+
this.emit();
|
|
650
|
+
if (NOTICE_STATES.has(state)) this.errorTimer = setTimeout(() => {
|
|
651
|
+
this.errorTimer = void 0;
|
|
652
|
+
if (NOTICE_STATES.has(this.snapshot.state)) this.setState("idle");
|
|
653
|
+
}, VOICE_ERROR_DISMISS_MS);
|
|
654
|
+
}
|
|
655
|
+
requestStop() {
|
|
656
|
+
for (const listener of this.stopListeners) listener();
|
|
657
|
+
}
|
|
658
|
+
requestCancel() {
|
|
659
|
+
if (this.snapshot.state !== "transcribing" && this.snapshot.state !== "polishing") return;
|
|
660
|
+
this.epoch += 1;
|
|
661
|
+
for (const listener of this.cancelListeners) listener();
|
|
662
|
+
this.setState("idle");
|
|
663
|
+
}
|
|
664
|
+
dispose() {
|
|
665
|
+
this.clearErrorTimer();
|
|
666
|
+
this.listeners.clear();
|
|
667
|
+
this.stopListeners.clear();
|
|
668
|
+
this.cancelListeners.clear();
|
|
669
|
+
}
|
|
670
|
+
clearErrorTimer() {
|
|
671
|
+
if (this.errorTimer === void 0) return;
|
|
672
|
+
clearTimeout(this.errorTimer);
|
|
673
|
+
this.errorTimer = void 0;
|
|
674
|
+
}
|
|
675
|
+
emit() {
|
|
676
|
+
for (const listener of this.listeners) listener();
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
function useVoiceInputSession(session) {
|
|
680
|
+
return (0, react.useSyncExternalStore)(session.subscribe, session.getSnapshot, session.getSnapshot);
|
|
681
|
+
}
|
|
682
|
+
//#endregion
|
|
683
|
+
//#region src/client/MicrophoneButton.tsx
|
|
684
|
+
/**
|
|
685
|
+
* The microphone button in the composer tool row. Click to start listening,
|
|
686
|
+
* click again to stop. Transcripts stream into the draft in real time; when
|
|
687
|
+
* polishing is enabled, the committed transcript is polished through the Host
|
|
688
|
+
* LLM route and replaces the draft (unless the user edited it meanwhile).
|
|
689
|
+
*/
|
|
690
|
+
function MicrophoneButton({ useInput, inputActions, voiceSession, remote, useSettings, t }) {
|
|
691
|
+
const state = useVoiceInputSession(voiceSession).state;
|
|
692
|
+
const setState = (next, detail = "") => voiceSession.setState(next, detail);
|
|
693
|
+
const input = useInput((state) => state);
|
|
694
|
+
const speechRef = (0, react.useRef)(null);
|
|
695
|
+
const baseDraftRef = (0, react.useRef)("");
|
|
696
|
+
const mountedRef = (0, react.useRef)(true);
|
|
697
|
+
const stopRef = (0, react.useRef)(null);
|
|
698
|
+
const polishAbortRef = (0, react.useRef)(null);
|
|
699
|
+
const recordingTimerRef = (0, react.useRef)(null);
|
|
700
|
+
const settingsRef = (0, react.useRef)(null);
|
|
701
|
+
const settingsFace = useSettings();
|
|
702
|
+
if (settingsFace.status === "ready") settingsRef.current = settingsFace.settings;
|
|
703
|
+
const latestDraftRef = (0, react.useRef)(input.draft);
|
|
704
|
+
(0, react.useEffect)(() => {
|
|
705
|
+
latestDraftRef.current = input.draft;
|
|
706
|
+
}, [input.draft]);
|
|
707
|
+
(0, react.useEffect)(() => {
|
|
708
|
+
mountedRef.current = true;
|
|
709
|
+
const stop = voiceSession.onStopRequested(() => stopRef.current?.());
|
|
710
|
+
const cancel = voiceSession.onCancelRequested(() => {
|
|
711
|
+
speechRef.current?.abort();
|
|
712
|
+
polishAbortRef.current?.abort();
|
|
713
|
+
});
|
|
714
|
+
return () => {
|
|
715
|
+
mountedRef.current = false;
|
|
716
|
+
stop();
|
|
717
|
+
cancel();
|
|
718
|
+
speechRef.current?.abort();
|
|
719
|
+
speechRef.current = null;
|
|
720
|
+
polishAbortRef.current?.abort();
|
|
721
|
+
polishAbortRef.current = null;
|
|
722
|
+
clearRecordingTimer(recordingTimerRef);
|
|
723
|
+
voiceSession.setState("idle");
|
|
724
|
+
};
|
|
725
|
+
}, [voiceSession]);
|
|
726
|
+
const active = state === "starting" || state === "recording";
|
|
727
|
+
const busy = state === "transcribing" || state === "polishing";
|
|
728
|
+
const settings = settingsFace.status === "ready" ? settingsFace.settings : settingsRef.current;
|
|
729
|
+
const polishingEnabled = settings?.polishingEnabled ?? false;
|
|
730
|
+
const polishProvider = (settings?.polishProvider ?? "").trim();
|
|
731
|
+
const polishModel = (settings?.polishModel ?? "").trim();
|
|
732
|
+
const polishConfigured = polishingEnabled && polishProvider !== "" && polishModel !== "";
|
|
733
|
+
const startListening = () => {
|
|
734
|
+
if (active || busy) return;
|
|
735
|
+
const baseDraft = input.draft;
|
|
736
|
+
baseDraftRef.current = baseDraft;
|
|
737
|
+
let sessionDraft = baseDraft;
|
|
738
|
+
let failed = false;
|
|
739
|
+
setState("starting");
|
|
740
|
+
let session;
|
|
741
|
+
try {
|
|
742
|
+
session = new WebSpeechSession({
|
|
743
|
+
language: effectiveRecognitionLanguage(settingsRef.current?.language ?? ""),
|
|
744
|
+
onInterim: (text) => {
|
|
745
|
+
sessionDraft = updateDraft(baseDraft, text);
|
|
746
|
+
inputActions.setDraft(sessionDraft);
|
|
747
|
+
},
|
|
748
|
+
onFinal: (text) => {
|
|
749
|
+
sessionDraft = updateDraft(baseDraft, text);
|
|
750
|
+
inputActions.setDraft(sessionDraft);
|
|
751
|
+
},
|
|
752
|
+
onError: (error) => {
|
|
753
|
+
failed = true;
|
|
754
|
+
setState("error", error.message);
|
|
755
|
+
},
|
|
756
|
+
onEnd: (text) => {
|
|
757
|
+
speechRef.current = null;
|
|
758
|
+
clearRecordingTimer(recordingTimerRef);
|
|
759
|
+
if (!mountedRef.current) return;
|
|
760
|
+
const transcript = text.trim();
|
|
761
|
+
if (failed) return;
|
|
762
|
+
if (transcript === "") {
|
|
763
|
+
setState("idle");
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
const draftAtStop = updateDraft(baseDraft, transcript);
|
|
767
|
+
inputActions.setDraft(draftAtStop);
|
|
768
|
+
latestDraftRef.current = draftAtStop;
|
|
769
|
+
const settings = settingsRef.current;
|
|
770
|
+
if (settings !== null && settings.polishingEnabled && settings.polishProvider.trim() !== "" && settings.polishModel.trim() !== "") polishDraft({
|
|
771
|
+
transcript,
|
|
772
|
+
baseDraft,
|
|
773
|
+
draftAtStop,
|
|
774
|
+
provider: settings.polishProvider,
|
|
775
|
+
model: settings.polishModel,
|
|
776
|
+
remote,
|
|
777
|
+
setState,
|
|
778
|
+
latestDraftRef,
|
|
779
|
+
actionsRef: { current: inputActions },
|
|
780
|
+
polishAbortRef
|
|
781
|
+
});
|
|
782
|
+
else setState("idle");
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
} catch (error) {
|
|
786
|
+
if (mountedRef.current) setState("error", error instanceof Error ? error.message : "Speech recognition is unavailable in this browser");
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
speechRef.current = session;
|
|
790
|
+
session.start();
|
|
791
|
+
if (failed) return;
|
|
792
|
+
setState("recording");
|
|
793
|
+
armRecordingTimer(recordingTimerRef, effectiveRecordingSeconds(settingsRef.current ?? { maxRecordingSeconds: 120 }), () => {
|
|
794
|
+
speechRef.current?.stop();
|
|
795
|
+
});
|
|
796
|
+
};
|
|
797
|
+
const stopListening = () => {
|
|
798
|
+
clearRecordingTimer(recordingTimerRef);
|
|
799
|
+
if (!active) return;
|
|
800
|
+
speechRef.current?.stop();
|
|
801
|
+
};
|
|
802
|
+
stopRef.current = stopListening;
|
|
803
|
+
const tooltip = busy ? t("voiceBusy") : active ? t("voiceStop") : state === "polish-error" ? t("polishFailedKeepOriginal") : state === "error" ? t("voiceFailed") : !polishConfigured ? `${t("voiceStart")} — ${t("polishNotConfigured")}` : t("voiceStart");
|
|
804
|
+
const label = busy ? "…" : active ? t("voiceStop") : t("voiceStart");
|
|
805
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
806
|
+
type: "button",
|
|
807
|
+
"aria-label": label,
|
|
808
|
+
"aria-pressed": active,
|
|
809
|
+
disabled: busy,
|
|
810
|
+
title: tooltip,
|
|
811
|
+
onClick: active ? stopListening : startListening,
|
|
812
|
+
"data-better-input-state": state,
|
|
813
|
+
"data-better-input-polish-configured": polishConfigured ? "yes" : "no",
|
|
814
|
+
style: buttonStyle$2(active, busy, !polishConfigured),
|
|
815
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MicrophoneIcon, {})
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
async function polishDraft(options) {
|
|
819
|
+
if (options.provider.trim() === "" || options.model.trim() === "") return;
|
|
820
|
+
const controller = new AbortController();
|
|
821
|
+
options.polishAbortRef.current = controller;
|
|
822
|
+
options.setState("polishing");
|
|
823
|
+
try {
|
|
824
|
+
const result = await options.remote.polish(options.transcript, options.provider, options.model, controller.signal);
|
|
825
|
+
if (controller.signal.aborted) return;
|
|
826
|
+
if (!shouldApplyPolishResult(options.latestDraftRef.current, options.draftAtStop, options.baseDraft)) {
|
|
827
|
+
options.setState("idle");
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
if (!result.ok) {
|
|
831
|
+
options.setState("polish-error", result.error.message);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
const text = result.value.trim() !== "" ? result.value.trim() : options.transcript;
|
|
835
|
+
const nextDraft = updateDraft(options.baseDraft, text);
|
|
836
|
+
options.latestDraftRef.current = nextDraft;
|
|
837
|
+
options.actionsRef.current.setDraft(nextDraft);
|
|
838
|
+
options.setState("idle");
|
|
839
|
+
} catch (error) {
|
|
840
|
+
if (controller.signal.aborted) return;
|
|
841
|
+
options.setState("polish-error", error instanceof Error ? error.message : "Polishing failed");
|
|
842
|
+
} finally {
|
|
843
|
+
if (options.polishAbortRef.current === controller) options.polishAbortRef.current = null;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Only replace the draft when the user has not edited it since the transcript
|
|
848
|
+
* landed. Both the transcript-at-stop and the base draft count as unchanged
|
|
849
|
+
* (the user may have reverted the interim edits).
|
|
850
|
+
*/
|
|
851
|
+
function shouldApplyPolishResult(currentDraft, draftAtStop, baseDraft) {
|
|
852
|
+
const current = collapseDraft(currentDraft);
|
|
853
|
+
return current === collapseDraft(draftAtStop) || current === collapseDraft(baseDraft);
|
|
854
|
+
}
|
|
855
|
+
function collapseDraft(text) {
|
|
856
|
+
return text.replace(/\s+/g, " ").trim();
|
|
857
|
+
}
|
|
858
|
+
function buttonStyle$2(active, busy, polishUnconfigured) {
|
|
859
|
+
return {
|
|
860
|
+
display: "inline-flex",
|
|
861
|
+
alignItems: "center",
|
|
862
|
+
justifyContent: "center",
|
|
863
|
+
width: 28,
|
|
864
|
+
height: 28,
|
|
865
|
+
padding: 0,
|
|
866
|
+
border: "none",
|
|
867
|
+
borderRadius: 6,
|
|
868
|
+
background: active ? "var(--dsw-alias-state-business-primary, #4f8cff)" : "transparent",
|
|
869
|
+
color: active ? "#fff" : "var(--dsw-alias-label-primary, inherit)",
|
|
870
|
+
cursor: busy ? "default" : "pointer",
|
|
871
|
+
opacity: busy ? .5 : polishUnconfigured ? .75 : 1,
|
|
872
|
+
flex: "none"
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
function MicrophoneIcon() {
|
|
876
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
877
|
+
"aria-hidden": "true",
|
|
878
|
+
fill: "none",
|
|
879
|
+
height: "16",
|
|
880
|
+
viewBox: "0 0 16 16",
|
|
881
|
+
width: "16",
|
|
882
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
883
|
+
d: "M8 1.75a2.25 2.25 0 0 0-2.25 2.25v3.5a2.25 2.25 0 0 0 4.5 0V4A2.25 2.25 0 0 0 8 1.75Z",
|
|
884
|
+
stroke: "currentColor",
|
|
885
|
+
strokeLinejoin: "round",
|
|
886
|
+
strokeWidth: "1.4"
|
|
887
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
888
|
+
d: "M3.5 7.5a4.5 4.5 0 0 0 9 0M8 12.25V14M5.5 14h5",
|
|
889
|
+
stroke: "currentColor",
|
|
890
|
+
strokeLinecap: "round",
|
|
891
|
+
strokeLinejoin: "round",
|
|
892
|
+
strokeWidth: "1.4"
|
|
893
|
+
})]
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
/** Append transcript to a base draft with one space separator. */
|
|
897
|
+
function updateDraft(baseDraft, transcript) {
|
|
898
|
+
const text = transcript.trim();
|
|
899
|
+
if (text === "") return baseDraft;
|
|
900
|
+
if (baseDraft === "") return text;
|
|
901
|
+
if (/\s$/.test(baseDraft) || /^\s/.test(text)) return baseDraft + text;
|
|
902
|
+
return `${baseDraft} ${text}`;
|
|
903
|
+
}
|
|
904
|
+
function armRecordingTimer(timerRef, seconds, stop) {
|
|
905
|
+
clearRecordingTimer(timerRef);
|
|
906
|
+
timerRef.current = setTimeout(stop, Math.max(1, seconds) * 1e3);
|
|
907
|
+
}
|
|
908
|
+
function clearRecordingTimer(timerRef) {
|
|
909
|
+
if (timerRef.current === null) return;
|
|
910
|
+
clearTimeout(timerRef.current);
|
|
911
|
+
timerRef.current = null;
|
|
912
|
+
}
|
|
913
|
+
//#endregion
|
|
914
|
+
//#region src/client/OptimizeButton.tsx
|
|
915
|
+
/**
|
|
916
|
+
* The ✨ optimize button rendered above the composer card (in
|
|
917
|
+
* `conversation.input.dock`), right-aligned. Click reads the current draft,
|
|
918
|
+
* calls the Host LLM to optimize it, then shows a confirmation panel with
|
|
919
|
+
* the original and optimized text. The draft is replaced only when the user
|
|
920
|
+
* clicks "Adopt".
|
|
921
|
+
*/
|
|
922
|
+
function OptimizeButton({ useChat, useInput, inputActions, remote, useSettings, t }) {
|
|
923
|
+
const [state, setState] = (0, react.useState)({ kind: "idle" });
|
|
924
|
+
const settingsFace = useSettings();
|
|
925
|
+
const abortRef = (0, react.useRef)(null);
|
|
926
|
+
const input = useInput((state) => state);
|
|
927
|
+
const chat = useChat((area) => area);
|
|
928
|
+
const draftRef = (0, react.useRef)(input.draft);
|
|
929
|
+
(0, react.useEffect)(() => {
|
|
930
|
+
draftRef.current = input.draft;
|
|
931
|
+
}, [input.draft]);
|
|
932
|
+
(0, react.useEffect)(() => {
|
|
933
|
+
return () => {
|
|
934
|
+
abortRef.current?.abort();
|
|
935
|
+
};
|
|
936
|
+
}, []);
|
|
937
|
+
const settings = settingsFace.status === "ready" ? settingsFace.settings : null;
|
|
938
|
+
const provider = settings?.optimizeProvider.trim() ?? "";
|
|
939
|
+
const model = settings?.optimizeModel.trim() ?? "";
|
|
940
|
+
const modelConfigured = provider !== "" && model !== "";
|
|
941
|
+
const handleClick = async () => {
|
|
942
|
+
if (state.kind === "optimizing") return;
|
|
943
|
+
const draft = draftRef.current.trim();
|
|
944
|
+
if (draft === "") {
|
|
945
|
+
setState({
|
|
946
|
+
kind: "error",
|
|
947
|
+
message: t("optimizeEmpty")
|
|
948
|
+
});
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
if (!modelConfigured) {
|
|
952
|
+
setState({
|
|
953
|
+
kind: "error",
|
|
954
|
+
message: t("optimizeNotConfigured")
|
|
955
|
+
});
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
abortRef.current?.abort();
|
|
959
|
+
const controller = new AbortController();
|
|
960
|
+
abortRef.current = controller;
|
|
961
|
+
setState({ kind: "optimizing" });
|
|
962
|
+
try {
|
|
963
|
+
const contextTurns = settings?.contextTurns ?? 3;
|
|
964
|
+
const context = contextTurns > 0 ? extractConversationContext(chat.legacy.nodes, contextTurns) : "";
|
|
965
|
+
const result = await remote.optimize(draft, provider, model, context, controller.signal);
|
|
966
|
+
if (controller.signal.aborted) return;
|
|
967
|
+
if (!result.ok) {
|
|
968
|
+
setState({
|
|
969
|
+
kind: "error",
|
|
970
|
+
message: result.error.message
|
|
971
|
+
});
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
const optimized = result.value.trim();
|
|
975
|
+
if (optimized === "") {
|
|
976
|
+
setState({
|
|
977
|
+
kind: "error",
|
|
978
|
+
message: t("optimizeFailed")
|
|
979
|
+
});
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
setState({
|
|
983
|
+
kind: "result",
|
|
984
|
+
original: draft,
|
|
985
|
+
optimized
|
|
986
|
+
});
|
|
987
|
+
} catch (error) {
|
|
988
|
+
if (controller.signal.aborted) return;
|
|
989
|
+
setState({
|
|
990
|
+
kind: "error",
|
|
991
|
+
message: error instanceof Error ? error.message : t("optimizeFailed")
|
|
992
|
+
});
|
|
993
|
+
} finally {
|
|
994
|
+
if (abortRef.current === controller) abortRef.current = null;
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
const handleAdopt = () => {
|
|
998
|
+
if (state.kind !== "result") return;
|
|
999
|
+
inputActions.setDraft(state.optimized);
|
|
1000
|
+
setState({ kind: "idle" });
|
|
1001
|
+
};
|
|
1002
|
+
const handleCancel = () => {
|
|
1003
|
+
setState({ kind: "idle" });
|
|
1004
|
+
};
|
|
1005
|
+
const busy = state.kind === "optimizing";
|
|
1006
|
+
const disabled = busy || !modelConfigured;
|
|
1007
|
+
const buttonTitle = modelConfigured ? t("optimizeButton") : t("optimizeNotConfigured");
|
|
1008
|
+
const errorToast = state.kind === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorToastPortal, {
|
|
1009
|
+
message: state.message,
|
|
1010
|
+
onDismiss: handleCancel
|
|
1011
|
+
}) : null;
|
|
1012
|
+
const confirmModal = state.kind === "result" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmModalPortal, {
|
|
1013
|
+
title: t("optimizePanelTitle"),
|
|
1014
|
+
originalLabel: t("optimizeOriginalLabel"),
|
|
1015
|
+
optimizedLabel: t("optimizeOptimizedLabel"),
|
|
1016
|
+
original: state.original,
|
|
1017
|
+
optimized: state.optimized,
|
|
1018
|
+
adoptLabel: t("optimizeAdopt"),
|
|
1019
|
+
cancelLabel: t("optimizeCancel"),
|
|
1020
|
+
onAdopt: handleAdopt,
|
|
1021
|
+
onCancel: handleCancel
|
|
1022
|
+
}) : null;
|
|
1023
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1024
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1025
|
+
style: containerStyle,
|
|
1026
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1027
|
+
type: "button",
|
|
1028
|
+
"aria-label": buttonTitle,
|
|
1029
|
+
title: buttonTitle,
|
|
1030
|
+
disabled,
|
|
1031
|
+
onClick: handleClick,
|
|
1032
|
+
style: buttonStyle$1(disabled),
|
|
1033
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, {}), busy ? t("optimizeBusy") : ""]
|
|
1034
|
+
})
|
|
1035
|
+
}),
|
|
1036
|
+
errorToast,
|
|
1037
|
+
confirmModal
|
|
1038
|
+
] });
|
|
1039
|
+
}
|
|
1040
|
+
/** Ctrl/Cmd+A inside a `<pre>` selects only that block's text. */
|
|
1041
|
+
function selectAllInPre(e) {
|
|
1042
|
+
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a") {
|
|
1043
|
+
e.preventDefault();
|
|
1044
|
+
const range = document.createRange();
|
|
1045
|
+
range.selectNodeContents(e.currentTarget);
|
|
1046
|
+
const selection = window.getSelection();
|
|
1047
|
+
if (selection) {
|
|
1048
|
+
selection.removeAllRanges();
|
|
1049
|
+
selection.addRange(range);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
function OptimizeConfirmPanel(props) {
|
|
1054
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1055
|
+
style: modalContentStyle,
|
|
1056
|
+
children: [
|
|
1057
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1058
|
+
style: panelTitleStyle,
|
|
1059
|
+
children: props.title
|
|
1060
|
+
}),
|
|
1061
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1062
|
+
style: compareRowStyle,
|
|
1063
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1064
|
+
style: compareColStyle,
|
|
1065
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1066
|
+
style: compareLabelStyle,
|
|
1067
|
+
children: props.originalLabel
|
|
1068
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
1069
|
+
style: preStyle,
|
|
1070
|
+
tabIndex: 0,
|
|
1071
|
+
onKeyDown: selectAllInPre,
|
|
1072
|
+
children: props.original
|
|
1073
|
+
})]
|
|
1074
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1075
|
+
style: compareColStyle,
|
|
1076
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1077
|
+
style: compareLabelStyle,
|
|
1078
|
+
children: props.optimizedLabel
|
|
1079
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
1080
|
+
style: {
|
|
1081
|
+
...preStyle,
|
|
1082
|
+
borderColor: "var(--dsw-alias-state-business-primary, #4f8cff)"
|
|
1083
|
+
},
|
|
1084
|
+
tabIndex: 0,
|
|
1085
|
+
onKeyDown: selectAllInPre,
|
|
1086
|
+
children: props.optimized
|
|
1087
|
+
})]
|
|
1088
|
+
})]
|
|
1089
|
+
}),
|
|
1090
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1091
|
+
style: panelActionsStyle,
|
|
1092
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1093
|
+
type: "button",
|
|
1094
|
+
style: cancelBtnStyle,
|
|
1095
|
+
onClick: props.onCancel,
|
|
1096
|
+
children: props.cancelLabel
|
|
1097
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1098
|
+
type: "button",
|
|
1099
|
+
style: adoptBtnStyle,
|
|
1100
|
+
onClick: props.onAdopt,
|
|
1101
|
+
children: props.adoptLabel
|
|
1102
|
+
})]
|
|
1103
|
+
})
|
|
1104
|
+
]
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Comparison modal portalled to `document.body`. DSH renders slot content
|
|
1109
|
+
* inside shadow roots whose ancestors may clip or contain fixed-position
|
|
1110
|
+
* children, so the overlay must escape the component tree entirely.
|
|
1111
|
+
*/
|
|
1112
|
+
function ConfirmModalPortal(props) {
|
|
1113
|
+
return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1114
|
+
style: modalOverlayStyle,
|
|
1115
|
+
onClick: (e) => {
|
|
1116
|
+
const selection = window.getSelection();
|
|
1117
|
+
if (selection && !selection.isCollapsed) return;
|
|
1118
|
+
props.onCancel();
|
|
1119
|
+
},
|
|
1120
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1121
|
+
onClick: (e) => e.stopPropagation(),
|
|
1122
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptimizeConfirmPanel, { ...props })
|
|
1123
|
+
})
|
|
1124
|
+
}), document.body);
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Small error modal, also portalled to `document.body` for the same
|
|
1128
|
+
* stacking/escaping reasons as the comparison modal.
|
|
1129
|
+
*/
|
|
1130
|
+
function ErrorToastPortal({ message, onDismiss }) {
|
|
1131
|
+
return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1132
|
+
style: modalOverlayStyle,
|
|
1133
|
+
onClick: (e) => {
|
|
1134
|
+
const selection = window.getSelection();
|
|
1135
|
+
if (selection && !selection.isCollapsed) return;
|
|
1136
|
+
onDismiss();
|
|
1137
|
+
},
|
|
1138
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1139
|
+
style: errorModalStyle,
|
|
1140
|
+
onClick: (e) => e.stopPropagation(),
|
|
1141
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1142
|
+
style: { flex: 1 },
|
|
1143
|
+
children: message
|
|
1144
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1145
|
+
type: "button",
|
|
1146
|
+
style: errorDismissStyle,
|
|
1147
|
+
onClick: onDismiss,
|
|
1148
|
+
children: "×"
|
|
1149
|
+
})]
|
|
1150
|
+
})
|
|
1151
|
+
}), document.body);
|
|
1152
|
+
}
|
|
1153
|
+
function SparkleIcon() {
|
|
1154
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1155
|
+
"aria-hidden": "true",
|
|
1156
|
+
fill: "none",
|
|
1157
|
+
height: "14",
|
|
1158
|
+
viewBox: "0 0 16 16",
|
|
1159
|
+
width: "14",
|
|
1160
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1161
|
+
d: "M8 1.5l1.3 3.7L13 6.5l-3.7 1.3L8 11.5 6.7 7.8 3 6.5l3.7-1.3L8 1.5z",
|
|
1162
|
+
fill: "currentColor"
|
|
1163
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1164
|
+
d: "M12.5 10.5l.6 1.7 1.7.6-1.7.6-.6 1.7-.6-1.7-1.7-.6 1.7-.6.6-1.7z",
|
|
1165
|
+
fill: "currentColor",
|
|
1166
|
+
opacity: "0.6"
|
|
1167
|
+
})]
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Extract recent conversation context from the message array. Returns a
|
|
1172
|
+
* plain-text summary of the last N user/assistant turns, formatted as:
|
|
1173
|
+
*
|
|
1174
|
+
* User: <message text>
|
|
1175
|
+
* Assistant: <response text (truncated at 500 chars)>
|
|
1176
|
+
*
|
|
1177
|
+
* Returns an empty string when there are no user/assistant nodes.
|
|
1178
|
+
*/
|
|
1179
|
+
function extractConversationContext(nodes, maxTurns) {
|
|
1180
|
+
const relevant = [];
|
|
1181
|
+
for (const node of nodes) if (node.kind === "user") {
|
|
1182
|
+
const text = node.content.filter((c) => c.type === "text").map((c) => c.text).join("");
|
|
1183
|
+
if (text.trim()) relevant.push({
|
|
1184
|
+
role: "user",
|
|
1185
|
+
text: text.trim()
|
|
1186
|
+
});
|
|
1187
|
+
} else if (node.kind === "assistant") {
|
|
1188
|
+
const text = node.blocks.filter((b) => b.kind === "text").map((b) => b.text).join("");
|
|
1189
|
+
if (text.trim()) relevant.push({
|
|
1190
|
+
role: "assistant",
|
|
1191
|
+
text: text.trim().slice(0, 500)
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
const recent = relevant.slice(-maxTurns * 2);
|
|
1195
|
+
if (recent.length === 0) return "";
|
|
1196
|
+
return recent.map((entry) => entry.role === "user" ? `User: ${entry.text}` : `Assistant: ${entry.text}`).join("\n");
|
|
1197
|
+
}
|
|
1198
|
+
const containerStyle = {
|
|
1199
|
+
display: "inline-flex",
|
|
1200
|
+
alignItems: "center",
|
|
1201
|
+
height: 26,
|
|
1202
|
+
flex: "none"
|
|
1203
|
+
};
|
|
1204
|
+
const buttonStyle$1 = (disabled) => ({
|
|
1205
|
+
display: "inline-flex",
|
|
1206
|
+
alignItems: "center",
|
|
1207
|
+
gap: 4,
|
|
1208
|
+
height: 26,
|
|
1209
|
+
padding: "0 8px",
|
|
1210
|
+
border: "none",
|
|
1211
|
+
borderRadius: 6,
|
|
1212
|
+
background: "transparent",
|
|
1213
|
+
color: "var(--dsw-alias-label-secondary, inherit)",
|
|
1214
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
1215
|
+
opacity: disabled ? .5 : 1,
|
|
1216
|
+
fontSize: 12,
|
|
1217
|
+
flex: "none"
|
|
1218
|
+
});
|
|
1219
|
+
const modalOverlayStyle = {
|
|
1220
|
+
position: "fixed",
|
|
1221
|
+
inset: 0,
|
|
1222
|
+
background: "rgba(0,0,0,0.35)",
|
|
1223
|
+
display: "flex",
|
|
1224
|
+
alignItems: "center",
|
|
1225
|
+
justifyContent: "center",
|
|
1226
|
+
zIndex: 99998
|
|
1227
|
+
};
|
|
1228
|
+
const modalContentStyle = {
|
|
1229
|
+
padding: 14,
|
|
1230
|
+
borderRadius: 10,
|
|
1231
|
+
background: "var(--dsw-alias-bg-layer-2, #fff)",
|
|
1232
|
+
border: "1px solid var(--dsw-alias-border-l2, #e0e0e0)",
|
|
1233
|
+
boxShadow: "0 8px 32px rgba(0,0,0,0.2)",
|
|
1234
|
+
width: "min(640px, 92vw)",
|
|
1235
|
+
zIndex: 99999
|
|
1236
|
+
};
|
|
1237
|
+
const panelTitleStyle = {
|
|
1238
|
+
fontSize: 14,
|
|
1239
|
+
fontWeight: 600,
|
|
1240
|
+
marginBottom: 10,
|
|
1241
|
+
color: "var(--dsw-alias-label-primary, inherit)"
|
|
1242
|
+
};
|
|
1243
|
+
const compareRowStyle = {
|
|
1244
|
+
display: "flex",
|
|
1245
|
+
gap: 10,
|
|
1246
|
+
maxHeight: 300
|
|
1247
|
+
};
|
|
1248
|
+
const compareColStyle = {
|
|
1249
|
+
flex: 1,
|
|
1250
|
+
display: "flex",
|
|
1251
|
+
flexDirection: "column",
|
|
1252
|
+
minWidth: 0
|
|
1253
|
+
};
|
|
1254
|
+
const compareLabelStyle = {
|
|
1255
|
+
fontSize: 11,
|
|
1256
|
+
color: "var(--dsw-alias-label-secondary, #888)",
|
|
1257
|
+
marginBottom: 4
|
|
1258
|
+
};
|
|
1259
|
+
const preStyle = {
|
|
1260
|
+
margin: 0,
|
|
1261
|
+
padding: 10,
|
|
1262
|
+
fontSize: 12,
|
|
1263
|
+
lineHeight: 1.6,
|
|
1264
|
+
whiteSpace: "pre-wrap",
|
|
1265
|
+
wordBreak: "break-word",
|
|
1266
|
+
overflow: "auto",
|
|
1267
|
+
maxHeight: 240,
|
|
1268
|
+
borderRadius: 6,
|
|
1269
|
+
border: "1px solid var(--dsw-alias-border-l2, #e8e8e8)",
|
|
1270
|
+
background: "var(--dsw-alias-bg-layer-1, #f9f9f9)",
|
|
1271
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
1272
|
+
fontFamily: "inherit"
|
|
1273
|
+
};
|
|
1274
|
+
const panelActionsStyle = {
|
|
1275
|
+
display: "flex",
|
|
1276
|
+
justifyContent: "flex-end",
|
|
1277
|
+
gap: 8,
|
|
1278
|
+
marginTop: 12
|
|
1279
|
+
};
|
|
1280
|
+
const adoptBtnStyle = {
|
|
1281
|
+
padding: "6px 16px",
|
|
1282
|
+
border: "none",
|
|
1283
|
+
borderRadius: 6,
|
|
1284
|
+
background: "var(--dsw-alias-state-business-primary, #4f8cff)",
|
|
1285
|
+
color: "#fff",
|
|
1286
|
+
cursor: "pointer",
|
|
1287
|
+
fontSize: 13
|
|
1288
|
+
};
|
|
1289
|
+
const cancelBtnStyle = {
|
|
1290
|
+
padding: "6px 16px",
|
|
1291
|
+
border: "1px solid var(--dsw-alias-border-l2, #ddd)",
|
|
1292
|
+
borderRadius: 6,
|
|
1293
|
+
background: "transparent",
|
|
1294
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
1295
|
+
cursor: "pointer",
|
|
1296
|
+
fontSize: 13
|
|
1297
|
+
};
|
|
1298
|
+
const errorModalStyle = {
|
|
1299
|
+
padding: "10px 14px",
|
|
1300
|
+
borderRadius: 8,
|
|
1301
|
+
background: "var(--dsw-alias-bg-layer-2, #fff)",
|
|
1302
|
+
border: "1px solid var(--dsw-alias-state-error-secondary, #f5c2c7)",
|
|
1303
|
+
boxShadow: "0 4px 16px rgba(0,0,0,0.15)",
|
|
1304
|
+
color: "var(--dsw-alias-state-error-primary, #c33)",
|
|
1305
|
+
fontSize: 13,
|
|
1306
|
+
display: "flex",
|
|
1307
|
+
alignItems: "center",
|
|
1308
|
+
gap: 10,
|
|
1309
|
+
zIndex: 99999,
|
|
1310
|
+
maxWidth: 420,
|
|
1311
|
+
width: "min(420px, 90vw)"
|
|
1312
|
+
};
|
|
1313
|
+
const errorDismissStyle = {
|
|
1314
|
+
border: "none",
|
|
1315
|
+
background: "transparent",
|
|
1316
|
+
color: "inherit",
|
|
1317
|
+
cursor: "pointer",
|
|
1318
|
+
fontSize: 18,
|
|
1319
|
+
padding: 0,
|
|
1320
|
+
lineHeight: 1,
|
|
1321
|
+
flex: "none"
|
|
1322
|
+
};
|
|
1323
|
+
//#endregion
|
|
1324
|
+
//#region src/client/VoiceRecognitionBar.tsx
|
|
1325
|
+
/**
|
|
1326
|
+
* The recognition status bar above the composer. Shows live state and a stop
|
|
1327
|
+
* button while recording; the bar renders nothing when idle.
|
|
1328
|
+
*/
|
|
1329
|
+
function VoiceRecognitionBar({ voiceSession, t }) {
|
|
1330
|
+
const snapshot = useVoiceInputSession(voiceSession);
|
|
1331
|
+
const active = snapshot.state === "starting" || snapshot.state === "recording";
|
|
1332
|
+
const label = active ? t("listening") : snapshot.state === "transcribing" ? t("transcribing") : snapshot.state === "polishing" ? t("polishing") : snapshot.state === "polish-error" ? t("polishFailedKeepOriginal") : snapshot.state === "error" ? t("voiceFailed") : "";
|
|
1333
|
+
if (label === "") return null;
|
|
1334
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1335
|
+
"data-better-input-bar": "true",
|
|
1336
|
+
role: "status",
|
|
1337
|
+
style: {
|
|
1338
|
+
display: "flex",
|
|
1339
|
+
alignItems: "center",
|
|
1340
|
+
gap: 8,
|
|
1341
|
+
padding: "4px 10px",
|
|
1342
|
+
borderRadius: 8,
|
|
1343
|
+
fontSize: 12,
|
|
1344
|
+
lineHeight: "18px",
|
|
1345
|
+
background: "var(--dsw-alias-bg-layer-3, rgba(0,0,0,0.04))",
|
|
1346
|
+
color: "var(--dsw-alias-label-primary, inherit)"
|
|
1347
|
+
},
|
|
1348
|
+
children: [
|
|
1349
|
+
active ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PulsingDot, {}) : null,
|
|
1350
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }),
|
|
1351
|
+
(snapshot.state === "error" || snapshot.state === "polish-error") && snapshot.detail !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1352
|
+
style: { opacity: .7 },
|
|
1353
|
+
children: snapshot.detail
|
|
1354
|
+
}) : null,
|
|
1355
|
+
active ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1356
|
+
type: "button",
|
|
1357
|
+
onClick: () => voiceSession.requestStop(),
|
|
1358
|
+
style: {
|
|
1359
|
+
marginLeft: "auto",
|
|
1360
|
+
padding: "2px 8px",
|
|
1361
|
+
border: "none",
|
|
1362
|
+
borderRadius: 6,
|
|
1363
|
+
fontSize: 12,
|
|
1364
|
+
background: "var(--dsw-alias-state-error-secondary, #e5484d)",
|
|
1365
|
+
color: "#fff",
|
|
1366
|
+
cursor: "pointer"
|
|
1367
|
+
},
|
|
1368
|
+
children: t("voiceStop")
|
|
1369
|
+
}) : null
|
|
1370
|
+
]
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
function PulsingDot() {
|
|
1374
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1375
|
+
"aria-hidden": "true",
|
|
1376
|
+
style: {
|
|
1377
|
+
width: 8,
|
|
1378
|
+
height: 8,
|
|
1379
|
+
borderRadius: "50%",
|
|
1380
|
+
background: "var(--dsw-alias-state-error-secondary, #e5484d)",
|
|
1381
|
+
animation: "sqs-dsh-better-input-pulse 1.2s ease-in-out infinite"
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
//#endregion
|
|
1386
|
+
//#region src/client/settings-controller.ts
|
|
1387
|
+
const EMPTY_ABOUT = {
|
|
1388
|
+
repository: "",
|
|
1389
|
+
repositorySlug: "",
|
|
1390
|
+
version: "",
|
|
1391
|
+
license: "",
|
|
1392
|
+
updateCommand: "",
|
|
1393
|
+
updateCommandNpx: ""
|
|
1394
|
+
};
|
|
1395
|
+
const EMPTY_VIEW = {
|
|
1396
|
+
available: false,
|
|
1397
|
+
writable: false,
|
|
1398
|
+
settings: { ...DEFAULT_SETTINGS },
|
|
1399
|
+
overridden: [],
|
|
1400
|
+
defaultPolishPrompt: "",
|
|
1401
|
+
defaultOptimizePrompt: ""
|
|
1402
|
+
};
|
|
1403
|
+
/**
|
|
1404
|
+
* Settings read/write controller for the settings page and the microphone
|
|
1405
|
+
* flow. Owns the remote calls and a simple external store so both the page
|
|
1406
|
+
* and the voice button observe the same values. Also caches per-model
|
|
1407
|
+
* reasoning-effort metadata loaded lazily through resolveModelEfforts so
|
|
1408
|
+
* opening the effort dropdown never double-fetches across renders.
|
|
1409
|
+
*/
|
|
1410
|
+
var SettingsController = class {
|
|
1411
|
+
remote;
|
|
1412
|
+
settingsSnapshot = {
|
|
1413
|
+
status: "loading",
|
|
1414
|
+
view: EMPTY_VIEW,
|
|
1415
|
+
detail: ""
|
|
1416
|
+
};
|
|
1417
|
+
routesSnapshot = {
|
|
1418
|
+
status: "loading",
|
|
1419
|
+
routes: [],
|
|
1420
|
+
detail: ""
|
|
1421
|
+
};
|
|
1422
|
+
effortsSnapshot = {};
|
|
1423
|
+
aboutSnapshot = {
|
|
1424
|
+
status: "loading",
|
|
1425
|
+
about: EMPTY_ABOUT,
|
|
1426
|
+
detail: ""
|
|
1427
|
+
};
|
|
1428
|
+
updateSnapshot = {
|
|
1429
|
+
status: "idle",
|
|
1430
|
+
update: null,
|
|
1431
|
+
detail: ""
|
|
1432
|
+
};
|
|
1433
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1434
|
+
disposed = false;
|
|
1435
|
+
constructor(remote) {
|
|
1436
|
+
this.remote = remote;
|
|
1437
|
+
}
|
|
1438
|
+
getSettingsSnapshot = () => this.settingsSnapshot;
|
|
1439
|
+
getRoutesSnapshot = () => this.routesSnapshot;
|
|
1440
|
+
getEffortsSnapshot = () => this.effortsSnapshot;
|
|
1441
|
+
getAboutSnapshot = () => this.aboutSnapshot;
|
|
1442
|
+
getUpdateSnapshot = () => this.updateSnapshot;
|
|
1443
|
+
subscribe = (listener) => {
|
|
1444
|
+
this.listeners.add(listener);
|
|
1445
|
+
return () => this.listeners.delete(listener);
|
|
1446
|
+
};
|
|
1447
|
+
async refreshSettings() {
|
|
1448
|
+
const result = await this.remote.getSettings();
|
|
1449
|
+
if (this.disposed) return;
|
|
1450
|
+
if (!result.ok) this.settingsSnapshot = {
|
|
1451
|
+
status: "error",
|
|
1452
|
+
view: EMPTY_VIEW,
|
|
1453
|
+
detail: result.error.message
|
|
1454
|
+
};
|
|
1455
|
+
else {
|
|
1456
|
+
this.settingsSnapshot = {
|
|
1457
|
+
status: "ready",
|
|
1458
|
+
view: result.value,
|
|
1459
|
+
detail: ""
|
|
1460
|
+
};
|
|
1461
|
+
this.autoPopulateDefaultRoutesIfNeeded(result.value.settings);
|
|
1462
|
+
}
|
|
1463
|
+
this.emit();
|
|
1464
|
+
}
|
|
1465
|
+
async refreshRoutes() {
|
|
1466
|
+
const result = await this.remote.listRoutes();
|
|
1467
|
+
if (this.disposed) return;
|
|
1468
|
+
if (!result.ok) this.routesSnapshot = {
|
|
1469
|
+
status: "error",
|
|
1470
|
+
routes: [],
|
|
1471
|
+
detail: result.error.message
|
|
1472
|
+
};
|
|
1473
|
+
else {
|
|
1474
|
+
this.routesSnapshot = {
|
|
1475
|
+
status: "ready",
|
|
1476
|
+
routes: result.value,
|
|
1477
|
+
detail: ""
|
|
1478
|
+
};
|
|
1479
|
+
if (this.settingsSnapshot.status === "ready") this.autoPopulateDefaultRoutesIfNeeded(this.settingsSnapshot.view.settings);
|
|
1480
|
+
}
|
|
1481
|
+
this.emit();
|
|
1482
|
+
}
|
|
1483
|
+
autoPopulateDefaultRoutesIfNeeded = async (settings) => {
|
|
1484
|
+
const polishEmpty = settings.polishProvider === "" || settings.polishModel === "";
|
|
1485
|
+
const optimizeEmpty = settings.optimizeProvider === "" || settings.optimizeModel === "";
|
|
1486
|
+
if (!polishEmpty && !optimizeEmpty) return;
|
|
1487
|
+
const first = (this.routesSnapshot.status === "ready" && this.routesSnapshot.routes.length > 0 ? this.routesSnapshot.routes : await (async () => {
|
|
1488
|
+
if (this.disposed) return [];
|
|
1489
|
+
const rs = await this.remote.listRoutes();
|
|
1490
|
+
if (this.disposed || !rs.ok) return [];
|
|
1491
|
+
this.routesSnapshot = {
|
|
1492
|
+
status: "ready",
|
|
1493
|
+
routes: rs.value,
|
|
1494
|
+
detail: ""
|
|
1495
|
+
};
|
|
1496
|
+
this.emit();
|
|
1497
|
+
return rs.value;
|
|
1498
|
+
})())[0];
|
|
1499
|
+
if (first === void 0) return;
|
|
1500
|
+
const patch = {};
|
|
1501
|
+
if (polishEmpty) {
|
|
1502
|
+
patch.polishProvider = first.provider;
|
|
1503
|
+
patch.polishModel = first.model;
|
|
1504
|
+
}
|
|
1505
|
+
if (optimizeEmpty) {
|
|
1506
|
+
patch.optimizeProvider = first.provider;
|
|
1507
|
+
patch.optimizeModel = first.model;
|
|
1508
|
+
}
|
|
1509
|
+
await this.update(patch);
|
|
1510
|
+
};
|
|
1511
|
+
async update(patch) {
|
|
1512
|
+
const result = await this.remote.updateSettings(patch);
|
|
1513
|
+
if (this.disposed) return false;
|
|
1514
|
+
if (!result.ok) return false;
|
|
1515
|
+
this.settingsSnapshot = {
|
|
1516
|
+
status: "ready",
|
|
1517
|
+
view: result.value,
|
|
1518
|
+
detail: ""
|
|
1519
|
+
};
|
|
1520
|
+
this.emit();
|
|
1521
|
+
return true;
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Lazily fetch reasoning efforts for a route. Results are cached in the
|
|
1525
|
+
* controller so changing the effort dropdown back and forth doesn't
|
|
1526
|
+
* re-trigger remote calls. Returns a snapshot entry immediately — the
|
|
1527
|
+
* caller subscribes via `useEffortsSnapshot` to re-render when the data
|
|
1528
|
+
* lands.
|
|
1529
|
+
*/
|
|
1530
|
+
async ensureEffortsFor(provider, model) {
|
|
1531
|
+
if (provider === "" || model === "" || this.disposed) return;
|
|
1532
|
+
const key = `${provider}\u0000${model}`;
|
|
1533
|
+
if (this.effortsSnapshot[key] !== void 0) return;
|
|
1534
|
+
this.effortsSnapshot = {
|
|
1535
|
+
...this.effortsSnapshot,
|
|
1536
|
+
[key]: {
|
|
1537
|
+
status: "loading",
|
|
1538
|
+
efforts: [],
|
|
1539
|
+
detail: ""
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
this.emit();
|
|
1543
|
+
try {
|
|
1544
|
+
const result = await this.remote.resolveModelEfforts(provider, model);
|
|
1545
|
+
if (this.disposed) return;
|
|
1546
|
+
if (result.ok) this.effortsSnapshot = {
|
|
1547
|
+
...this.effortsSnapshot,
|
|
1548
|
+
[key]: {
|
|
1549
|
+
status: "ready",
|
|
1550
|
+
efforts: result.value.efforts,
|
|
1551
|
+
defaultEffort: result.value.defaultEffort,
|
|
1552
|
+
detail: ""
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
else this.effortsSnapshot = {
|
|
1556
|
+
...this.effortsSnapshot,
|
|
1557
|
+
[key]: {
|
|
1558
|
+
status: "error",
|
|
1559
|
+
efforts: [],
|
|
1560
|
+
detail: result.error.message
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
} catch (error) {
|
|
1564
|
+
if (this.disposed) return;
|
|
1565
|
+
this.effortsSnapshot = {
|
|
1566
|
+
...this.effortsSnapshot,
|
|
1567
|
+
[key]: {
|
|
1568
|
+
status: "error",
|
|
1569
|
+
efforts: [],
|
|
1570
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
this.emit();
|
|
1575
|
+
}
|
|
1576
|
+
async refreshAbout() {
|
|
1577
|
+
const result = await this.remote.getAbout();
|
|
1578
|
+
if (this.disposed) return;
|
|
1579
|
+
if (!result.ok) this.aboutSnapshot = {
|
|
1580
|
+
status: "error",
|
|
1581
|
+
about: EMPTY_ABOUT,
|
|
1582
|
+
detail: result.error.message
|
|
1583
|
+
};
|
|
1584
|
+
else this.aboutSnapshot = {
|
|
1585
|
+
status: "ready",
|
|
1586
|
+
about: result.value,
|
|
1587
|
+
detail: ""
|
|
1588
|
+
};
|
|
1589
|
+
this.emit();
|
|
1590
|
+
}
|
|
1591
|
+
async checkForUpdate() {
|
|
1592
|
+
if (this.disposed) return;
|
|
1593
|
+
if (this.updateSnapshot.status === "loading") return;
|
|
1594
|
+
this.updateSnapshot = {
|
|
1595
|
+
status: "loading",
|
|
1596
|
+
update: null,
|
|
1597
|
+
detail: ""
|
|
1598
|
+
};
|
|
1599
|
+
this.emit();
|
|
1600
|
+
try {
|
|
1601
|
+
const result = await this.remote.checkForUpdate();
|
|
1602
|
+
if (this.disposed) return;
|
|
1603
|
+
if (result.ok) this.updateSnapshot = {
|
|
1604
|
+
status: "ready",
|
|
1605
|
+
update: result.value,
|
|
1606
|
+
detail: ""
|
|
1607
|
+
};
|
|
1608
|
+
else this.updateSnapshot = {
|
|
1609
|
+
status: "error",
|
|
1610
|
+
update: null,
|
|
1611
|
+
detail: result.error.message
|
|
1612
|
+
};
|
|
1613
|
+
} catch (error) {
|
|
1614
|
+
if (this.disposed) return;
|
|
1615
|
+
this.updateSnapshot = {
|
|
1616
|
+
status: "error",
|
|
1617
|
+
update: null,
|
|
1618
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
this.emit();
|
|
1622
|
+
}
|
|
1623
|
+
dispose() {
|
|
1624
|
+
this.disposed = true;
|
|
1625
|
+
this.listeners.clear();
|
|
1626
|
+
}
|
|
1627
|
+
emit() {
|
|
1628
|
+
for (const listener of this.listeners) listener();
|
|
1629
|
+
}
|
|
1630
|
+
};
|
|
1631
|
+
function useSettingsSnapshot(controller) {
|
|
1632
|
+
return (0, react.useSyncExternalStore)(controller.subscribe, controller.getSettingsSnapshot, controller.getSettingsSnapshot);
|
|
1633
|
+
}
|
|
1634
|
+
function useRoutesSnapshot(controller) {
|
|
1635
|
+
return (0, react.useSyncExternalStore)(controller.subscribe, controller.getRoutesSnapshot, controller.getRoutesSnapshot);
|
|
1636
|
+
}
|
|
1637
|
+
function useEffortsSnapshot(controller) {
|
|
1638
|
+
return (0, react.useSyncExternalStore)(controller.subscribe, controller.getEffortsSnapshot, controller.getEffortsSnapshot);
|
|
1639
|
+
}
|
|
1640
|
+
function useAboutSnapshot(controller) {
|
|
1641
|
+
return (0, react.useSyncExternalStore)(controller.subscribe, controller.getAboutSnapshot, controller.getAboutSnapshot);
|
|
1642
|
+
}
|
|
1643
|
+
function useUpdateSnapshot(controller) {
|
|
1644
|
+
return (0, react.useSyncExternalStore)(controller.subscribe, controller.getUpdateSnapshot, controller.getUpdateSnapshot);
|
|
1645
|
+
}
|
|
1646
|
+
//#endregion
|
|
1647
|
+
//#region src/client/settings.tsx
|
|
1648
|
+
function ReasoningEffortSelect(props) {
|
|
1649
|
+
const { settingsController, provider, model, storedEffort, onChange, t } = props;
|
|
1650
|
+
const efforts = useEffortsSnapshot(settingsController);
|
|
1651
|
+
(0, react.useEffect)(() => {
|
|
1652
|
+
if (provider === "" || model === "") return;
|
|
1653
|
+
settingsController.ensureEffortsFor(provider, model);
|
|
1654
|
+
}, [
|
|
1655
|
+
settingsController,
|
|
1656
|
+
provider,
|
|
1657
|
+
model
|
|
1658
|
+
]);
|
|
1659
|
+
if (provider === "" || model === "") return null;
|
|
1660
|
+
const entry = efforts[`${provider}\u0000${model}`];
|
|
1661
|
+
if (entry === void 0 || entry.status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
1662
|
+
value: "",
|
|
1663
|
+
disabled: true,
|
|
1664
|
+
style: inputStyle$1,
|
|
1665
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1666
|
+
value: "",
|
|
1667
|
+
children: t("effortLoadingLabel")
|
|
1668
|
+
})
|
|
1669
|
+
});
|
|
1670
|
+
if (entry.status === "error" || entry.efforts.length === 0) return null;
|
|
1671
|
+
const items = entry.efforts;
|
|
1672
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1673
|
+
value: storedEffort,
|
|
1674
|
+
onChange: (event) => onChange(event.target.value),
|
|
1675
|
+
style: inputStyle$1,
|
|
1676
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1677
|
+
value: "",
|
|
1678
|
+
children: t("effortDefaultLabel")
|
|
1679
|
+
}), items.map((effort) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1680
|
+
value: effort.id,
|
|
1681
|
+
title: effort.description,
|
|
1682
|
+
children: effort.name
|
|
1683
|
+
}, effort.id))]
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* The BetterInput settings page. Renders the recognition and polishing
|
|
1688
|
+
* configuration; every field edits a local draft and saves on blur/change.
|
|
1689
|
+
*/
|
|
1690
|
+
function BetterInputSettingsSection({ close, settingsController, t }) {
|
|
1691
|
+
const settings = useSettingsSnapshot(settingsController);
|
|
1692
|
+
const routes = useRoutesSnapshot(settingsController);
|
|
1693
|
+
const about = useAboutSnapshot(settingsController);
|
|
1694
|
+
const update = useUpdateSnapshot(settingsController);
|
|
1695
|
+
const [drafts, setDrafts] = (0, react.useState)({});
|
|
1696
|
+
const [saveFailed, setSaveFailed] = (0, react.useState)(false);
|
|
1697
|
+
const [showDefaultPrompt, setShowDefaultPrompt] = (0, react.useState)(false);
|
|
1698
|
+
const [showDefaultOptimizePrompt, setShowDefaultOptimizePrompt] = (0, react.useState)(false);
|
|
1699
|
+
const draftsRef = (0, react.useRef)(drafts);
|
|
1700
|
+
draftsRef.current = drafts;
|
|
1701
|
+
(0, react.useEffect)(() => {
|
|
1702
|
+
settingsController.refreshSettings();
|
|
1703
|
+
settingsController.refreshRoutes();
|
|
1704
|
+
settingsController.refreshAbout();
|
|
1705
|
+
}, [settingsController]);
|
|
1706
|
+
if (settings.status === "loading" || routes.status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionFrame$1, {
|
|
1707
|
+
title: t("settingsTitle"),
|
|
1708
|
+
children: t("loading")
|
|
1709
|
+
});
|
|
1710
|
+
const field = (name, current) => ({
|
|
1711
|
+
text: drafts[name] ?? current,
|
|
1712
|
+
invalid: false
|
|
1713
|
+
});
|
|
1714
|
+
const setField = (name, value) => {
|
|
1715
|
+
setDrafts((prev) => ({
|
|
1716
|
+
...prev,
|
|
1717
|
+
[name]: value
|
|
1718
|
+
}));
|
|
1719
|
+
};
|
|
1720
|
+
const save = async (patch) => {
|
|
1721
|
+
setSaveFailed(false);
|
|
1722
|
+
if (await settingsController.update(patch)) setDrafts((prev) => {
|
|
1723
|
+
const next = { ...prev };
|
|
1724
|
+
for (const key of Object.keys(patch)) delete next[key];
|
|
1725
|
+
return next;
|
|
1726
|
+
});
|
|
1727
|
+
else setSaveFailed(true);
|
|
1728
|
+
};
|
|
1729
|
+
const s = settings.view.settings;
|
|
1730
|
+
const languageField = field("language", s.language);
|
|
1731
|
+
const secondsField = field("maxRecordingSeconds", String(s.maxRecordingSeconds));
|
|
1732
|
+
const polishPromptField = field("polishPrompt", s.polishPrompt);
|
|
1733
|
+
const optimizePromptField = field("optimizePrompt", s.optimizePrompt);
|
|
1734
|
+
const contextTurnsField = field("contextTurns", String(s.contextTurns));
|
|
1735
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SectionFrame$1, {
|
|
1736
|
+
title: t("settingsTitle"),
|
|
1737
|
+
children: [
|
|
1738
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1739
|
+
style: hintStyle$1,
|
|
1740
|
+
children: t("settingsDescription")
|
|
1741
|
+
}),
|
|
1742
|
+
saveFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1743
|
+
style: errorStyle$1,
|
|
1744
|
+
children: t("saveFailed")
|
|
1745
|
+
}) : null,
|
|
1746
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1747
|
+
style: sectionTitleStyle,
|
|
1748
|
+
children: t("voiceSectionLabel")
|
|
1749
|
+
}),
|
|
1750
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
1751
|
+
label: t("languageLabel"),
|
|
1752
|
+
hint: t("languageHint"),
|
|
1753
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1754
|
+
type: "text",
|
|
1755
|
+
value: languageField.text,
|
|
1756
|
+
placeholder: t("languagePlaceholder"),
|
|
1757
|
+
onChange: (event) => setField("language", event.target.value),
|
|
1758
|
+
onBlur: () => void save({ language: languageField.text.trim() }),
|
|
1759
|
+
style: inputStyle$1
|
|
1760
|
+
})
|
|
1761
|
+
}),
|
|
1762
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
1763
|
+
label: t("recordingLimitLabel"),
|
|
1764
|
+
hint: t("recordingLimitHint"),
|
|
1765
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1766
|
+
type: "number",
|
|
1767
|
+
min: 1,
|
|
1768
|
+
max: 600,
|
|
1769
|
+
value: secondsField.text,
|
|
1770
|
+
onChange: (event) => setField("maxRecordingSeconds", event.target.value),
|
|
1771
|
+
onBlur: () => {
|
|
1772
|
+
const parsed = Number(secondsField.text);
|
|
1773
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 600) return;
|
|
1774
|
+
save({ maxRecordingSeconds: parsed });
|
|
1775
|
+
},
|
|
1776
|
+
style: inputStyle$1
|
|
1777
|
+
})
|
|
1778
|
+
}),
|
|
1779
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1780
|
+
style: sectionTitleStyle,
|
|
1781
|
+
children: t("polishSectionLabel")
|
|
1782
|
+
}),
|
|
1783
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
1784
|
+
label: t("polishLabel"),
|
|
1785
|
+
hint: t("polishHint"),
|
|
1786
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1787
|
+
style: switchStyle,
|
|
1788
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1789
|
+
type: "checkbox",
|
|
1790
|
+
checked: s.polishingEnabled,
|
|
1791
|
+
onChange: (event) => void save({ polishingEnabled: event.target.checked })
|
|
1792
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: s.polishingEnabled ? t("on") : t("off") })]
|
|
1793
|
+
})
|
|
1794
|
+
}),
|
|
1795
|
+
s.polishingEnabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1796
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
1797
|
+
label: t("polishModelLabel"),
|
|
1798
|
+
hint: t("polishModelHint"),
|
|
1799
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1800
|
+
value: drafts.polishProvider !== void 0 || drafts.polishModel !== void 0 ? `${drafts.polishProvider ?? s.polishProvider}\u0000${drafts.polishModel ?? s.polishModel}` : `${s.polishProvider}\u0000${s.polishModel}`,
|
|
1801
|
+
onChange: (event) => {
|
|
1802
|
+
const [provider, model] = event.target.value.split("\0");
|
|
1803
|
+
save({
|
|
1804
|
+
polishProvider: provider ?? "",
|
|
1805
|
+
polishModel: model ?? ""
|
|
1806
|
+
});
|
|
1807
|
+
},
|
|
1808
|
+
style: inputStyle$1,
|
|
1809
|
+
disabled: routes.status !== "ready" || routes.routes.length === 0,
|
|
1810
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1811
|
+
value: "\0",
|
|
1812
|
+
children: t("polishModelNone")
|
|
1813
|
+
}), routes.status === "ready" && routes.routes.map((route) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
1814
|
+
value: `${route.provider}\u0000${route.model}`,
|
|
1815
|
+
children: [
|
|
1816
|
+
route.providerName,
|
|
1817
|
+
" / ",
|
|
1818
|
+
route.modelName
|
|
1819
|
+
]
|
|
1820
|
+
}, `${route.provider}\u0000${route.model}`))]
|
|
1821
|
+
})
|
|
1822
|
+
}),
|
|
1823
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
1824
|
+
label: t("polishEffortLabel"),
|
|
1825
|
+
hint: t("polishEffortHint"),
|
|
1826
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReasoningEffortSelect, {
|
|
1827
|
+
settingsController,
|
|
1828
|
+
provider: s.polishProvider,
|
|
1829
|
+
model: s.polishModel,
|
|
1830
|
+
storedEffort: s.polishReasoningEffort,
|
|
1831
|
+
onChange: (effortId) => void save({ polishReasoningEffort: effortId }),
|
|
1832
|
+
t
|
|
1833
|
+
})
|
|
1834
|
+
}),
|
|
1835
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Field, {
|
|
1836
|
+
label: t("polishPromptLabel"),
|
|
1837
|
+
hint: t("polishPromptHint"),
|
|
1838
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
1839
|
+
value: polishPromptField.text,
|
|
1840
|
+
rows: 5,
|
|
1841
|
+
placeholder: t("polishPromptPlaceholder"),
|
|
1842
|
+
onChange: (event) => setField("polishPrompt", event.target.value),
|
|
1843
|
+
onBlur: () => void save({ polishPrompt: polishPromptField.text }),
|
|
1844
|
+
style: {
|
|
1845
|
+
...inputStyle$1,
|
|
1846
|
+
resize: "vertical",
|
|
1847
|
+
fontFamily: "monospace"
|
|
1848
|
+
}
|
|
1849
|
+
}), settings.view.defaultPolishPrompt !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1850
|
+
type: "button",
|
|
1851
|
+
onClick: () => setShowDefaultPrompt((prev) => !prev),
|
|
1852
|
+
style: toggleLinkStyle,
|
|
1853
|
+
children: showDefaultPrompt ? t("hideDefaultPrompt") : t("showDefaultPrompt")
|
|
1854
|
+
}), showDefaultPrompt ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
1855
|
+
style: {
|
|
1856
|
+
margin: "6px 0 0",
|
|
1857
|
+
padding: 8,
|
|
1858
|
+
maxHeight: 220,
|
|
1859
|
+
overflow: "auto",
|
|
1860
|
+
whiteSpace: "pre-wrap",
|
|
1861
|
+
wordBreak: "break-word",
|
|
1862
|
+
fontSize: 11,
|
|
1863
|
+
lineHeight: 1.5,
|
|
1864
|
+
background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.03))",
|
|
1865
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.3))",
|
|
1866
|
+
borderRadius: 6,
|
|
1867
|
+
fontFamily: "monospace"
|
|
1868
|
+
},
|
|
1869
|
+
children: settings.view.defaultPolishPrompt
|
|
1870
|
+
}) : null] }) : null]
|
|
1871
|
+
})
|
|
1872
|
+
] }) : null,
|
|
1873
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1874
|
+
style: {
|
|
1875
|
+
margin: "16px 0 0",
|
|
1876
|
+
fontSize: 14
|
|
1877
|
+
},
|
|
1878
|
+
children: t("optimizeSectionLabel")
|
|
1879
|
+
}),
|
|
1880
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
1881
|
+
label: t("optimizeModelLabel"),
|
|
1882
|
+
hint: t("optimizeModelHint"),
|
|
1883
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1884
|
+
value: drafts.optimizeProvider !== void 0 || drafts.optimizeModel !== void 0 ? `${drafts.optimizeProvider ?? s.optimizeProvider}\u0000${drafts.optimizeModel ?? s.optimizeModel}` : `${s.optimizeProvider}\u0000${s.optimizeModel}`,
|
|
1885
|
+
onChange: (event) => {
|
|
1886
|
+
const [provider, model] = event.target.value.split("\0");
|
|
1887
|
+
save({
|
|
1888
|
+
optimizeProvider: provider ?? "",
|
|
1889
|
+
optimizeModel: model ?? ""
|
|
1890
|
+
});
|
|
1891
|
+
},
|
|
1892
|
+
style: inputStyle$1,
|
|
1893
|
+
disabled: routes.status !== "ready" || routes.routes.length === 0,
|
|
1894
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1895
|
+
value: "\0",
|
|
1896
|
+
children: t("polishModelNone")
|
|
1897
|
+
}), routes.status === "ready" && routes.routes.map((route) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
1898
|
+
value: `${route.provider}\u0000${route.model}`,
|
|
1899
|
+
children: [
|
|
1900
|
+
route.providerName,
|
|
1901
|
+
" / ",
|
|
1902
|
+
route.modelName
|
|
1903
|
+
]
|
|
1904
|
+
}, `${route.provider}\u0000${route.model}`))]
|
|
1905
|
+
})
|
|
1906
|
+
}),
|
|
1907
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
1908
|
+
label: t("optimizeEffortLabel"),
|
|
1909
|
+
hint: t("optimizeEffortHint"),
|
|
1910
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReasoningEffortSelect, {
|
|
1911
|
+
settingsController,
|
|
1912
|
+
provider: s.optimizeProvider,
|
|
1913
|
+
model: s.optimizeModel,
|
|
1914
|
+
storedEffort: s.optimizeReasoningEffort,
|
|
1915
|
+
onChange: (effortId) => void save({ optimizeReasoningEffort: effortId }),
|
|
1916
|
+
t
|
|
1917
|
+
})
|
|
1918
|
+
}),
|
|
1919
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Field, {
|
|
1920
|
+
label: t("optimizePromptLabel"),
|
|
1921
|
+
hint: t("optimizePromptHint"),
|
|
1922
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
1923
|
+
value: optimizePromptField.text,
|
|
1924
|
+
rows: 5,
|
|
1925
|
+
placeholder: t("optimizePromptPlaceholder"),
|
|
1926
|
+
onChange: (event) => setField("optimizePrompt", event.target.value),
|
|
1927
|
+
onBlur: () => void save({ optimizePrompt: optimizePromptField.text }),
|
|
1928
|
+
style: {
|
|
1929
|
+
...inputStyle$1,
|
|
1930
|
+
resize: "vertical",
|
|
1931
|
+
fontFamily: "monospace"
|
|
1932
|
+
}
|
|
1933
|
+
}), settings.view.defaultOptimizePrompt !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1934
|
+
type: "button",
|
|
1935
|
+
onClick: () => setShowDefaultOptimizePrompt((prev) => !prev),
|
|
1936
|
+
style: toggleLinkStyle,
|
|
1937
|
+
children: showDefaultOptimizePrompt ? t("hideDefaultPrompt") : t("showDefaultPrompt")
|
|
1938
|
+
}), showDefaultOptimizePrompt ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
1939
|
+
style: {
|
|
1940
|
+
margin: "6px 0 0",
|
|
1941
|
+
padding: 8,
|
|
1942
|
+
maxHeight: 220,
|
|
1943
|
+
overflow: "auto",
|
|
1944
|
+
whiteSpace: "pre-wrap",
|
|
1945
|
+
wordBreak: "break-word",
|
|
1946
|
+
fontSize: 11,
|
|
1947
|
+
lineHeight: 1.5,
|
|
1948
|
+
background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.03))",
|
|
1949
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.3))",
|
|
1950
|
+
borderRadius: 6,
|
|
1951
|
+
fontFamily: "monospace"
|
|
1952
|
+
},
|
|
1953
|
+
children: settings.view.defaultOptimizePrompt
|
|
1954
|
+
}) : null] }) : null]
|
|
1955
|
+
}),
|
|
1956
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
1957
|
+
label: t("contextTurnsLabel"),
|
|
1958
|
+
hint: t("contextTurnsHint"),
|
|
1959
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1960
|
+
type: "number",
|
|
1961
|
+
min: 0,
|
|
1962
|
+
max: 20,
|
|
1963
|
+
value: contextTurnsField.text,
|
|
1964
|
+
onChange: (event) => setField("contextTurns", event.target.value),
|
|
1965
|
+
onBlur: () => {
|
|
1966
|
+
const parsed = Number(contextTurnsField.text);
|
|
1967
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0 || parsed > 20) return;
|
|
1968
|
+
save({ contextTurns: parsed });
|
|
1969
|
+
},
|
|
1970
|
+
style: inputStyle$1
|
|
1971
|
+
})
|
|
1972
|
+
}),
|
|
1973
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1974
|
+
style: hintStyle$1,
|
|
1975
|
+
children: [
|
|
1976
|
+
t("routesStatus"),
|
|
1977
|
+
": ",
|
|
1978
|
+
routes.status === "ready" ? `${routes.routes.length}` : routes.detail || t("routesUnavailable")
|
|
1979
|
+
]
|
|
1980
|
+
}),
|
|
1981
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("hr", { style: dividerStyle }),
|
|
1982
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AboutUpdateSection, {
|
|
1983
|
+
aboutStatus: about.status,
|
|
1984
|
+
version: about.about.version,
|
|
1985
|
+
repository: about.about.repository,
|
|
1986
|
+
license: about.about.license,
|
|
1987
|
+
update,
|
|
1988
|
+
t,
|
|
1989
|
+
onCheckUpdate: () => void settingsController.checkForUpdate()
|
|
1990
|
+
})
|
|
1991
|
+
]
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
function AboutUpdateSection(props) {
|
|
1995
|
+
const { aboutStatus, version, repository, license, update, t, onCheckUpdate } = props;
|
|
1996
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1997
|
+
style: {
|
|
1998
|
+
display: "flex",
|
|
1999
|
+
flexDirection: "column",
|
|
2000
|
+
gap: 8
|
|
2001
|
+
},
|
|
2002
|
+
children: [
|
|
2003
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
2004
|
+
style: {
|
|
2005
|
+
margin: 0,
|
|
2006
|
+
fontSize: 14
|
|
2007
|
+
},
|
|
2008
|
+
children: t("aboutTitle")
|
|
2009
|
+
}),
|
|
2010
|
+
aboutStatus === "ready" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2011
|
+
style: {
|
|
2012
|
+
display: "flex",
|
|
2013
|
+
flexDirection: "column",
|
|
2014
|
+
gap: 2,
|
|
2015
|
+
fontSize: 13,
|
|
2016
|
+
opacity: .85
|
|
2017
|
+
},
|
|
2018
|
+
children: [
|
|
2019
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
2020
|
+
t("aboutVersionLabel"),
|
|
2021
|
+
": ",
|
|
2022
|
+
version || "—"
|
|
2023
|
+
] }),
|
|
2024
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
2025
|
+
t("aboutLicenseLabel"),
|
|
2026
|
+
": ",
|
|
2027
|
+
license || "—"
|
|
2028
|
+
] }),
|
|
2029
|
+
repository !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
|
|
2030
|
+
href: repository,
|
|
2031
|
+
target: "_blank",
|
|
2032
|
+
rel: "noreferrer",
|
|
2033
|
+
style: { color: "var(--dsw-alias-state-business-primary, #4f8cff)" },
|
|
2034
|
+
children: [
|
|
2035
|
+
t("aboutRepositoryLabel"),
|
|
2036
|
+
": ",
|
|
2037
|
+
repository
|
|
2038
|
+
]
|
|
2039
|
+
}) : null,
|
|
2040
|
+
repository !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
2041
|
+
href: `${repository.replace(/\/+$/, "")}/blob/main/CHANGELOG.md`,
|
|
2042
|
+
target: "_blank",
|
|
2043
|
+
rel: "noreferrer",
|
|
2044
|
+
style: { color: "var(--dsw-alias-state-business-primary, #4f8cff)" },
|
|
2045
|
+
children: t("aboutChangelogLabel")
|
|
2046
|
+
}) : null
|
|
2047
|
+
]
|
|
2048
|
+
}) : null,
|
|
2049
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2050
|
+
style: {
|
|
2051
|
+
display: "flex",
|
|
2052
|
+
flexDirection: "column",
|
|
2053
|
+
gap: 6
|
|
2054
|
+
},
|
|
2055
|
+
children: [
|
|
2056
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2057
|
+
type: "button",
|
|
2058
|
+
onClick: onCheckUpdate,
|
|
2059
|
+
disabled: update.status === "loading",
|
|
2060
|
+
style: {
|
|
2061
|
+
width: "fit-content",
|
|
2062
|
+
padding: "6px 12px",
|
|
2063
|
+
borderRadius: 6,
|
|
2064
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4))",
|
|
2065
|
+
background: "var(--dsh-color-surface, transparent)",
|
|
2066
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
2067
|
+
fontSize: 13,
|
|
2068
|
+
cursor: update.status === "loading" ? "default" : "pointer"
|
|
2069
|
+
},
|
|
2070
|
+
children: update.status === "loading" ? t("checkingUpdate") : t("checkUpdateButton")
|
|
2071
|
+
}),
|
|
2072
|
+
update.status === "ready" && update.update !== null ? (() => {
|
|
2073
|
+
const status = update.update.status;
|
|
2074
|
+
if (status === "up-to-date") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2075
|
+
style: hintStyle$1,
|
|
2076
|
+
children: t("updateUpToDate")
|
|
2077
|
+
});
|
|
2078
|
+
if (status === "unpublished") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2079
|
+
style: hintStyle$1,
|
|
2080
|
+
children: t("updateUnpublished")
|
|
2081
|
+
});
|
|
2082
|
+
if (status === "update-available") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2083
|
+
style: {
|
|
2084
|
+
display: "flex",
|
|
2085
|
+
flexDirection: "column",
|
|
2086
|
+
gap: 4
|
|
2087
|
+
},
|
|
2088
|
+
children: [
|
|
2089
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2090
|
+
style: {
|
|
2091
|
+
...hintStyle$1,
|
|
2092
|
+
color: "var(--dsw-alias-state-error-primary, #e5484d)"
|
|
2093
|
+
},
|
|
2094
|
+
children: [
|
|
2095
|
+
t("updateAvailable"),
|
|
2096
|
+
": ",
|
|
2097
|
+
update.update.installed,
|
|
2098
|
+
" → ",
|
|
2099
|
+
update.update.latest
|
|
2100
|
+
]
|
|
2101
|
+
}),
|
|
2102
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2103
|
+
style: hintStyle$1,
|
|
2104
|
+
children: [t("updateCommandLabel"), ":"]
|
|
2105
|
+
}),
|
|
2106
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
2107
|
+
style: codeStyle,
|
|
2108
|
+
children: update.update.updateCommand
|
|
2109
|
+
}),
|
|
2110
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2111
|
+
style: hintStyle$1,
|
|
2112
|
+
children: [t("updateCommandNpxLabel"), ":"]
|
|
2113
|
+
}),
|
|
2114
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
2115
|
+
style: codeStyle,
|
|
2116
|
+
children: update.update.updateCommandNpx
|
|
2117
|
+
}),
|
|
2118
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2119
|
+
style: hintStyle$1,
|
|
2120
|
+
children: [t("updateCommandPick"), ":"]
|
|
2121
|
+
})
|
|
2122
|
+
]
|
|
2123
|
+
});
|
|
2124
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2125
|
+
style: errorStyle$1,
|
|
2126
|
+
children: t("updateCheckFailed")
|
|
2127
|
+
});
|
|
2128
|
+
})() : null,
|
|
2129
|
+
update.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2130
|
+
style: errorStyle$1,
|
|
2131
|
+
children: [
|
|
2132
|
+
t("updateCheckFailed"),
|
|
2133
|
+
": ",
|
|
2134
|
+
update.detail
|
|
2135
|
+
]
|
|
2136
|
+
}) : null
|
|
2137
|
+
]
|
|
2138
|
+
})
|
|
2139
|
+
]
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
function SectionFrame$1({ title, children }) {
|
|
2143
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2144
|
+
style: {
|
|
2145
|
+
padding: 16,
|
|
2146
|
+
display: "flex",
|
|
2147
|
+
flexDirection: "column",
|
|
2148
|
+
gap: 12
|
|
2149
|
+
},
|
|
2150
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
2151
|
+
style: {
|
|
2152
|
+
margin: 0,
|
|
2153
|
+
fontSize: 16
|
|
2154
|
+
},
|
|
2155
|
+
children: title
|
|
2156
|
+
}), children]
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
2159
|
+
function Field({ label, hint, children }) {
|
|
2160
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2161
|
+
style: {
|
|
2162
|
+
display: "flex",
|
|
2163
|
+
flexDirection: "column",
|
|
2164
|
+
gap: 4
|
|
2165
|
+
},
|
|
2166
|
+
children: [
|
|
2167
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2168
|
+
style: {
|
|
2169
|
+
fontSize: 13,
|
|
2170
|
+
fontWeight: 600
|
|
2171
|
+
},
|
|
2172
|
+
children: label
|
|
2173
|
+
}),
|
|
2174
|
+
children,
|
|
2175
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2176
|
+
style: hintStyle$1,
|
|
2177
|
+
children: hint
|
|
2178
|
+
})
|
|
2179
|
+
]
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
const inputStyle$1 = {
|
|
2183
|
+
padding: "6px 8px",
|
|
2184
|
+
borderRadius: 6,
|
|
2185
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4))",
|
|
2186
|
+
background: "var(--dsw-alias-bg-layer-1, #f9f9f9)",
|
|
2187
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
2188
|
+
fontSize: 13
|
|
2189
|
+
};
|
|
2190
|
+
const hintStyle$1 = {
|
|
2191
|
+
margin: 0,
|
|
2192
|
+
fontSize: 12,
|
|
2193
|
+
opacity: .7
|
|
2194
|
+
};
|
|
2195
|
+
const errorStyle$1 = {
|
|
2196
|
+
margin: 0,
|
|
2197
|
+
fontSize: 12,
|
|
2198
|
+
color: "var(--dsw-alias-state-error-primary, #e5484d)"
|
|
2199
|
+
};
|
|
2200
|
+
const switchStyle = {
|
|
2201
|
+
display: "flex",
|
|
2202
|
+
alignItems: "center",
|
|
2203
|
+
gap: 8,
|
|
2204
|
+
fontSize: 13
|
|
2205
|
+
};
|
|
2206
|
+
const toggleLinkStyle = {
|
|
2207
|
+
marginTop: 6,
|
|
2208
|
+
padding: 0,
|
|
2209
|
+
border: "none",
|
|
2210
|
+
background: "none",
|
|
2211
|
+
color: "var(--dsw-alias-state-business-primary, #4f8cff)",
|
|
2212
|
+
fontSize: 12,
|
|
2213
|
+
cursor: "pointer",
|
|
2214
|
+
textDecoration: "underline"
|
|
2215
|
+
};
|
|
2216
|
+
const dividerStyle = {
|
|
2217
|
+
margin: "8px 0",
|
|
2218
|
+
border: "none",
|
|
2219
|
+
borderTop: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.3))"
|
|
2220
|
+
};
|
|
2221
|
+
const sectionTitleStyle = {
|
|
2222
|
+
margin: "16px 0 0",
|
|
2223
|
+
fontSize: 14
|
|
2224
|
+
};
|
|
2225
|
+
const codeStyle = {
|
|
2226
|
+
padding: "6px 8px",
|
|
2227
|
+
overflow: "auto",
|
|
2228
|
+
whiteSpace: "pre-wrap",
|
|
2229
|
+
wordBreak: "break-all",
|
|
2230
|
+
fontSize: 12,
|
|
2231
|
+
fontFamily: "monospace",
|
|
2232
|
+
background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.03))",
|
|
2233
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.3))",
|
|
2234
|
+
borderRadius: 6
|
|
2235
|
+
};
|
|
2236
|
+
//#endregion
|
|
2237
|
+
//#region src/client/templates-source.ts
|
|
2238
|
+
/** Unique source name on the `/` trigger (the menu group title). */
|
|
2239
|
+
const TEMPLATES_SOURCE_NAME = "better-input-templates";
|
|
2240
|
+
const MAX_CANDIDATES = 50;
|
|
2241
|
+
/**
|
|
2242
|
+
* The input-trigger source that inserts stored prompt templates as literal
|
|
2243
|
+
* text on the `/` trigger.
|
|
2244
|
+
*
|
|
2245
|
+
* - `warm()` prefetches the template list when the input box mounts, so the
|
|
2246
|
+
* first `/` keystroke already shows entries.
|
|
2247
|
+
* - `candidates()` filters by name / description / tag substring.
|
|
2248
|
+
* - `onPick()` returns `{ text: content }` — the trigger token span is
|
|
2249
|
+
* replaced by the template body, so `/name` never persists in the draft
|
|
2250
|
+
* and no lexicon/codec registration is needed.
|
|
2251
|
+
*/
|
|
2252
|
+
function createTemplatesSource(controller) {
|
|
2253
|
+
return {
|
|
2254
|
+
trigger: "/",
|
|
2255
|
+
name: TEMPLATES_SOURCE_NAME,
|
|
2256
|
+
order: 300,
|
|
2257
|
+
showGroupTitle: true,
|
|
2258
|
+
async candidates(_session, req) {
|
|
2259
|
+
const query = req.query.trim().toLowerCase();
|
|
2260
|
+
const templates = controller.getSnapshot().templates;
|
|
2261
|
+
return (query === "" ? templates : templates.filter((template) => template.name.toLowerCase().includes(query) || template.description !== "" && template.description.toLowerCase().includes(query) || template.tags.some((tag) => tag.toLowerCase().includes(query)))).slice(0, MAX_CANDIDATES).map((template) => ({
|
|
2262
|
+
name: template.name,
|
|
2263
|
+
...template.description === "" ? {} : { description: template.description },
|
|
2264
|
+
...template.tags.length === 0 ? {} : { hint: template.tags.join(" / ") },
|
|
2265
|
+
value: template.id
|
|
2266
|
+
}));
|
|
2267
|
+
},
|
|
2268
|
+
onPick(pick) {
|
|
2269
|
+
const id = pick.candidate.value ?? "";
|
|
2270
|
+
const template = controller.byId(id);
|
|
2271
|
+
if (template === void 0) return;
|
|
2272
|
+
return { text: template.content };
|
|
2273
|
+
},
|
|
2274
|
+
warm(_session) {
|
|
2275
|
+
controller.ensureLoaded();
|
|
2276
|
+
}
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
//#endregion
|
|
2280
|
+
//#region src/client/templates-controller.ts
|
|
2281
|
+
/**
|
|
2282
|
+
* Client-side store for the prompt template library. Mirrors the settings
|
|
2283
|
+
* controller: an external-store class consumed via useSyncExternalStore,
|
|
2284
|
+
* one sticky initial fetch (also kicked off by the `/` trigger warm hook),
|
|
2285
|
+
* and optimistic list updates after save/remove so the menu and the settings
|
|
2286
|
+
* section stay in sync without refetching.
|
|
2287
|
+
*/
|
|
2288
|
+
const EMPTY_SNAPSHOT = {
|
|
2289
|
+
status: "loading",
|
|
2290
|
+
templates: [],
|
|
2291
|
+
detail: ""
|
|
2292
|
+
};
|
|
2293
|
+
function sortByRecency(templates) {
|
|
2294
|
+
return [...templates].sort((left, right) => right.updatedAt - left.updatedAt);
|
|
2295
|
+
}
|
|
2296
|
+
/** Split on ASCII and full-width commas, trim, drop empties. */
|
|
2297
|
+
function parseTags(raw) {
|
|
2298
|
+
return raw.split(/[,,]/).map((tag) => tag.trim()).filter((tag) => tag !== "");
|
|
2299
|
+
}
|
|
2300
|
+
var TemplatesController = class {
|
|
2301
|
+
remote;
|
|
2302
|
+
snapshot = EMPTY_SNAPSHOT;
|
|
2303
|
+
listeners = /* @__PURE__ */ new Set();
|
|
2304
|
+
disposed = false;
|
|
2305
|
+
loadPromise;
|
|
2306
|
+
constructor(remote) {
|
|
2307
|
+
this.remote = remote;
|
|
2308
|
+
}
|
|
2309
|
+
getSnapshot = () => this.snapshot;
|
|
2310
|
+
subscribe = (listener) => {
|
|
2311
|
+
this.listeners.add(listener);
|
|
2312
|
+
return () => this.listeners.delete(listener);
|
|
2313
|
+
};
|
|
2314
|
+
byId = (id) => this.snapshot.templates.find((template) => template.id === id);
|
|
2315
|
+
/** Kick off the one-time initial fetch; safe to call repeatedly. */
|
|
2316
|
+
ensureLoaded = () => {
|
|
2317
|
+
if (this.loadPromise !== void 0 || this.disposed) return;
|
|
2318
|
+
this.loadPromise = this.refresh();
|
|
2319
|
+
};
|
|
2320
|
+
async refresh() {
|
|
2321
|
+
try {
|
|
2322
|
+
const result = await this.remote.templatesList();
|
|
2323
|
+
if (this.disposed) return;
|
|
2324
|
+
if (result.ok) this.snapshot = {
|
|
2325
|
+
status: "ready",
|
|
2326
|
+
templates: sortByRecency(result.value.templates),
|
|
2327
|
+
detail: ""
|
|
2328
|
+
};
|
|
2329
|
+
else this.snapshot = {
|
|
2330
|
+
status: "error",
|
|
2331
|
+
templates: [],
|
|
2332
|
+
detail: result.error.message
|
|
2333
|
+
};
|
|
2334
|
+
} catch (error) {
|
|
2335
|
+
if (this.disposed) return;
|
|
2336
|
+
this.snapshot = {
|
|
2337
|
+
status: "error",
|
|
2338
|
+
templates: [],
|
|
2339
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
this.emit();
|
|
2343
|
+
}
|
|
2344
|
+
/** Create or update one template; resolves false when the call failed. */
|
|
2345
|
+
async save(draft) {
|
|
2346
|
+
const tags = parseTags(draft.tags);
|
|
2347
|
+
const input = {
|
|
2348
|
+
name: draft.name,
|
|
2349
|
+
content: draft.content,
|
|
2350
|
+
...draft.id === void 0 ? {} : { id: draft.id },
|
|
2351
|
+
...draft.description === "" ? {} : { description: draft.description },
|
|
2352
|
+
...tags.length === 0 ? {} : { tags }
|
|
2353
|
+
};
|
|
2354
|
+
try {
|
|
2355
|
+
const result = await this.remote.templatesSave(input);
|
|
2356
|
+
if (this.disposed || !result.ok) return false;
|
|
2357
|
+
const saved = result.value.template;
|
|
2358
|
+
const rest = this.snapshot.templates.filter((template) => template.id !== saved.id);
|
|
2359
|
+
this.snapshot = {
|
|
2360
|
+
...this.snapshot,
|
|
2361
|
+
status: "ready",
|
|
2362
|
+
templates: sortByRecency([...rest, saved])
|
|
2363
|
+
};
|
|
2364
|
+
this.emit();
|
|
2365
|
+
return true;
|
|
2366
|
+
} catch {
|
|
2367
|
+
return false;
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
/** Remove one template; resolves false when the call failed. */
|
|
2371
|
+
async remove(id) {
|
|
2372
|
+
try {
|
|
2373
|
+
const result = await this.remote.templatesRemove(id);
|
|
2374
|
+
if (this.disposed || !result.ok || !result.value.removed) return false;
|
|
2375
|
+
this.snapshot = {
|
|
2376
|
+
...this.snapshot,
|
|
2377
|
+
templates: this.snapshot.templates.filter((template) => template.id !== id)
|
|
2378
|
+
};
|
|
2379
|
+
this.emit();
|
|
2380
|
+
return true;
|
|
2381
|
+
} catch {
|
|
2382
|
+
return false;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
dispose() {
|
|
2386
|
+
this.disposed = true;
|
|
2387
|
+
this.listeners.clear();
|
|
2388
|
+
}
|
|
2389
|
+
emit() {
|
|
2390
|
+
for (const listener of this.listeners) listener();
|
|
2391
|
+
}
|
|
2392
|
+
};
|
|
2393
|
+
function useTemplatesSnapshot(controller) {
|
|
2394
|
+
return (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
|
|
2395
|
+
}
|
|
2396
|
+
//#endregion
|
|
2397
|
+
//#region src/templates/model.ts
|
|
2398
|
+
const MAX_TEMPLATE_CONTENT_LENGTH = 8e3;
|
|
2399
|
+
//#endregion
|
|
2400
|
+
//#region src/client/templates-section.tsx
|
|
2401
|
+
/**
|
|
2402
|
+
* Settings section for the prompt template library: list saved templates,
|
|
2403
|
+
* create/edit through an inline form, and delete with a two-step confirm.
|
|
2404
|
+
* Mirrors the settings section conventions (framework-injected `t` + local
|
|
2405
|
+
* frame/field components + dsw CSS variables).
|
|
2406
|
+
*/
|
|
2407
|
+
const EMPTY_DRAFT = {
|
|
2408
|
+
name: "",
|
|
2409
|
+
description: "",
|
|
2410
|
+
tags: "",
|
|
2411
|
+
content: ""
|
|
2412
|
+
};
|
|
2413
|
+
function draftFromTemplate(template) {
|
|
2414
|
+
return {
|
|
2415
|
+
id: template.id,
|
|
2416
|
+
name: template.name,
|
|
2417
|
+
description: template.description,
|
|
2418
|
+
tags: [...template.tags].join(", "),
|
|
2419
|
+
content: template.content
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2422
|
+
function TemplatesSection({ t, templatesController }) {
|
|
2423
|
+
const snapshot = useTemplatesSnapshot(templatesController);
|
|
2424
|
+
const [editor, setEditor] = (0, react.useState)({ kind: "closed" });
|
|
2425
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
2426
|
+
const [actionFailed, setActionFailed] = (0, react.useState)(false);
|
|
2427
|
+
const [armedDeleteId, setArmedDeleteId] = (0, react.useState)(null);
|
|
2428
|
+
(0, react.useEffect)(() => {
|
|
2429
|
+
templatesController.refresh();
|
|
2430
|
+
}, [templatesController]);
|
|
2431
|
+
if (snapshot.status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionFrame, {
|
|
2432
|
+
title: t("templatesTitle"),
|
|
2433
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2434
|
+
style: hintStyle,
|
|
2435
|
+
children: t("loading")
|
|
2436
|
+
})
|
|
2437
|
+
});
|
|
2438
|
+
if (snapshot.status === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SectionFrame, {
|
|
2439
|
+
title: t("templatesTitle"),
|
|
2440
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2441
|
+
style: errorStyle,
|
|
2442
|
+
children: [
|
|
2443
|
+
t("templatesLoadFailed"),
|
|
2444
|
+
": ",
|
|
2445
|
+
snapshot.detail
|
|
2446
|
+
]
|
|
2447
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2448
|
+
type: "button",
|
|
2449
|
+
style: buttonStyle,
|
|
2450
|
+
onClick: () => void templatesController.refresh(),
|
|
2451
|
+
children: t("templatesRetry")
|
|
2452
|
+
})]
|
|
2453
|
+
});
|
|
2454
|
+
const editing = editor.kind === "open" ? editor.draft : null;
|
|
2455
|
+
const saveDraft = async () => {
|
|
2456
|
+
if (editing === null || busy) return;
|
|
2457
|
+
if (editing.name.trim() === "" || editing.content.trim() === "") return;
|
|
2458
|
+
setBusy(true);
|
|
2459
|
+
setActionFailed(false);
|
|
2460
|
+
const ok = await templatesController.save(editing);
|
|
2461
|
+
setBusy(false);
|
|
2462
|
+
if (ok) setEditor({ kind: "closed" });
|
|
2463
|
+
else setActionFailed(true);
|
|
2464
|
+
};
|
|
2465
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SectionFrame, {
|
|
2466
|
+
title: t("templatesTitle"),
|
|
2467
|
+
children: [
|
|
2468
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2469
|
+
style: hintStyle,
|
|
2470
|
+
children: t("templatesDescription")
|
|
2471
|
+
}),
|
|
2472
|
+
actionFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2473
|
+
style: errorStyle,
|
|
2474
|
+
children: t("templatesActionFailed")
|
|
2475
|
+
}) : null,
|
|
2476
|
+
editing === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2477
|
+
type: "button",
|
|
2478
|
+
style: buttonStyle,
|
|
2479
|
+
onClick: () => {
|
|
2480
|
+
setActionFailed(false);
|
|
2481
|
+
setArmedDeleteId(null);
|
|
2482
|
+
setEditor({
|
|
2483
|
+
kind: "open",
|
|
2484
|
+
draft: EMPTY_DRAFT
|
|
2485
|
+
});
|
|
2486
|
+
},
|
|
2487
|
+
children: t("templatesNew")
|
|
2488
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2489
|
+
style: editorStyle,
|
|
2490
|
+
children: [
|
|
2491
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2492
|
+
style: {
|
|
2493
|
+
display: "flex",
|
|
2494
|
+
flexDirection: "column",
|
|
2495
|
+
gap: 4
|
|
2496
|
+
},
|
|
2497
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2498
|
+
style: labelStyle,
|
|
2499
|
+
children: t("templatesNameLabel")
|
|
2500
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2501
|
+
value: editing.name,
|
|
2502
|
+
maxLength: 60,
|
|
2503
|
+
placeholder: t("templatesNamePlaceholder"),
|
|
2504
|
+
onChange: (event) => setEditor({
|
|
2505
|
+
kind: "open",
|
|
2506
|
+
draft: {
|
|
2507
|
+
...editing,
|
|
2508
|
+
name: event.target.value
|
|
2509
|
+
}
|
|
2510
|
+
}),
|
|
2511
|
+
style: inputStyle
|
|
2512
|
+
})]
|
|
2513
|
+
}),
|
|
2514
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2515
|
+
style: {
|
|
2516
|
+
display: "flex",
|
|
2517
|
+
flexDirection: "column",
|
|
2518
|
+
gap: 4
|
|
2519
|
+
},
|
|
2520
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2521
|
+
style: labelStyle,
|
|
2522
|
+
children: t("templatesDescriptionLabel")
|
|
2523
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2524
|
+
value: editing.description,
|
|
2525
|
+
maxLength: 200,
|
|
2526
|
+
placeholder: t("templatesDescriptionPlaceholder"),
|
|
2527
|
+
onChange: (event) => setEditor({
|
|
2528
|
+
kind: "open",
|
|
2529
|
+
draft: {
|
|
2530
|
+
...editing,
|
|
2531
|
+
description: event.target.value
|
|
2532
|
+
}
|
|
2533
|
+
}),
|
|
2534
|
+
style: inputStyle
|
|
2535
|
+
})]
|
|
2536
|
+
}),
|
|
2537
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2538
|
+
style: {
|
|
2539
|
+
display: "flex",
|
|
2540
|
+
flexDirection: "column",
|
|
2541
|
+
gap: 4
|
|
2542
|
+
},
|
|
2543
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2544
|
+
style: labelStyle,
|
|
2545
|
+
children: t("templatesTagsLabel")
|
|
2546
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2547
|
+
value: editing.tags,
|
|
2548
|
+
placeholder: t("templatesTagsHint"),
|
|
2549
|
+
onChange: (event) => setEditor({
|
|
2550
|
+
kind: "open",
|
|
2551
|
+
draft: {
|
|
2552
|
+
...editing,
|
|
2553
|
+
tags: event.target.value
|
|
2554
|
+
}
|
|
2555
|
+
}),
|
|
2556
|
+
style: inputStyle
|
|
2557
|
+
})]
|
|
2558
|
+
}),
|
|
2559
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2560
|
+
style: {
|
|
2561
|
+
display: "flex",
|
|
2562
|
+
flexDirection: "column",
|
|
2563
|
+
gap: 4
|
|
2564
|
+
},
|
|
2565
|
+
children: [
|
|
2566
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2567
|
+
style: labelStyle,
|
|
2568
|
+
children: t("templatesContentLabel")
|
|
2569
|
+
}),
|
|
2570
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
2571
|
+
value: editing.content,
|
|
2572
|
+
rows: 6,
|
|
2573
|
+
maxLength: MAX_TEMPLATE_CONTENT_LENGTH,
|
|
2574
|
+
placeholder: t("templatesContentPlaceholder"),
|
|
2575
|
+
onChange: (event) => setEditor({
|
|
2576
|
+
kind: "open",
|
|
2577
|
+
draft: {
|
|
2578
|
+
...editing,
|
|
2579
|
+
content: event.target.value
|
|
2580
|
+
}
|
|
2581
|
+
}),
|
|
2582
|
+
style: {
|
|
2583
|
+
...inputStyle,
|
|
2584
|
+
resize: "vertical",
|
|
2585
|
+
fontFamily: "monospace"
|
|
2586
|
+
}
|
|
2587
|
+
}),
|
|
2588
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2589
|
+
style: hintStyle,
|
|
2590
|
+
children: [
|
|
2591
|
+
editing.content.length,
|
|
2592
|
+
" / ",
|
|
2593
|
+
MAX_TEMPLATE_CONTENT_LENGTH
|
|
2594
|
+
]
|
|
2595
|
+
})
|
|
2596
|
+
]
|
|
2597
|
+
}),
|
|
2598
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2599
|
+
style: {
|
|
2600
|
+
display: "flex",
|
|
2601
|
+
gap: 8
|
|
2602
|
+
},
|
|
2603
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2604
|
+
type: "button",
|
|
2605
|
+
style: buttonStyle,
|
|
2606
|
+
disabled: busy || editing.name.trim() === "" || editing.content.trim() === "",
|
|
2607
|
+
onClick: () => void saveDraft(),
|
|
2608
|
+
children: t("templatesSave")
|
|
2609
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2610
|
+
type: "button",
|
|
2611
|
+
style: buttonStyle,
|
|
2612
|
+
disabled: busy,
|
|
2613
|
+
onClick: () => {
|
|
2614
|
+
setEditor({ kind: "closed" });
|
|
2615
|
+
setActionFailed(false);
|
|
2616
|
+
},
|
|
2617
|
+
children: t("templatesCancel")
|
|
2618
|
+
})]
|
|
2619
|
+
})
|
|
2620
|
+
]
|
|
2621
|
+
}),
|
|
2622
|
+
snapshot.templates.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2623
|
+
style: hintStyle,
|
|
2624
|
+
children: t("templatesEmpty")
|
|
2625
|
+
}) : null,
|
|
2626
|
+
snapshot.templates.map((template) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2627
|
+
style: cardStyle,
|
|
2628
|
+
children: [
|
|
2629
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2630
|
+
style: {
|
|
2631
|
+
display: "flex",
|
|
2632
|
+
alignItems: "baseline",
|
|
2633
|
+
justifyContent: "space-between",
|
|
2634
|
+
gap: 8
|
|
2635
|
+
},
|
|
2636
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
|
|
2637
|
+
style: { fontSize: 13 },
|
|
2638
|
+
children: template.name
|
|
2639
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2640
|
+
style: hintStyle,
|
|
2641
|
+
children: new Date(template.updatedAt).toLocaleDateString()
|
|
2642
|
+
})]
|
|
2643
|
+
}),
|
|
2644
|
+
template.description !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2645
|
+
style: hintStyle,
|
|
2646
|
+
children: template.description
|
|
2647
|
+
}) : null,
|
|
2648
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2649
|
+
style: previewStyle,
|
|
2650
|
+
children: template.content.length > 120 ? `${template.content.slice(0, 120)}…` : template.content
|
|
2651
|
+
}),
|
|
2652
|
+
template.tags.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2653
|
+
style: {
|
|
2654
|
+
display: "flex",
|
|
2655
|
+
flexWrap: "wrap",
|
|
2656
|
+
gap: 4
|
|
2657
|
+
},
|
|
2658
|
+
children: template.tags.map((tag) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2659
|
+
style: tagStyle,
|
|
2660
|
+
children: tag
|
|
2661
|
+
}, tag))
|
|
2662
|
+
}) : null,
|
|
2663
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2664
|
+
style: {
|
|
2665
|
+
display: "flex",
|
|
2666
|
+
gap: 8
|
|
2667
|
+
},
|
|
2668
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2669
|
+
type: "button",
|
|
2670
|
+
style: buttonStyle,
|
|
2671
|
+
onClick: () => {
|
|
2672
|
+
setActionFailed(false);
|
|
2673
|
+
setArmedDeleteId(null);
|
|
2674
|
+
setEditor({
|
|
2675
|
+
kind: "open",
|
|
2676
|
+
draft: draftFromTemplate(template)
|
|
2677
|
+
});
|
|
2678
|
+
},
|
|
2679
|
+
children: t("templatesEdit")
|
|
2680
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2681
|
+
type: "button",
|
|
2682
|
+
style: armedDeleteId === template.id ? deleteArmedStyle : buttonStyle,
|
|
2683
|
+
onClick: () => {
|
|
2684
|
+
if (armedDeleteId !== template.id) {
|
|
2685
|
+
setArmedDeleteId(template.id);
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
setArmedDeleteId(null);
|
|
2689
|
+
templatesController.remove(template.id).then((ok) => {
|
|
2690
|
+
if (!ok) setActionFailed(true);
|
|
2691
|
+
});
|
|
2692
|
+
},
|
|
2693
|
+
children: armedDeleteId === template.id ? t("templatesDeleteConfirm") : t("templatesDelete")
|
|
2694
|
+
})]
|
|
2695
|
+
})
|
|
2696
|
+
]
|
|
2697
|
+
}, template.id))
|
|
2698
|
+
]
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
function SectionFrame({ title, children }) {
|
|
2702
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2703
|
+
style: {
|
|
2704
|
+
padding: 16,
|
|
2705
|
+
display: "flex",
|
|
2706
|
+
flexDirection: "column",
|
|
2707
|
+
gap: 12
|
|
2708
|
+
},
|
|
2709
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
2710
|
+
style: {
|
|
2711
|
+
margin: 0,
|
|
2712
|
+
fontSize: 16
|
|
2713
|
+
},
|
|
2714
|
+
children: title
|
|
2715
|
+
}), children]
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
2718
|
+
const editorStyle = {
|
|
2719
|
+
display: "flex",
|
|
2720
|
+
flexDirection: "column",
|
|
2721
|
+
gap: 8,
|
|
2722
|
+
padding: 12,
|
|
2723
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4))",
|
|
2724
|
+
borderRadius: 6,
|
|
2725
|
+
background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.03))"
|
|
2726
|
+
};
|
|
2727
|
+
const cardStyle = {
|
|
2728
|
+
display: "flex",
|
|
2729
|
+
flexDirection: "column",
|
|
2730
|
+
gap: 6,
|
|
2731
|
+
padding: 12,
|
|
2732
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.3))",
|
|
2733
|
+
borderRadius: 6
|
|
2734
|
+
};
|
|
2735
|
+
const previewStyle = {
|
|
2736
|
+
margin: 0,
|
|
2737
|
+
fontSize: 12,
|
|
2738
|
+
fontFamily: "monospace",
|
|
2739
|
+
whiteSpace: "pre-wrap",
|
|
2740
|
+
wordBreak: "break-all",
|
|
2741
|
+
opacity: .8
|
|
2742
|
+
};
|
|
2743
|
+
const tagStyle = {
|
|
2744
|
+
padding: "1px 6px",
|
|
2745
|
+
fontSize: 11,
|
|
2746
|
+
borderRadius: 4,
|
|
2747
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.3))",
|
|
2748
|
+
opacity: .85
|
|
2749
|
+
};
|
|
2750
|
+
const labelStyle = {
|
|
2751
|
+
fontSize: 13,
|
|
2752
|
+
fontWeight: 600
|
|
2753
|
+
};
|
|
2754
|
+
const buttonStyle = {
|
|
2755
|
+
width: "fit-content",
|
|
2756
|
+
padding: "6px 12px",
|
|
2757
|
+
borderRadius: 6,
|
|
2758
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4))",
|
|
2759
|
+
background: "var(--dsh-color-surface, transparent)",
|
|
2760
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
2761
|
+
fontSize: 13,
|
|
2762
|
+
cursor: "pointer"
|
|
2763
|
+
};
|
|
2764
|
+
const deleteArmedStyle = {
|
|
2765
|
+
...buttonStyle,
|
|
2766
|
+
borderColor: "var(--dsw-alias-state-error-primary, #e5484d)",
|
|
2767
|
+
color: "var(--dsw-alias-state-error-primary, #e5484d)"
|
|
2768
|
+
};
|
|
2769
|
+
const inputStyle = {
|
|
2770
|
+
padding: "6px 8px",
|
|
2771
|
+
borderRadius: 6,
|
|
2772
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4))",
|
|
2773
|
+
background: "var(--dsw-alias-bg-layer-1, #f9f9f9)",
|
|
2774
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
2775
|
+
fontSize: 13
|
|
2776
|
+
};
|
|
2777
|
+
const hintStyle = {
|
|
2778
|
+
margin: 0,
|
|
2779
|
+
fontSize: 12,
|
|
2780
|
+
opacity: .7
|
|
2781
|
+
};
|
|
2782
|
+
const errorStyle = {
|
|
2783
|
+
margin: 0,
|
|
2784
|
+
fontSize: 12,
|
|
2785
|
+
color: "var(--dsw-alias-state-error-primary, #e5484d)"
|
|
2786
|
+
};
|
|
2787
|
+
//#endregion
|
|
2788
|
+
//#region src/client/index.ts
|
|
2789
|
+
const PULSE_KEYFRAMES = `@keyframes sqs-dsh-better-input-pulse {
|
|
2790
|
+
0%, 100% { opacity: 1; }
|
|
2791
|
+
50% { opacity: 0.3; }
|
|
2792
|
+
}`;
|
|
2793
|
+
/**
|
|
2794
|
+
* Required Client services: the slot registry, the Typert remote hub, and
|
|
2795
|
+
* the DSH locale runtime. `remote.betterInput` is mounted by this plugin's
|
|
2796
|
+
* own apply() via `ctx.remote.$mount`, so it MUST NOT appear here — the
|
|
2797
|
+
* outer inject gates plugin activation and would deadlock waiting for
|
|
2798
|
+
* itself. It is declared only on the inner ctx.inject() below, which runs
|
|
2799
|
+
* after the mount. Settings reach the browser through `SettingsScopeBinder`
|
|
2800
|
+
* (provided by `@deepseek-ai/dsh-client-ui-settings`) and are read inside
|
|
2801
|
+
* the settings section slot itself, not via a top-level `settings` service
|
|
2802
|
+
* here.
|
|
2803
|
+
*/
|
|
2804
|
+
const inject = [
|
|
2805
|
+
"slots",
|
|
2806
|
+
"remote",
|
|
2807
|
+
"locale",
|
|
2808
|
+
"inputTriggers",
|
|
2809
|
+
"conversation"
|
|
2810
|
+
];
|
|
2811
|
+
async function apply(ctx) {
|
|
2812
|
+
const disposeRemote = await ctx.remote.$mount(TYPERT_REMOTE);
|
|
2813
|
+
const disposeLocaleDicts = ctx.locale.register(BETTER_INPUT_NS, {
|
|
2814
|
+
zh,
|
|
2815
|
+
en
|
|
2816
|
+
});
|
|
2817
|
+
await ctx.inject([
|
|
2818
|
+
"slots",
|
|
2819
|
+
"remote",
|
|
2820
|
+
"remote.betterInput",
|
|
2821
|
+
"inputTriggers",
|
|
2822
|
+
"conversation"
|
|
2823
|
+
], async (remoteCtx) => {
|
|
2824
|
+
const remote = remoteCtx.remote.betterInput;
|
|
2825
|
+
const controller = new SettingsController(remote);
|
|
2826
|
+
const templatesController = new TemplatesController(remote);
|
|
2827
|
+
const voiceSessions = /* @__PURE__ */ new Map();
|
|
2828
|
+
const voiceSessionFor = (sessionId) => {
|
|
2829
|
+
let session = voiceSessions.get(sessionId);
|
|
2830
|
+
if (session === void 0) {
|
|
2831
|
+
session = new VoiceInputSession();
|
|
2832
|
+
voiceSessions.set(sessionId, session);
|
|
2833
|
+
}
|
|
2834
|
+
return session;
|
|
2835
|
+
};
|
|
2836
|
+
const disposeTemplatesSource = remoteCtx.inputTriggers.registerSource(createTemplatesSource(templatesController));
|
|
2837
|
+
remoteCtx.effect(() => () => {
|
|
2838
|
+
for (const session of voiceSessions.values()) session.dispose();
|
|
2839
|
+
voiceSessions.clear();
|
|
2840
|
+
disposeTemplatesSource();
|
|
2841
|
+
controller.dispose();
|
|
2842
|
+
templatesController.dispose();
|
|
2843
|
+
}, "sqs-dsh-better-input sessions lifecycle");
|
|
2844
|
+
remoteCtx.effect(() => {
|
|
2845
|
+
const styleTag = document.createElement("style");
|
|
2846
|
+
styleTag.dataset.plugin = "sqs-dsh-better-input";
|
|
2847
|
+
styleTag.textContent = PULSE_KEYFRAMES;
|
|
2848
|
+
document.head.appendChild(styleTag);
|
|
2849
|
+
return () => {
|
|
2850
|
+
styleTag.remove();
|
|
2851
|
+
};
|
|
2852
|
+
}, "sqs-dsh-better-input styles");
|
|
2853
|
+
controller.refreshSettings();
|
|
2854
|
+
controller.refreshRoutes();
|
|
2855
|
+
templatesController.ensureLoaded();
|
|
2856
|
+
const useSettings = () => {
|
|
2857
|
+
const snapshot = useSettingsSnapshot(controller);
|
|
2858
|
+
if (snapshot.status !== "ready") return {
|
|
2859
|
+
status: "loading",
|
|
2860
|
+
settings: snapshot.view.settings
|
|
2861
|
+
};
|
|
2862
|
+
return {
|
|
2863
|
+
status: "ready",
|
|
2864
|
+
settings: snapshot.view.settings
|
|
2865
|
+
};
|
|
2866
|
+
};
|
|
2867
|
+
remoteCtx.slots.inject("conversation.input.dock", () => remoteCtx.slots.register({
|
|
2868
|
+
name: "conversation.input.dock",
|
|
2869
|
+
id: "better-input-recognition-bar",
|
|
2870
|
+
order: 15,
|
|
2871
|
+
locale: BETTER_INPUT_NS,
|
|
2872
|
+
inject: (sessionId) => ({ voiceSession: voiceSessionFor(sessionId) })
|
|
2873
|
+
}, VoiceRecognitionBar));
|
|
2874
|
+
remoteCtx.slots.inject("conversation.input.right", () => remoteCtx.slots.register({
|
|
2875
|
+
name: "conversation.input.right",
|
|
2876
|
+
id: "better-input-optimize",
|
|
2877
|
+
order: 9998,
|
|
2878
|
+
locale: BETTER_INPUT_NS,
|
|
2879
|
+
inject: () => ({
|
|
2880
|
+
remote,
|
|
2881
|
+
useSettings
|
|
2882
|
+
})
|
|
2883
|
+
}, OptimizeButton));
|
|
2884
|
+
remoteCtx.slots.inject("conversation.input.right", () => remoteCtx.slots.register({
|
|
2885
|
+
name: "conversation.input.right",
|
|
2886
|
+
id: "better-input-voice",
|
|
2887
|
+
order: 9999,
|
|
2888
|
+
locale: BETTER_INPUT_NS,
|
|
2889
|
+
inject: (sessionId) => ({
|
|
2890
|
+
remote,
|
|
2891
|
+
voiceSession: voiceSessionFor(sessionId),
|
|
2892
|
+
useSettings
|
|
2893
|
+
})
|
|
2894
|
+
}, MicrophoneButton));
|
|
2895
|
+
remoteCtx.slots.inject("settings.section", () => remoteCtx.slots.register({
|
|
2896
|
+
name: "settings.section",
|
|
2897
|
+
id: "sqs-dsh-better-input",
|
|
2898
|
+
order: 16,
|
|
2899
|
+
label: () => ctx.locale.bind(BETTER_INPUT_NS)("settingsTitle"),
|
|
2900
|
+
locale: BETTER_INPUT_NS,
|
|
2901
|
+
inject: () => ({ settingsController: controller })
|
|
2902
|
+
}, BetterInputSettingsSection));
|
|
2903
|
+
remoteCtx.slots.inject("settings.section", () => remoteCtx.slots.register({
|
|
2904
|
+
name: "settings.section",
|
|
2905
|
+
id: "sqs-dsh-better-input-templates",
|
|
2906
|
+
order: 17,
|
|
2907
|
+
label: () => ctx.locale.bind(BETTER_INPUT_NS)("templatesTitle"),
|
|
2908
|
+
locale: BETTER_INPUT_NS,
|
|
2909
|
+
inject: () => ({ templatesController })
|
|
2910
|
+
}, TemplatesSection));
|
|
2911
|
+
return () => {};
|
|
2912
|
+
});
|
|
2913
|
+
return async () => {
|
|
2914
|
+
disposeLocaleDicts();
|
|
2915
|
+
await disposeRemote();
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
//#endregion
|
|
2919
|
+
exports.apply = apply;
|
|
2920
|
+
exports.inject = inject;
|
|
2921
|
+
return module.exports;
|
|
2922
|
+
}
|
|
2923
|
+
});
|
|
2924
|
+
|
|
2925
|
+
//# sourceMappingURL=client.js.map
|