dsh-custom-mode 2.2.5 → 2.2.6
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/README.i18n.yaml +2 -2
- package/README.md +43 -45
- package/README.zh.md +43 -45
- package/assistants.mjs +8 -2
- package/atomic.mjs +156 -56
- package/client.js +269 -211
- package/composition.mjs +5 -4
- package/index.mjs +49 -42
- package/locales.mjs +58 -20
- package/package.json +3 -3
- package/preset/prompt-reader.mjs +35 -2
- package/preset/prompt-tool.mjs +1 -0
- package/preset/prompt.md +0 -2
- package/preset-backend/declarative.mjs +13 -11
- package/seed.mjs +5 -1
package/client.js
CHANGED
|
@@ -58,15 +58,32 @@ try {
|
|
|
58
58
|
* truth, checked for key parity by test/locales.test.mjs).
|
|
59
59
|
*/
|
|
60
60
|
const ZH = {
|
|
61
|
+
"create.nextStep": "创建后可在上方继续编辑并保存;使用时,在新会话的模式菜单选择它。",
|
|
62
|
+
"btn.retryRead": "重试读取",
|
|
63
|
+
"btn.retrySync": "重试生效",
|
|
64
|
+
"api.savedButSyncFailed": "文件已保存,但这些助手尚未生效:{ids}。请点「重试生效」,无需重复原操作。",
|
|
65
|
+
"api.syncFailed": "这些助手仍未生效:{ids}。请重试;仍失败时查看宿主日志。",
|
|
66
|
+
"api.synchronized": "配置已生效。新会话请从模式菜单选择助手。",
|
|
67
|
+
"api.recoveryRequired": "助手文件需要恢复:{path}。请保留目录中的备份,并查看宿主日志中的恢复说明。",
|
|
68
|
+
"msg.completedReadFailed": "{result} 页面刷新失败。请点「重试读取」,无需重复原操作。",
|
|
69
|
+
"msg.synchronized": "配置已生效。",
|
|
70
|
+
"msg.syncFailed": "重新应用配置失败,请重试。",
|
|
71
|
+
"msg.loaded": "已载入到编辑器。尚未保存。",
|
|
72
|
+
"status.reading": "正在读取…",
|
|
73
|
+
"status.saving": "正在保存…",
|
|
74
|
+
"status.creating": "正在创建助手…",
|
|
75
|
+
"status.deleting": "正在删除助手…",
|
|
76
|
+
"status.reordering": "正在保存顺序…",
|
|
77
|
+
"status.repairing": "正在修复…",
|
|
78
|
+
"status.importing": "正在读取导入文件…",
|
|
79
|
+
"status.synchronizing": "正在应用配置…",
|
|
61
80
|
"nav": "自定义模式",
|
|
62
81
|
"assistant.heading": "助手",
|
|
63
|
-
"assistant.hint": "
|
|
64
|
-
"posture.hint": "
|
|
82
|
+
"assistant.hint": "选择要编辑的助手。每位助手独立保存系统提示词、基础模式和插件开关。",
|
|
83
|
+
"posture.hint": "选择起步姿态并命名,再点「新增助手」。",
|
|
65
84
|
"tree.posture": "起步",
|
|
66
|
-
"tree.base": "
|
|
67
|
-
"tree.
|
|
68
|
-
"tree.baseSkip": "这一支点不了。建好后到「当前助手」里改。",
|
|
69
|
-
"tree.baseLocked": "只有官方标准模式是亮的。其余四档是暗的,新建时点不了。",
|
|
85
|
+
"tree.base": "初始基础模式",
|
|
86
|
+
"tree.baseLocked": "新建助手从官方标准模式起步。建好后,可在「当前助手」调整基础模式。",
|
|
70
87
|
"posture.develop.label": "开发",
|
|
71
88
|
"posture.develop.note": "官方标准模式,工具按出厂。",
|
|
72
89
|
"posture.write.label": "写作",
|
|
@@ -74,7 +91,7 @@ try {
|
|
|
74
91
|
"posture.chat.label": "聊天",
|
|
75
92
|
"posture.chat.note": "官方标准模式。文件和终端关闭。",
|
|
76
93
|
"editor.heading": "当前助手",
|
|
77
|
-
"editor.hint": "
|
|
94
|
+
"editor.hint": "正在编辑「{name}」。这里的选择只决定编辑对象;新会话请从模式菜单选择助手。",
|
|
78
95
|
"assistant.empty": "还没有助手,用下面的输入框新建一个。",
|
|
79
96
|
"assistant.loadingList": "正在读取助手列表…",
|
|
80
97
|
"assistant.newPlaceholder": "新助手的名字(例如:写作助手)",
|
|
@@ -95,7 +112,7 @@ try {
|
|
|
95
112
|
"btn.resetHint": "把编辑器换回此助手新建时保存的提示词。只改草稿,保存后才写入。",
|
|
96
113
|
"btn.delete": "删除",
|
|
97
114
|
"btn.cancel": "取消",
|
|
98
|
-
"msg.created": "
|
|
115
|
+
"msg.created": "已创建。可继续编辑;使用时请在新会话的模式菜单选择它。",
|
|
99
116
|
"msg.duplicated": "已复制。两份配置彼此独立。",
|
|
100
117
|
"msg.deleted": "已删除。",
|
|
101
118
|
"msg.createFailed": "创建失败",
|
|
@@ -135,7 +152,7 @@ try {
|
|
|
135
152
|
"api.saved": "已保存({name},{mode})。下一步组装时会重读已保存的提示词。",
|
|
136
153
|
"api.conflict": "磁盘上的提示词已经变了,这次没有写入。先重新读取,再决定要不要覆盖。",
|
|
137
154
|
"api.savedWithExclusiveRows": "已保存({name},{mode})。已关闭不能同时启用的行:{rows}。",
|
|
138
|
-
"api.created": "已创建「{name}
|
|
155
|
+
"api.created": "已创建「{name}」。可继续编辑;使用时请在新会话的模式菜单选择它。",
|
|
139
156
|
"api.savedPrompt": "已保存系统提示词({name})。下一步组装时会重读这个文件。",
|
|
140
157
|
"api.duplicated": "已从「{from}」复制。两份互不影响。",
|
|
141
158
|
"api.deleted": "已删除「{name}」。正在使用它的会话不受影响。",
|
|
@@ -218,7 +235,7 @@ try {
|
|
|
218
235
|
"tag.essential": "核心",
|
|
219
236
|
"tag.followPlatform": "随平台",
|
|
220
237
|
"btn.save": "保存",
|
|
221
|
-
"btn.saving": "
|
|
238
|
+
"btn.saving": "保存中…",
|
|
222
239
|
"btn.reload": "重新读取",
|
|
223
240
|
"btn.reloadDiscard": "放弃修改并重新读取",
|
|
224
241
|
"btn.reloadConfirm": "放弃未保存的修改并重新读取?",
|
|
@@ -277,6 +294,8 @@ try {
|
|
|
277
294
|
"row.tool-plugin-manager.label": "插件管理(安装 / 启停)",
|
|
278
295
|
"row.tool-plugin-manager.note": "供模型安装或开关插件。",
|
|
279
296
|
"row.tool-presentation.label": "PTC 工具呈现",
|
|
297
|
+
"row.tool-executor-ptc.label": "PTC 程序执行器",
|
|
298
|
+
"row.tool-executor-ptc.note": "通过 run_code 执行程序并调用工具,同时向模型提供工具 SDK。",
|
|
280
299
|
"row.present.label": "交付文件(present)",
|
|
281
300
|
"row.command-goal.label": "目标命令",
|
|
282
301
|
"row.tool-goal.label": "目标",
|
|
@@ -301,15 +320,32 @@ try {
|
|
|
301
320
|
}
|
|
302
321
|
|
|
303
322
|
const EN = {
|
|
323
|
+
"create.nextStep": "After creating, edit and save above. To use the assistant, choose it in the mode menu for a new session.",
|
|
324
|
+
"btn.retryRead": "Retry refresh",
|
|
325
|
+
"btn.retrySync": "Retry activation",
|
|
326
|
+
"api.savedButSyncFailed": "Files are saved, but these assistants are not active yet: {ids}. Select Retry activation; you do not need to repeat the original action.",
|
|
327
|
+
"api.syncFailed": "These assistants are still not active: {ids}. Retry, then check the host log if the problem continues.",
|
|
328
|
+
"api.synchronized": "Configuration is active. Choose an assistant in the mode menu for a new session.",
|
|
329
|
+
"api.recoveryRequired": "Assistant files need recovery: {path}. Keep the backups in that directory and check the host log for recovery details.",
|
|
330
|
+
"msg.completedReadFailed": "{result} The page could not refresh. Select Retry refresh; do not repeat the original action.",
|
|
331
|
+
"msg.synchronized": "Configuration is active.",
|
|
332
|
+
"msg.syncFailed": "Could not apply the configuration. Please retry.",
|
|
333
|
+
"msg.loaded": "Loaded into the editor. Not saved yet.",
|
|
334
|
+
"status.reading": "Reading…",
|
|
335
|
+
"status.saving": "Saving…",
|
|
336
|
+
"status.creating": "Creating assistant…",
|
|
337
|
+
"status.deleting": "Deleting assistant…",
|
|
338
|
+
"status.reordering": "Saving order…",
|
|
339
|
+
"status.repairing": "Repairing…",
|
|
340
|
+
"status.importing": "Reading import file…",
|
|
341
|
+
"status.synchronizing": "Applying configuration…",
|
|
304
342
|
"nav": "Custom mode",
|
|
305
343
|
"assistant.heading": "Assistants",
|
|
306
|
-
"assistant.hint": "
|
|
307
|
-
"posture.hint": "
|
|
344
|
+
"assistant.hint": "Choose an assistant to edit. Each keeps its own system prompt, base mode and plugin switches.",
|
|
345
|
+
"posture.hint": "Choose a starting posture and a name, then select New assistant.",
|
|
308
346
|
"tree.posture": "Starter",
|
|
309
|
-
"tree.base": "
|
|
310
|
-
"tree.
|
|
311
|
-
"tree.baseSkip": "Cannot be chosen on this branch. Change it later under This assistant.",
|
|
312
|
-
"tree.baseLocked": "Only the official Standard mode is lit. The other four are dim and cannot be chosen when creating.",
|
|
347
|
+
"tree.base": "Starting base mode",
|
|
348
|
+
"tree.baseLocked": "New assistants start on the official Standard mode. You can change the base mode under This assistant after creating one.",
|
|
313
349
|
"posture.develop.label": "Coding",
|
|
314
350
|
"posture.develop.note": "Official Standard mode, tools as shipped.",
|
|
315
351
|
"posture.write.label": "Writing",
|
|
@@ -317,7 +353,7 @@ try {
|
|
|
317
353
|
"posture.chat.label": "Chat",
|
|
318
354
|
"posture.chat.note": "Official Standard mode. Files and terminal off.",
|
|
319
355
|
"editor.heading": "This assistant",
|
|
320
|
-
"editor.hint": "This
|
|
356
|
+
"editor.hint": "Editing {name}. This selection controls what you edit here; choose an assistant in the mode menu when starting a new session.",
|
|
321
357
|
"assistant.empty": "No assistants yet — create one with the field below.",
|
|
322
358
|
"assistant.loadingList": "Loading assistants…",
|
|
323
359
|
"assistant.newPlaceholder": "Name of the new assistant (e.g. Writing assistant)",
|
|
@@ -338,7 +374,7 @@ try {
|
|
|
338
374
|
"btn.resetHint": "Restores the prompt saved when this assistant was created. The editor changes. Save writes it.",
|
|
339
375
|
"btn.delete": "Delete",
|
|
340
376
|
"btn.cancel": "Cancel",
|
|
341
|
-
"msg.created": "Created.
|
|
377
|
+
"msg.created": "Created. Continue editing here, then choose it in the mode menu for a new session.",
|
|
342
378
|
"msg.duplicated": "Duplicated. The two are independent from here on.",
|
|
343
379
|
"msg.deleted": "Deleted.",
|
|
344
380
|
"msg.createFailed": "Could not create",
|
|
@@ -378,7 +414,7 @@ try {
|
|
|
378
414
|
"api.saved": "Saved ({name}, {mode}). The next model step rereads the saved prompt.",
|
|
379
415
|
"api.conflict": "The prompt on disk has changed, so this save was not written. Reload, then decide whether to overwrite.",
|
|
380
416
|
"api.savedWithExclusiveRows": "Saved ({name}, {mode}). Turned off rows that cannot be on together: {rows}.",
|
|
381
|
-
"api.created": "Created {name}.
|
|
417
|
+
"api.created": "Created {name}. Continue editing here, then choose it in the mode menu for a new session.",
|
|
382
418
|
"api.savedPrompt": "System prompt saved ({name}). The next model step rereads this file.",
|
|
383
419
|
"api.duplicated": "Copied from {from}. The two are independent from now on.",
|
|
384
420
|
"api.deleted": "Deleted {name}. Sessions already using it are unchanged.",
|
|
@@ -461,7 +497,7 @@ try {
|
|
|
461
497
|
"tag.essential": "Core",
|
|
462
498
|
"tag.followPlatform": "Platform",
|
|
463
499
|
"btn.save": "Save",
|
|
464
|
-
"btn.saving": "
|
|
500
|
+
"btn.saving": "Saving…",
|
|
465
501
|
"btn.reload": "Reload",
|
|
466
502
|
"btn.reloadDiscard": "Discard edits and reload",
|
|
467
503
|
"btn.reloadConfirm": "Discard unsaved edits and reload?",
|
|
@@ -520,6 +556,8 @@ try {
|
|
|
520
556
|
"row.tool-plugin-manager.label": "Plugin manager (install / toggle)",
|
|
521
557
|
"row.tool-plugin-manager.note": "Lets the model install or toggle plugins.",
|
|
522
558
|
"row.tool-presentation.label": "PTC tool presentation",
|
|
559
|
+
"row.tool-executor-ptc.label": "PTC program executor",
|
|
560
|
+
"row.tool-executor-ptc.note": "Runs programs and calls tools through run_code, and provides the tool SDK to the model.",
|
|
523
561
|
"row.present.label": "Deliverables (present)",
|
|
524
562
|
"row.command-goal.label": "Goal command",
|
|
525
563
|
"row.tool-goal.label": "Goals",
|
|
@@ -779,6 +817,11 @@ try {
|
|
|
779
817
|
".cpfe-pills button{white-space:nowrap}",
|
|
780
818
|
".cpfe-picker{display:flex;align-items:center;gap:8px;margin:2px 0 4px}",
|
|
781
819
|
".cpfe-history{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px}",
|
|
820
|
+
// Native Menu and Button keep their appearance; only the trigger's
|
|
821
|
+
// own text shrinks so long names cannot wrap outside its fixed height.
|
|
822
|
+
".cpfe-selector-menu,.cpfe-selector{min-width:0;max-width:100%}",
|
|
823
|
+
".cpfe-selector-label{min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
824
|
+
".cpfe-selector-chevron{flex:0 0 auto}",
|
|
782
825
|
".cpfe-history-label{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
|
|
783
826
|
".cpfe-history-hint{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
|
|
784
827
|
".cpfe-pill-wrap{display:inline-flex;align-items:center;gap:4px}",
|
|
@@ -789,7 +832,9 @@ try {
|
|
|
789
832
|
".cpfe-branch{display:flex;flex-direction:column;gap:16px;margin-top:8px;padding-left:12px;border-left:1px solid var(--dsw-alias-border-l1)}",
|
|
790
833
|
".cpfe-node{display:flex;flex-direction:column;gap:4px}",
|
|
791
834
|
".cpfe-node-label{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
|
|
792
|
-
|
|
835
|
+
// Native Input applies className to its padded wrapper span, so its
|
|
836
|
+
// outer box (not only the inner input) must fit the plugin column.
|
|
837
|
+
".cpfe-field{display:flex;box-sizing:border-box;width:100%;min-width:0;max-width:100%;margin-bottom:8px}",
|
|
793
838
|
".cpfe-row-head{font-size:14px;line-height:22px;color:var(--dsw-alias-label-primary)}",
|
|
794
839
|
".cpfe-row-switch{flex:0 0 auto}",
|
|
795
840
|
".cpfe-row-line{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}",
|
|
@@ -802,7 +847,7 @@ try {
|
|
|
802
847
|
".cpfe-detail-value{min-width:0;overflow-wrap:anywhere}",
|
|
803
848
|
".cpfe-sec-head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px}",
|
|
804
849
|
".cpfe-sec-head .cpfe-actions{margin-top:0}",
|
|
805
|
-
".cpfe-newinput{flex:1;min-width:
|
|
850
|
+
".cpfe-newinput{box-sizing:border-box;flex:1 1 200px;min-width:0;max-width:100%}",
|
|
806
851
|
// 单列、行高统一(对齐官方插件页的形态);之前的自适应多列网格会让行高参差不齐。
|
|
807
852
|
// ★ 对齐官方设置行(实测 0.1.7-rc.2 的行:padding 16px 0、无背景、无圆角,行间只有一条 1px
|
|
808
853
|
// 分隔线)。原来是"圆角卡片 + 描边 + 灰底" —— 那正是廉价感最重的部分。
|
|
@@ -880,6 +925,7 @@ try {
|
|
|
880
925
|
delete: ROUTE + "/delete",
|
|
881
926
|
reorder: ROUTE + "/reorder",
|
|
882
927
|
repair: ROUTE + "/repair",
|
|
928
|
+
sync: ROUTE + "/sync",
|
|
883
929
|
}
|
|
884
930
|
|
|
885
931
|
/** Every assistant this feature manages. */
|
|
@@ -1215,6 +1261,12 @@ try {
|
|
|
1215
1261
|
const [status, setStatus] = react.useState("")
|
|
1216
1262
|
const [failed, setFailed] = react.useState(false)
|
|
1217
1263
|
const [busy, setBusy] = react.useState(false)
|
|
1264
|
+
const [pending, setPending] = react.useState("")
|
|
1265
|
+
const [syncNeeded, setSyncNeeded] = react.useState(false)
|
|
1266
|
+
const [refreshNeeded, setRefreshNeeded] = react.useState(null)
|
|
1267
|
+
// A ref closes the interval before React renders disabled controls. Only the
|
|
1268
|
+
// owning operation may apply a response or release the busy state.
|
|
1269
|
+
const operation = react.useRef({ alive: true, current: null })
|
|
1218
1270
|
const [newName, setNewName] = react.useState("")
|
|
1219
1271
|
const [postureChoices, setPostureChoices] = react.useState([])
|
|
1220
1272
|
const [posture, setPosture] = react.useState("develop")
|
|
@@ -1236,12 +1288,11 @@ try {
|
|
|
1236
1288
|
/** 本机这条线上有无法解析的行时,一键把它们关掉(服务端复用保存同一条排版手术)。 */
|
|
1237
1289
|
const repairRowsNow = async () => {
|
|
1238
1290
|
if (draft === null) return
|
|
1239
|
-
|
|
1240
|
-
setFailed(false)
|
|
1241
|
-
try {
|
|
1291
|
+
return runOperation("status.repairing", "msg.saveFailed", async (token) => {
|
|
1242
1292
|
const result = await postJson(ROUTES.repair, { id: selected })
|
|
1243
|
-
if (
|
|
1244
|
-
|
|
1293
|
+
if (!ownsOperation(token)) return
|
|
1294
|
+
showResult(result, "msg.repaired", "msg.saveFailed")
|
|
1295
|
+
if (result?.ok === true || result?.persisted === true) {
|
|
1245
1296
|
// ★ 把修复折进**草稿**(issue #8)。
|
|
1246
1297
|
//
|
|
1247
1298
|
// 修复在磁盘上是生效的,但草稿里那几行仍然是"启用"。而 `reload()` 对脏草稿是
|
|
@@ -1255,17 +1306,9 @@ try {
|
|
|
1255
1306
|
for (const rowId of repairedIds) nextOverrides[rowId] = false
|
|
1256
1307
|
update({ overrides: nextOverrides }, { force: true })
|
|
1257
1308
|
}
|
|
1258
|
-
await reload()
|
|
1259
|
-
} else {
|
|
1260
|
-
setFailed(true)
|
|
1261
|
-
setStatus(apiText(result, "msg.saveFailed"))
|
|
1309
|
+
await refreshAfterWrite(result, "msg.repaired", selected, token, () => reload(selected, undefined, token))
|
|
1262
1310
|
}
|
|
1263
|
-
}
|
|
1264
|
-
setFailed(true)
|
|
1265
|
-
setStatus(t("msg.saveFailed"))
|
|
1266
|
-
} finally {
|
|
1267
|
-
setBusy(false)
|
|
1268
|
-
}
|
|
1311
|
+
})
|
|
1269
1312
|
}
|
|
1270
1313
|
|
|
1271
1314
|
const toggleRowExpanded = (id) =>
|
|
@@ -1309,6 +1352,52 @@ try {
|
|
|
1309
1352
|
|
|
1310
1353
|
const describeError = (error) => String((error && error.message) || error)
|
|
1311
1354
|
|
|
1355
|
+
const ownsOperation = (token) => operation.current.alive && operation.current.current === token
|
|
1356
|
+
const runOperation = async (pendingKey, failureKey, action) => {
|
|
1357
|
+
if (!operation.current.alive || operation.current.current !== null) return
|
|
1358
|
+
const token = {}
|
|
1359
|
+
operation.current.current = token
|
|
1360
|
+
setBusy(true)
|
|
1361
|
+
setPending(pendingKey)
|
|
1362
|
+
try {
|
|
1363
|
+
await action(token)
|
|
1364
|
+
} catch (error) {
|
|
1365
|
+
if (ownsOperation(token)) {
|
|
1366
|
+
setFailed(true)
|
|
1367
|
+
if (pendingKey === "status.reading" && list === null) setListFailed(true)
|
|
1368
|
+
setStatus(fillPlaceholders(t("status.detail"), { message: t(failureKey), detail: describeError(error) }))
|
|
1369
|
+
}
|
|
1370
|
+
} finally {
|
|
1371
|
+
if (ownsOperation(token)) {
|
|
1372
|
+
operation.current.current = null
|
|
1373
|
+
setBusy(false)
|
|
1374
|
+
setPending("")
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
const showResult = (result, successKey, failureKey) => {
|
|
1380
|
+
setFailed(result?.ok !== true)
|
|
1381
|
+
setStatus(apiText(result, result?.ok === true ? successKey : failureKey))
|
|
1382
|
+
if (result?.synchronized === false) setSyncNeeded(true)
|
|
1383
|
+
else if (result?.synchronized === true) setSyncNeeded(false)
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// Once the host confirms a mutation, a failed follow-up GET cannot turn
|
|
1387
|
+
// that fact into a failed mutation or invite the user to repeat it.
|
|
1388
|
+
const refreshAfterWrite = async (result, fallbackKey, targetId, token, refresh) => {
|
|
1389
|
+
try {
|
|
1390
|
+
await refresh()
|
|
1391
|
+
if (ownsOperation(token)) setRefreshNeeded(null)
|
|
1392
|
+
} catch (error) {
|
|
1393
|
+
if (!ownsOperation(token)) return
|
|
1394
|
+
setRefreshNeeded({ result, fallbackKey, targetId })
|
|
1395
|
+
setFailed(true)
|
|
1396
|
+
const message = fillPlaceholders(t("msg.completedReadFailed"), { result: apiText(result, fallbackKey) })
|
|
1397
|
+
setStatus(fillPlaceholders(t("status.detail"), { message, detail: describeError(error) }))
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1312
1401
|
/**
|
|
1313
1402
|
* Fetch one assistant into `entries`, keeping any unsaved draft it already has.
|
|
1314
1403
|
*
|
|
@@ -1316,17 +1405,17 @@ try {
|
|
|
1316
1405
|
* `force` is "re-read even if this entry is already in memory". Neither is set by
|
|
1317
1406
|
* ordinary switching: moving between assistants must never race a half-typed prompt.
|
|
1318
1407
|
*/
|
|
1319
|
-
const open = async (id, options) => {
|
|
1408
|
+
const open = async (id, options, token) => {
|
|
1409
|
+
if (!ownsOperation(token)) return
|
|
1320
1410
|
if (typeof id !== "string" || id === "") return
|
|
1321
1411
|
const force = options !== null && typeof options === "object" && options.force === true
|
|
1322
1412
|
const discard = options !== null && typeof options === "object" && options.discard === true
|
|
1323
1413
|
const held = entries[id]
|
|
1324
1414
|
if (force !== true && held !== undefined) return
|
|
1325
1415
|
const result = await fetchState(id)
|
|
1416
|
+
if (!ownsOperation(token)) return
|
|
1326
1417
|
if (result === null || result === undefined || result.ok !== true) {
|
|
1327
|
-
|
|
1328
|
-
setStatus(apiText(result, "msg.readFailed"))
|
|
1329
|
-
return
|
|
1418
|
+
throw new Error(apiText(result, "msg.readFailed"))
|
|
1330
1419
|
}
|
|
1331
1420
|
const fresh = draftOf(result)
|
|
1332
1421
|
setEntries((previous) => {
|
|
@@ -1334,17 +1423,16 @@ try {
|
|
|
1334
1423
|
const keep = discard !== true && before !== undefined && !sameDraft(before.value, before.saved) ? before.value : fresh
|
|
1335
1424
|
return { ...previous, [id]: { payload: result, saved: fresh, value: keep } }
|
|
1336
1425
|
})
|
|
1337
|
-
setFailed(false)
|
|
1338
1426
|
}
|
|
1339
1427
|
|
|
1340
1428
|
/** Re-read the assistant list and force-reload the one it settles on. */
|
|
1341
|
-
const reload = async (preferId, options) => {
|
|
1429
|
+
const reload = async (preferId, options, token) => {
|
|
1430
|
+
if (!ownsOperation(token)) return
|
|
1342
1431
|
const result = await fetchList()
|
|
1432
|
+
if (!ownsOperation(token)) return
|
|
1343
1433
|
if (result === null || result === undefined || result.ok !== true) {
|
|
1344
|
-
setFailed(true)
|
|
1345
1434
|
setListFailed(true)
|
|
1346
|
-
|
|
1347
|
-
return
|
|
1435
|
+
throw new Error(apiText(result, "msg.readFailed"))
|
|
1348
1436
|
}
|
|
1349
1437
|
const assistants = Array.isArray(result.assistants) ? result.assistants : []
|
|
1350
1438
|
const choices = Array.isArray(result.postures) ? result.postures.filter((item) => item !== null && typeof item === "object" && typeof item.id === "string") : []
|
|
@@ -1365,7 +1453,7 @@ try {
|
|
|
1365
1453
|
setEntries({})
|
|
1366
1454
|
return
|
|
1367
1455
|
}
|
|
1368
|
-
await open(wanted, { force: true, discard: options !== null && typeof options === "object" && options.discard === true })
|
|
1456
|
+
await open(wanted, { force: true, discard: options !== null && typeof options === "object" && options.discard === true }, token)
|
|
1369
1457
|
}
|
|
1370
1458
|
|
|
1371
1459
|
/**
|
|
@@ -1374,11 +1462,12 @@ try {
|
|
|
1374
1462
|
* A save can rename an assistant but never changes ids, so the list needs a new
|
|
1375
1463
|
* fetch while the selection, the payloads and every draft stay exactly as they are.
|
|
1376
1464
|
*/
|
|
1377
|
-
const refreshNames = async () => {
|
|
1465
|
+
const refreshNames = async (token) => {
|
|
1378
1466
|
const result = await fetchList()
|
|
1467
|
+
if (!ownsOperation(token)) return
|
|
1379
1468
|
if (result !== null && result !== undefined && result.ok === true) {
|
|
1380
1469
|
setList(Array.isArray(result.assistants) ? result.assistants : [])
|
|
1381
|
-
}
|
|
1470
|
+
} else throw new Error(apiText(result, "msg.readFailed"))
|
|
1382
1471
|
}
|
|
1383
1472
|
|
|
1384
1473
|
/**
|
|
@@ -1397,22 +1486,11 @@ try {
|
|
|
1397
1486
|
}, [])
|
|
1398
1487
|
|
|
1399
1488
|
react.useEffect(() => {
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
setBusy(true)
|
|
1403
|
-
try {
|
|
1404
|
-
await reload("")
|
|
1405
|
-
} catch (error) {
|
|
1406
|
-
if (alive) {
|
|
1407
|
-
setFailed(true)
|
|
1408
|
-
setStatus(fillPlaceholders(t("status.detail"), { message: t("msg.readFailed"), detail: describeError(error) }))
|
|
1409
|
-
}
|
|
1410
|
-
} finally {
|
|
1411
|
-
if (alive) setBusy(false)
|
|
1412
|
-
}
|
|
1413
|
-
})()
|
|
1489
|
+
operation.current.alive = true
|
|
1490
|
+
runOperation("status.reading", "msg.readFailed", (token) => reload("", undefined, token))
|
|
1414
1491
|
return () => {
|
|
1415
|
-
alive = false
|
|
1492
|
+
operation.current.alive = false
|
|
1493
|
+
operation.current.current = null
|
|
1416
1494
|
}
|
|
1417
1495
|
}, [])
|
|
1418
1496
|
|
|
@@ -1430,7 +1508,7 @@ try {
|
|
|
1430
1508
|
//
|
|
1431
1509
|
// `{ force: true }` 是给**内部**调用用的(例如「按本线修复」把结果折进草稿):
|
|
1432
1510
|
// 那不是用户输入,不受这条守护约束。
|
|
1433
|
-
if (
|
|
1511
|
+
if (operation.current.current !== null && options?.force !== true) return
|
|
1434
1512
|
setEntries((previous) => {
|
|
1435
1513
|
const current = previous[selected]
|
|
1436
1514
|
if (current === undefined) return previous
|
|
@@ -1440,36 +1518,25 @@ try {
|
|
|
1440
1518
|
}
|
|
1441
1519
|
|
|
1442
1520
|
const pick = async (id) => {
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
setBusy(false)
|
|
1452
|
-
}
|
|
1453
|
-
return
|
|
1454
|
-
}
|
|
1455
|
-
setSelected(id)
|
|
1456
|
-
setDeleteOpen(false)
|
|
1457
|
-
setAcknowledged(false)
|
|
1458
|
-
setStatus("")
|
|
1459
|
-
setBusy(true)
|
|
1460
|
-
try {
|
|
1461
|
-
await open(id)
|
|
1462
|
-
} catch (error) {
|
|
1463
|
-
setFailed(true)
|
|
1464
|
-
setStatus(fillPlaceholders(t("status.detail"), { message: t("msg.readFailed"), detail: describeError(error) }))
|
|
1465
|
-
} finally {
|
|
1466
|
-
setBusy(false)
|
|
1467
|
-
}
|
|
1521
|
+
return runOperation("status.reading", "msg.readFailed", async (token) => {
|
|
1522
|
+
setSelected(id)
|
|
1523
|
+
setDeleteOpen(false)
|
|
1524
|
+
setAcknowledged(false)
|
|
1525
|
+
setStatus("")
|
|
1526
|
+
setFailed(false)
|
|
1527
|
+
await open(id, { force: id === selected }, token)
|
|
1528
|
+
})
|
|
1468
1529
|
}
|
|
1469
1530
|
|
|
1531
|
+
const reread = (discard = false) => runOperation("status.reading", "msg.readFailed", async (token) => {
|
|
1532
|
+
setStatus("")
|
|
1533
|
+
setFailed(false)
|
|
1534
|
+
await reload(selected, { discard }, token)
|
|
1535
|
+
})
|
|
1536
|
+
|
|
1470
1537
|
/** Create a new assistant, or duplicate the selected one. */
|
|
1471
1538
|
const create = async (options) => {
|
|
1472
|
-
if (
|
|
1539
|
+
if (operation.current.current !== null) return
|
|
1473
1540
|
const from = options !== null && typeof options === "object" ? options.from : undefined
|
|
1474
1541
|
const name =
|
|
1475
1542
|
options !== null && typeof options === "object" && typeof options.name === "string"
|
|
@@ -1480,27 +1547,18 @@ try {
|
|
|
1480
1547
|
setStatus(t("msg.nameRequired"))
|
|
1481
1548
|
return
|
|
1482
1549
|
}
|
|
1483
|
-
|
|
1484
|
-
try {
|
|
1550
|
+
return runOperation("status.creating", "msg.createFailed", async (token) => {
|
|
1485
1551
|
const body = { name, locale: activeLanguage(localeRef) }
|
|
1486
1552
|
if (from !== undefined) body.from = from
|
|
1487
1553
|
else if (postureChoices.some((item) => item.id === posture)) body.posture = posture
|
|
1488
1554
|
const result = await postJson(ROUTES.create, body)
|
|
1489
|
-
if (
|
|
1555
|
+
if (!ownsOperation(token)) return
|
|
1556
|
+
showResult(result, from === undefined ? "msg.created" : "msg.duplicated", "msg.createFailed")
|
|
1557
|
+
if (result?.ok === true || result?.persisted === true) {
|
|
1490
1558
|
setNewName("")
|
|
1491
|
-
|
|
1492
|
-
setStatus(apiText(result, from === undefined ? "msg.created" : "msg.duplicated"))
|
|
1493
|
-
await reload(result.id)
|
|
1494
|
-
} else {
|
|
1495
|
-
setFailed(true)
|
|
1496
|
-
setStatus(apiText(result, "msg.createFailed"))
|
|
1559
|
+
await refreshAfterWrite(result, from === undefined ? "msg.created" : "msg.duplicated", result.id, token, () => reload(result.id, undefined, token))
|
|
1497
1560
|
}
|
|
1498
|
-
}
|
|
1499
|
-
setFailed(true)
|
|
1500
|
-
setStatus(fillPlaceholders(t("status.detail"), { message: t("msg.createFailed"), detail: describeError(error) }))
|
|
1501
|
-
} finally {
|
|
1502
|
-
setBusy(false)
|
|
1503
|
-
}
|
|
1561
|
+
})
|
|
1504
1562
|
}
|
|
1505
1563
|
|
|
1506
1564
|
const duplicate = () => create({ from: draft.id, name: copyName(t("assistant.copyName"), draft.name || draft.id) })
|
|
@@ -1512,23 +1570,14 @@ try {
|
|
|
1512
1570
|
* (the default) and only the roster order changes.
|
|
1513
1571
|
*/
|
|
1514
1572
|
const move = async (direction) => {
|
|
1515
|
-
|
|
1516
|
-
try {
|
|
1573
|
+
return runOperation("status.reordering", "msg.reorderFailed", async (token) => {
|
|
1517
1574
|
const result = await postJson(ROUTES.reorder, { id: draft.id, direction })
|
|
1518
|
-
if (
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
await reload(draft.id)
|
|
1522
|
-
} else {
|
|
1523
|
-
setFailed(true)
|
|
1524
|
-
setStatus(apiText(result, "msg.reorderFailed"))
|
|
1575
|
+
if (!ownsOperation(token)) return
|
|
1576
|
+
showResult(result, "msg.reordered", "msg.reorderFailed")
|
|
1577
|
+
if (result?.ok === true || result?.persisted === true) {
|
|
1578
|
+
await refreshAfterWrite(result, "msg.reordered", draft.id, token, () => reload(draft.id, undefined, token))
|
|
1525
1579
|
}
|
|
1526
|
-
}
|
|
1527
|
-
setFailed(true)
|
|
1528
|
-
setStatus(fillPlaceholders(t("status.detail"), { message: t("msg.reorderFailed"), detail: describe(error) }))
|
|
1529
|
-
} finally {
|
|
1530
|
-
setBusy(false)
|
|
1531
|
-
}
|
|
1580
|
+
})
|
|
1532
1581
|
}
|
|
1533
1582
|
|
|
1534
1583
|
/** Download the prompt being edited (client-side only — the host is not involved). */
|
|
@@ -1548,18 +1597,20 @@ try {
|
|
|
1548
1597
|
const loadVersion = async () => {
|
|
1549
1598
|
const picked = typeof draft.historyPick === "string" ? draft.historyPick : ""
|
|
1550
1599
|
if (picked === "" || draft.id === undefined) return
|
|
1551
|
-
|
|
1600
|
+
return runOperation("status.reading", "msg.loadFailed", async (token) => {
|
|
1552
1601
|
// 版本键是序号(时间戳会在同一毫秒内撞车 —— 单测就是那样抓到它的)。
|
|
1553
1602
|
const response = await fetch(ROUTES.history + "?id=" + encodeURIComponent(draft.id) + "&n=" + encodeURIComponent(picked))
|
|
1554
1603
|
const payload = await asJson(response)
|
|
1604
|
+
if (!ownsOperation(token)) return
|
|
1555
1605
|
if (payload === null || payload.ok !== true) {
|
|
1606
|
+
setFailed(true)
|
|
1556
1607
|
setStatus(t("msg.loadFailed"))
|
|
1557
1608
|
return
|
|
1558
1609
|
}
|
|
1559
|
-
update({ prompt: payload.text })
|
|
1560
|
-
|
|
1561
|
-
setStatus(
|
|
1562
|
-
}
|
|
1610
|
+
update({ prompt: payload.text }, { force: true })
|
|
1611
|
+
setFailed(false)
|
|
1612
|
+
setStatus(t("msg.loaded"))
|
|
1613
|
+
})
|
|
1563
1614
|
}
|
|
1564
1615
|
|
|
1565
1616
|
const exportPrompt = () => {
|
|
@@ -1588,61 +1639,54 @@ try {
|
|
|
1588
1639
|
* path (and the same `{{…}}` validation) as anything typed by hand — an import can
|
|
1589
1640
|
* never silently publish a prompt the renderer would refuse.
|
|
1590
1641
|
*/
|
|
1591
|
-
const importPrompt = (event) => {
|
|
1642
|
+
const importPrompt = async (event) => {
|
|
1592
1643
|
const input = event !== null && event !== undefined ? event.target : undefined
|
|
1593
1644
|
const file = input !== null && input !== undefined && input.files !== undefined ? input.files[0] : undefined
|
|
1594
1645
|
// Reset so picking the same file twice in a row fires `change` again.
|
|
1595
1646
|
if (input !== null && input !== undefined) input.value = ""
|
|
1647
|
+
if (operation.current.current !== null) return
|
|
1596
1648
|
if (file === undefined || file === null) return
|
|
1597
1649
|
if (file.size > 1_000_000) {
|
|
1598
1650
|
setFailed(true)
|
|
1599
1651
|
setStatus(t("msg.importTooLarge"))
|
|
1600
1652
|
return
|
|
1601
1653
|
}
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1654
|
+
return runOperation("status.importing", "msg.importFailed", async (token) => {
|
|
1655
|
+
const text = await new Promise((resolve, reject) => {
|
|
1656
|
+
const reader = new FileReader()
|
|
1657
|
+
reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : "")
|
|
1658
|
+
reader.onerror = () => reject(reader.error || new Error(t("msg.importFailed")))
|
|
1659
|
+
reader.onabort = () => reject(new Error(t("msg.importFailed")))
|
|
1660
|
+
reader.readAsText(file, "utf-8")
|
|
1661
|
+
})
|
|
1662
|
+
if (!ownsOperation(token)) return
|
|
1605
1663
|
if (text.trim() === "") {
|
|
1606
1664
|
setFailed(true)
|
|
1607
1665
|
setStatus(t("msg.importEmpty"))
|
|
1608
1666
|
return
|
|
1609
1667
|
}
|
|
1610
|
-
update({ prompt: text })
|
|
1668
|
+
update({ prompt: text }, { force: true })
|
|
1611
1669
|
setFailed(false)
|
|
1612
1670
|
setStatus(t("msg.imported"))
|
|
1613
|
-
}
|
|
1614
|
-
reader.onerror = () => {
|
|
1615
|
-
setFailed(true)
|
|
1616
|
-
setStatus(t("msg.importFailed"))
|
|
1617
|
-
}
|
|
1618
|
-
reader.readAsText(file, "utf-8")
|
|
1671
|
+
})
|
|
1619
1672
|
}
|
|
1620
1673
|
|
|
1621
1674
|
const remove = async (id) => {
|
|
1622
|
-
|
|
1623
|
-
try {
|
|
1675
|
+
return runOperation("status.deleting", "msg.deleteFailed", async (token) => {
|
|
1624
1676
|
const result = await postJson(ROUTES.delete, { id })
|
|
1625
|
-
if (
|
|
1677
|
+
if (!ownsOperation(token)) return
|
|
1678
|
+
showResult(result, "msg.deleted", "msg.deleteFailed")
|
|
1679
|
+
if (result?.ok === true || result?.persisted === true) {
|
|
1626
1680
|
setDeleteOpen(false)
|
|
1627
1681
|
setAcknowledged(false)
|
|
1628
|
-
setFailed(false)
|
|
1629
|
-
setStatus(apiText(result, "msg.deleted"))
|
|
1630
1682
|
setEntries((previous) => {
|
|
1631
1683
|
const next = { ...previous }
|
|
1632
1684
|
delete next[id]
|
|
1633
1685
|
return next
|
|
1634
1686
|
})
|
|
1635
|
-
await reload("")
|
|
1636
|
-
} else {
|
|
1637
|
-
setFailed(true)
|
|
1638
|
-
setStatus(apiText(result, "msg.deleteFailed"))
|
|
1687
|
+
await refreshAfterWrite(result, "msg.deleted", "", token, () => reload("", undefined, token))
|
|
1639
1688
|
}
|
|
1640
|
-
}
|
|
1641
|
-
setFailed(true)
|
|
1642
|
-
setStatus(fillPlaceholders(t("status.detail"), { message: t("msg.deleteFailed"), detail: describeError(error) }))
|
|
1643
|
-
} finally {
|
|
1644
|
-
setBusy(false)
|
|
1645
|
-
}
|
|
1689
|
+
})
|
|
1646
1690
|
}
|
|
1647
1691
|
|
|
1648
1692
|
/**
|
|
@@ -1669,9 +1713,10 @@ try {
|
|
|
1669
1713
|
}
|
|
1670
1714
|
|
|
1671
1715
|
const save = async () => {
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
const
|
|
1716
|
+
if (draft === null) return
|
|
1717
|
+
return runOperation("status.saving", "msg.saveFailed", async (token) => {
|
|
1718
|
+
const savedHistory = entry?.saved?.history
|
|
1719
|
+
const newest = Array.isArray(savedHistory) && savedHistory.length > 0 ? savedHistory[0].n : undefined
|
|
1675
1720
|
const result = await postJson(ROUTES.state, {
|
|
1676
1721
|
id: draft.id,
|
|
1677
1722
|
mode: draft.mode,
|
|
@@ -1681,6 +1726,9 @@ try {
|
|
|
1681
1726
|
description: draft.description,
|
|
1682
1727
|
...(typeof newest === "number" ? { baseN: newest } : {}),
|
|
1683
1728
|
})
|
|
1729
|
+
if (!ownsOperation(token)) return
|
|
1730
|
+
if (result?.synchronized === false) setSyncNeeded(true)
|
|
1731
|
+
else if (result?.synchronized === true) setSyncNeeded(false)
|
|
1684
1732
|
if (result !== null && result !== undefined && result.ok === true) {
|
|
1685
1733
|
setFailed(false)
|
|
1686
1734
|
const compositionChanged =
|
|
@@ -1693,42 +1741,66 @@ try {
|
|
|
1693
1741
|
// The server normalises what it stores (trimmed name, recomputed overrides),
|
|
1694
1742
|
// so the just-saved draft is replaced by what it actually wrote. Without this
|
|
1695
1743
|
// the page would show 「已保存」 and 「未保存」 at the same time.
|
|
1696
|
-
|
|
1697
|
-
|
|
1744
|
+
await refreshAfterWrite(spoken, "msg.saved", draft.id, token, async () => {
|
|
1745
|
+
const fresh = await fetchState(draft.id)
|
|
1746
|
+
if (!ownsOperation(token)) return
|
|
1747
|
+
if (fresh?.ok !== true) throw new Error(apiText(fresh, "msg.readFailed"))
|
|
1698
1748
|
const normalized = draftOf(fresh)
|
|
1699
1749
|
setEntries((previous) => ({
|
|
1700
1750
|
...previous,
|
|
1701
1751
|
[draft.id]: { payload: fresh, saved: normalized, value: normalized },
|
|
1702
1752
|
}))
|
|
1703
|
-
await refreshNames()
|
|
1704
|
-
}
|
|
1705
|
-
await reload(draft.id, { discard: true })
|
|
1706
|
-
}
|
|
1753
|
+
await refreshNames(token)
|
|
1754
|
+
})
|
|
1707
1755
|
} else {
|
|
1708
1756
|
setFailed(true)
|
|
1709
1757
|
setStatus(apiText(result, "msg.saveFailed"))
|
|
1710
1758
|
}
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1759
|
+
})
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
const retrySync = () => runOperation("status.synchronizing", "msg.syncFailed", async (token) => {
|
|
1763
|
+
const result = await postJson(ROUTES.sync, {})
|
|
1764
|
+
if (!ownsOperation(token)) return
|
|
1765
|
+
showResult(result, "msg.synchronized", "msg.syncFailed")
|
|
1766
|
+
if (result?.ok === true) {
|
|
1767
|
+
await refreshAfterWrite(result, "msg.synchronized", selected, token, () => reload(selected, undefined, token))
|
|
1716
1768
|
}
|
|
1769
|
+
})
|
|
1770
|
+
|
|
1771
|
+
const retryRead = () => {
|
|
1772
|
+
if (refreshNeeded === null) return
|
|
1773
|
+
const { result, fallbackKey, targetId } = refreshNeeded
|
|
1774
|
+
return runOperation("status.reading", "msg.readFailed", (token) =>
|
|
1775
|
+
refreshAfterWrite(result, fallbackKey, targetId, token, async () => {
|
|
1776
|
+
await reload(targetId, undefined, token)
|
|
1777
|
+
if (ownsOperation(token)) showResult(result, fallbackKey, fallbackKey)
|
|
1778
|
+
}),
|
|
1779
|
+
)
|
|
1717
1780
|
}
|
|
1718
1781
|
|
|
1719
1782
|
const assistants = list === null ? [] : list
|
|
1720
1783
|
const position = assistants.findIndex((item) => item.id === selected)
|
|
1721
|
-
const statusClass =
|
|
1784
|
+
const statusClass = busy
|
|
1785
|
+
? "cpfe-status"
|
|
1786
|
+
: failed
|
|
1722
1787
|
? "cpfe-status cpfe-err"
|
|
1723
1788
|
: dirty
|
|
1724
1789
|
? "cpfe-status cpfe-dirty"
|
|
1725
1790
|
: "cpfe-status cpfe-ok"
|
|
1726
|
-
const shown = dirty && status === "" ? t("msg.unsaved") : status
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1791
|
+
const shown = pending !== "" ? t(pending) : dirty && status === "" ? t("msg.unsaved") : status
|
|
1792
|
+
const feedback = [
|
|
1793
|
+
refreshNeeded !== null
|
|
1794
|
+
? react.createElement(A.Button, { variant: "outline", disabled: busy, onClick: retryRead }, t("btn.retryRead"))
|
|
1795
|
+
: null,
|
|
1796
|
+
refreshNeeded === null && !editorReady && failed && list !== null && selected !== ""
|
|
1797
|
+
? react.createElement(A.Button, { variant: "outline", disabled: busy, onClick: () => reread() }, t("btn.reload"))
|
|
1798
|
+
: null,
|
|
1799
|
+
syncNeeded
|
|
1800
|
+
? react.createElement(A.Button, { variant: "outline", disabled: busy, onClick: retrySync }, t("btn.retrySync"))
|
|
1801
|
+
: null,
|
|
1802
|
+
react.createElement("span", { className: statusClass, role: "status", "aria-live": "polite", "aria-atomic": true }, shown),
|
|
1803
|
+
]
|
|
1732
1804
|
const createGroup = react.createElement(
|
|
1733
1805
|
"section",
|
|
1734
1806
|
{ className: "cpfe-create" },
|
|
@@ -1766,24 +1838,6 @@ try {
|
|
|
1766
1838
|
"div",
|
|
1767
1839
|
{ className: "cpfe-node" },
|
|
1768
1840
|
react.createElement("span", { className: "cpfe-node-label" }, t("tree.base")),
|
|
1769
|
-
react.createElement(
|
|
1770
|
-
"div",
|
|
1771
|
-
{ className: "cpfe-pills cpfe-locked", role: "radiogroup", "aria-disabled": "true", "aria-label": t("tree.base") },
|
|
1772
|
-
lockedModes.map((mode) =>
|
|
1773
|
-
react.createElement(
|
|
1774
|
-
A.Pill,
|
|
1775
|
-
{
|
|
1776
|
-
key: mode.id,
|
|
1777
|
-
active: mode.id === "standard",
|
|
1778
|
-
disabled: true,
|
|
1779
|
-
"aria-pressed": mode.id === "standard",
|
|
1780
|
-
title: mode.id === "standard" ? t("tree.baseHold") : t("tree.baseSkip"),
|
|
1781
|
-
onClick: () => {},
|
|
1782
|
-
},
|
|
1783
|
-
t("base." + mode.id + ".label", mode.label),
|
|
1784
|
-
),
|
|
1785
|
-
),
|
|
1786
|
-
),
|
|
1787
1841
|
react.createElement("p", { className: "cpfe-note" }, t("tree.baseLocked")),
|
|
1788
1842
|
),
|
|
1789
1843
|
react.createElement(
|
|
@@ -1792,6 +1846,7 @@ try {
|
|
|
1792
1846
|
react.createElement(A.Input, {
|
|
1793
1847
|
className: "cpfe-newinput",
|
|
1794
1848
|
value: newName,
|
|
1849
|
+
disabled: busy,
|
|
1795
1850
|
placeholder: t("assistant.newPlaceholder"),
|
|
1796
1851
|
"aria-label": t("assistant.newPlaceholder"),
|
|
1797
1852
|
onKeyDown: (event) => {
|
|
@@ -1810,13 +1865,19 @@ try {
|
|
|
1810
1865
|
disabled: busy,
|
|
1811
1866
|
onClick: () => create(),
|
|
1812
1867
|
},
|
|
1813
|
-
|
|
1868
|
+
pending === "status.creating" ? t("btn.creating") : t("btn.create"),
|
|
1814
1869
|
),
|
|
1815
1870
|
),
|
|
1871
|
+
react.createElement("p", { className: "cpfe-note" }, t("create.nextStep")),
|
|
1816
1872
|
),
|
|
1817
1873
|
)
|
|
1818
1874
|
|
|
1819
1875
|
// ── assistant list ────────────────────────────────────────────────────
|
|
1876
|
+
const selectedAssistantLabel = (assistants.find((item) => item.id === selected) || {}).name || selected || t("assistant.heading")
|
|
1877
|
+
const pickedHistoryEntry = draft === null || draft.historyPick === "" ? undefined : draft.history.find((item) => String(item.n) === draft.historyPick)
|
|
1878
|
+
const historySelectionLabel = pickedHistoryEntry === undefined
|
|
1879
|
+
? t("history.pick")
|
|
1880
|
+
: formatWhen(pickedHistoryEntry.at) + " · " + t("history.by." + pickedHistoryEntry.by)
|
|
1820
1881
|
const assistantList = react.createElement(
|
|
1821
1882
|
"section",
|
|
1822
1883
|
null,
|
|
@@ -1828,7 +1889,7 @@ try {
|
|
|
1828
1889
|
null,
|
|
1829
1890
|
react.createElement("p", { className: "cpfe-sub" }, listFailed ? t("msg.readFailed") : t("assistant.loadingList")),
|
|
1830
1891
|
listFailed
|
|
1831
|
-
? react.createElement(A.Button, { disabled: busy, onClick: () =>
|
|
1892
|
+
? react.createElement(A.Button, { disabled: busy, onClick: () => reread() }, t("btn.reload"))
|
|
1832
1893
|
: null,
|
|
1833
1894
|
)
|
|
1834
1895
|
: assistants.length === 0
|
|
@@ -1842,6 +1903,7 @@ try {
|
|
|
1842
1903
|
renderable(A.Menu)
|
|
1843
1904
|
? react.createElement(A.Menu, {
|
|
1844
1905
|
open: assistantOpen,
|
|
1906
|
+
className: "cpfe-selector-menu",
|
|
1845
1907
|
// ★ portal:不把浮层渲染在我们的容器里。设置面板有裁剪与叠层,非 portal 的
|
|
1846
1908
|
// 菜单会被裁掉表面,只剩文字"压"在下面的输入框上(实测截图如此)。
|
|
1847
1909
|
portal: true,
|
|
@@ -1853,15 +1915,16 @@ try {
|
|
|
1853
1915
|
{
|
|
1854
1916
|
variant: "toolbar",
|
|
1855
1917
|
className: "cpfe-selector",
|
|
1918
|
+
title: selectedAssistantLabel,
|
|
1856
1919
|
disabled: busy,
|
|
1857
1920
|
onClick: () => setAssistantOpen((open) => open !== true),
|
|
1858
1921
|
},
|
|
1859
|
-
|
|
1922
|
+
react.createElement("span", { className: "cpfe-selector-label" }, selectedAssistantLabel),
|
|
1860
1923
|
// 官方设置项的下拉是「文字在左、chevron 在右」(实测「通用设置」页的
|
|
1861
1924
|
// 「工作区内修改 / 中文 / 详细」都是这个形态)⇒ 图标放在 children 末尾,
|
|
1862
1925
|
// 不走 `icon` prop —— 那个会渲染到文字**前面**,成为页面上唯一一个反向的下拉。
|
|
1863
1926
|
renderable(A.IconChevronDownOutline14)
|
|
1864
|
-
? react.createElement(A.IconChevronDownOutline14, { size: 14 })
|
|
1927
|
+
? react.createElement(A.IconChevronDownOutline14, { size: 14, className: "cpfe-selector-chevron", "aria-hidden": true })
|
|
1865
1928
|
: null,
|
|
1866
1929
|
),
|
|
1867
1930
|
items: assistants.map((item) => ({
|
|
@@ -2166,6 +2229,7 @@ try {
|
|
|
2166
2229
|
renderable(A.Menu)
|
|
2167
2230
|
? react.createElement(A.Menu, {
|
|
2168
2231
|
open: historyOpen,
|
|
2232
|
+
className: "cpfe-selector-menu",
|
|
2169
2233
|
portal: true,
|
|
2170
2234
|
align: "start",
|
|
2171
2235
|
side: "bottom",
|
|
@@ -2175,19 +2239,13 @@ try {
|
|
|
2175
2239
|
{
|
|
2176
2240
|
variant: "toolbar",
|
|
2177
2241
|
className: "cpfe-selector",
|
|
2242
|
+
title: historySelectionLabel,
|
|
2178
2243
|
disabled: busy,
|
|
2179
2244
|
onClick: () => setHistoryOpen((open) => open !== true),
|
|
2180
2245
|
},
|
|
2181
|
-
|
|
2182
|
-
? t("history.pick")
|
|
2183
|
-
: (() => {
|
|
2184
|
-
const entry = draft.history.find((item) => String(item.n) === draft.historyPick)
|
|
2185
|
-
return entry === undefined
|
|
2186
|
-
? t("history.pick")
|
|
2187
|
-
: formatWhen(entry.at) + " · " + t("history.by." + entry.by)
|
|
2188
|
-
})(),
|
|
2246
|
+
react.createElement("span", { className: "cpfe-selector-label" }, historySelectionLabel),
|
|
2189
2247
|
renderable(A.IconChevronDownOutline14)
|
|
2190
|
-
? react.createElement(A.IconChevronDownOutline14, { size: 14 })
|
|
2248
|
+
? react.createElement(A.IconChevronDownOutline14, { size: 14, className: "cpfe-selector-chevron", "aria-hidden": true })
|
|
2191
2249
|
: null,
|
|
2192
2250
|
),
|
|
2193
2251
|
items: draft.history.map((entry) => ({
|
|
@@ -2236,9 +2294,9 @@ try {
|
|
|
2236
2294
|
"div",
|
|
2237
2295
|
// `cpfe-busy` 是 issue #6 的兜底:面板里**所有**会改草稿的控件在忙碌期间一律不可交互。
|
|
2238
2296
|
// 逐个控件加 `disabled` 会漏(本轮就漏了行开关与描述框),一个根类 + 一条 CSS 反而漏不掉。
|
|
2239
|
-
{ className: busy === true ? "cpfe cpfe-busy" : "cpfe" },
|
|
2297
|
+
{ className: busy === true ? "cpfe cpfe-busy" : "cpfe", "aria-busy": busy === true },
|
|
2240
2298
|
assistantList,
|
|
2241
|
-
null,
|
|
2299
|
+
editorReady ? null : react.createElement("div", { className: "cpfe-bar" }, ...feedback),
|
|
2242
2300
|
// 配了却不生效的项:主动点名,而不是让用户对着"我明明写了"发呆。
|
|
2243
2301
|
// `draft` 在没选中任何助手时是 null(列表还没加载完 / 一个都没有)—— 这里必须先守卫,
|
|
2244
2302
|
// 否则整块设置页崩掉(实测:浏览器验收当场报 Cannot read properties of null)。
|
|
@@ -2296,7 +2354,7 @@ try {
|
|
|
2296
2354
|
"section",
|
|
2297
2355
|
null,
|
|
2298
2356
|
react.createElement("h2", { className: "cpfe-h" }, t("editor.heading")),
|
|
2299
|
-
react.createElement(SectionHint, { text: t("editor.hint") }),
|
|
2357
|
+
react.createElement(SectionHint, { text: fillPlaceholders(t("editor.hint"), { name: draft.name || draft.id }) }),
|
|
2300
2358
|
react.createElement(
|
|
2301
2359
|
"div",
|
|
2302
2360
|
{ className: "cpfe-branch" },
|
|
@@ -2309,10 +2367,10 @@ try {
|
|
|
2309
2367
|
{
|
|
2310
2368
|
variant: "primary",
|
|
2311
2369
|
icon: A.IconCheckOutline14 === undefined ? null : react.createElement(A.IconCheckOutline14),
|
|
2312
|
-
disabled: busy || editorReady === false || dirty === false,
|
|
2370
|
+
disabled: busy || syncNeeded || editorReady === false || dirty === false,
|
|
2313
2371
|
onClick: save,
|
|
2314
2372
|
},
|
|
2315
|
-
|
|
2373
|
+
pending === "status.saving" ? t("btn.saving") : t("btn.save"),
|
|
2316
2374
|
),
|
|
2317
2375
|
react.createElement(
|
|
2318
2376
|
A.Button,
|
|
@@ -2325,12 +2383,12 @@ try {
|
|
|
2325
2383
|
// one place that does.
|
|
2326
2384
|
onClick: () => {
|
|
2327
2385
|
if (dirty && typeof window !== "undefined" && window.confirm(t("btn.reloadConfirm")) !== true) return
|
|
2328
|
-
|
|
2386
|
+
reread(true)
|
|
2329
2387
|
},
|
|
2330
2388
|
},
|
|
2331
2389
|
dirty ? t("btn.reloadDiscard") : t("btn.reload"),
|
|
2332
2390
|
),
|
|
2333
|
-
|
|
2391
|
+
...feedback,
|
|
2334
2392
|
),
|
|
2335
2393
|
...(editorReady ? editorSections.filter((section) => section.key === "name") : []),
|
|
2336
2394
|
editorReady
|