dsh-soul 0.4.0 → 0.6.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/README.md +35 -11
- package/README_EN.md +35 -11
- package/client/index.mjs +897 -216
- package/index.mjs +503 -69
- package/lib/config.mjs +97 -3
- package/package.json +1 -1
- package/scripts/verify-config.mjs +94 -1
package/client/index.mjs
CHANGED
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
// 在设置页面添加「个性化」栏目:
|
|
4
4
|
// - 启用/禁用个性化设置
|
|
5
5
|
// - 「关于你」(昵称/职业/介绍)、回复风格和语调、特质、输出语言、自定义指令
|
|
6
|
-
// -
|
|
6
|
+
// - 人设预设:保存当前为预设、一键使用(★ 标记当前匹配项)、删除
|
|
7
|
+
// - Agent 工具:set_persona 确认模式开关
|
|
8
|
+
// - 输入框光轨:Agent 回复中时输入框边框的流光动效(颜色 / 速度 / 粗细 + 实时示例)
|
|
9
|
+
// - dirty 检测(无改动禁用保存)、统一 toast 提示
|
|
7
10
|
// - 查看当前生效提示词(只读)与字符数
|
|
8
11
|
//
|
|
12
|
+
// 光轨另注册到 conversation.input.overlay 槽位(输入框卡片内部),
|
|
13
|
+
// 运行时按会话 running 状态在卡片上挂载/隐藏 SVG 环,见 SoulTrail。
|
|
14
|
+
//
|
|
9
15
|
// 纯 JS + React jsx-runtime 手写(不依赖 JSX 构建),开箱即用。
|
|
10
16
|
// 注意:dsh web 使用 React 17+ 的 jsx-runtime,children 必须放在 props.children
|
|
11
17
|
// 里;不能把 children 当第三个参数传(第三个参数是 key)。
|
|
@@ -41,6 +47,7 @@ window.__ModuleLoader__.load({
|
|
|
41
47
|
// -------------------------------------------------------------------------
|
|
42
48
|
|
|
43
49
|
const NS = 'soul'
|
|
50
|
+
const VERSION = '0.6.0'
|
|
44
51
|
const INITIAL = {
|
|
45
52
|
enabled: true,
|
|
46
53
|
nickname: '',
|
|
@@ -51,10 +58,20 @@ window.__ModuleLoader__.load({
|
|
|
51
58
|
emoji: 'default',
|
|
52
59
|
language: 'zh',
|
|
53
60
|
customInstructions: '',
|
|
61
|
+
// set_persona 确认模式(v0.5.0)
|
|
62
|
+
requireToolConfirmation: false,
|
|
63
|
+
// 输入框光轨(v0.6.0):Agent 回复中时输入框边框的流光动效
|
|
64
|
+
trailEnabled: true,
|
|
65
|
+
trailColor: '#679EFE',
|
|
66
|
+
trailSpeed: 'slow',
|
|
67
|
+
trailWidth: 'thin',
|
|
68
|
+
// 人设预设库与当前匹配项(GET /api/soul/personas 带回)
|
|
69
|
+
personas: null,
|
|
70
|
+
activePersona: null,
|
|
54
71
|
loading: false,
|
|
55
72
|
saving: false,
|
|
56
73
|
error: null,
|
|
57
|
-
//
|
|
74
|
+
// 最近一次保存实际变化的字段名数组(来自写路径的 changed)
|
|
58
75
|
lastChanged: null
|
|
59
76
|
}
|
|
60
77
|
|
|
@@ -87,6 +104,95 @@ window.__ModuleLoader__.load({
|
|
|
87
104
|
return error instanceof Error ? error.message : String(error)
|
|
88
105
|
}
|
|
89
106
|
|
|
107
|
+
// -------------------------------------------------------------------------
|
|
108
|
+
// 输入框光轨:渲染层
|
|
109
|
+
// -------------------------------------------------------------------------
|
|
110
|
+
//
|
|
111
|
+
// 匀速原理:沿「卡片边缘」画一个 SVG 圆角矩形,pathLength 归一化到 100,
|
|
112
|
+
// 用 stroke-dashoffset 线性动画推进 —— 按弧长运动,圆角与直边线速度一致
|
|
113
|
+
// (旋转 conic-gradient 的角速度恒定,但周长线速度会随位置变化,故不采用)。
|
|
114
|
+
// 渐隐拖尾:8 层等长 dash 依次错位叠加、透明度递减 —— 头部各层重叠最亮,
|
|
115
|
+
// 向后剩余层数递减形成拖尾;所有层共用同一时长,整体仍严格匀速。
|
|
116
|
+
// 与边框重合:stroke 中心线落在容器边框盒边缘(rect 即 0,0,w,h),
|
|
117
|
+
// 并靠 overflow:visible 让外半圈可见;光轨开启时隐藏宿主自身细边框,
|
|
118
|
+
// 保证边界上只有一条线。
|
|
119
|
+
|
|
120
|
+
const TRAIL_DASH = 12
|
|
121
|
+
const TRAIL_LAYERS = 8
|
|
122
|
+
const TRAIL_LAYER_STEP = 2.2
|
|
123
|
+
const TRAIL_RADIUS = 22
|
|
124
|
+
const TRAIL_COLOR_FALLBACK = '#679EFE'
|
|
125
|
+
const TRAIL_PRESET_COLORS = ['#679EFE', '#22C55E', '#EF4444', '#A855F7', '#06B6D4', '#F59E0B']
|
|
126
|
+
const TRAIL_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/
|
|
127
|
+
const TRAIL_SPEED_VALUES = ['slow', 'normal', 'fast']
|
|
128
|
+
const TRAIL_WIDTH_VALUES = ['thin', 'normal', 'thick']
|
|
129
|
+
|
|
130
|
+
// 颜色容错:输入中途(如只打了 #6)回退到默认色,避免示例与提示丢失;
|
|
131
|
+
// 统一大写,与宿主归一化结果及调色板取值一致
|
|
132
|
+
function safeTrailColor(value) {
|
|
133
|
+
if (typeof value !== 'string') return TRAIL_COLOR_FALLBACK
|
|
134
|
+
const text = value.trim()
|
|
135
|
+
return TRAIL_COLOR_PATTERN.test(text) ? text.toUpperCase() : TRAIL_COLOR_FALLBACK
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function createSvgNode(tag, attrs) {
|
|
139
|
+
const node = document.createElementNS('http://www.w3.org/2000/svg', tag)
|
|
140
|
+
for (const [key, value] of Object.entries(attrs)) node.setAttribute(key, value)
|
|
141
|
+
return node
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 构建拖尾环;层数、dash 长度与错位步长与 CSS 中的 keyframes 一一对应
|
|
145
|
+
function buildTrailSvg() {
|
|
146
|
+
const svg = createSvgNode('svg', { class: 'soul-trail-svg', 'aria-hidden': 'true' })
|
|
147
|
+
for (let layer = 0; layer < TRAIL_LAYERS; layer++) {
|
|
148
|
+
svg.appendChild(createSvgNode('rect', {
|
|
149
|
+
class: 'soul-trail-layer',
|
|
150
|
+
'data-layer': String(layer),
|
|
151
|
+
fill: 'none',
|
|
152
|
+
pathLength: '100',
|
|
153
|
+
'stroke-dasharray': `${TRAIL_DASH} ${100 - TRAIL_DASH}`,
|
|
154
|
+
'stroke-dashoffset': String(layer * TRAIL_LAYER_STEP)
|
|
155
|
+
}))
|
|
156
|
+
}
|
|
157
|
+
return svg
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 让环与容器像素尺寸一致(viewBox 用像素),圆角半径与宿主输入框卡片一致
|
|
161
|
+
function sizeTrailSvg(svg, width, height) {
|
|
162
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return
|
|
163
|
+
if (svg.dataset.trailWidth === String(width) && svg.dataset.trailHeight === String(height)) return
|
|
164
|
+
svg.dataset.trailWidth = String(width)
|
|
165
|
+
svg.dataset.trailHeight = String(height)
|
|
166
|
+
svg.setAttribute('viewBox', `0 0 ${width} ${height}`)
|
|
167
|
+
for (const rect of svg.querySelectorAll('.soul-trail-layer')) {
|
|
168
|
+
rect.setAttribute('x', '0')
|
|
169
|
+
rect.setAttribute('y', '0')
|
|
170
|
+
rect.setAttribute('width', String(width))
|
|
171
|
+
rect.setAttribute('height', String(height))
|
|
172
|
+
rect.setAttribute('rx', String(TRAIL_RADIUS))
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 把环挂到宿主元素上并跟随其尺寸变化(输入框随内容增高时环不变形);返回卸载函数
|
|
177
|
+
function mountTrailRing(host) {
|
|
178
|
+
const svg = buildTrailSvg()
|
|
179
|
+
host.appendChild(svg)
|
|
180
|
+
const sync = () => sizeTrailSvg(svg, host.clientWidth, host.clientHeight)
|
|
181
|
+
sync()
|
|
182
|
+
let observer = null
|
|
183
|
+
if (typeof ResizeObserver === 'function') {
|
|
184
|
+
observer = new ResizeObserver(sync)
|
|
185
|
+
observer.observe(host)
|
|
186
|
+
} else if (typeof globalThis.addEventListener === 'function') {
|
|
187
|
+
globalThis.addEventListener('resize', sync)
|
|
188
|
+
}
|
|
189
|
+
return () => {
|
|
190
|
+
if (observer) observer.disconnect()
|
|
191
|
+
else if (typeof globalThis.removeEventListener === 'function') globalThis.removeEventListener('resize', sync)
|
|
192
|
+
svg.remove()
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
90
196
|
var SoulController = class {
|
|
91
197
|
constructor(fetcher = (input, init) => fetch(input, init)) {
|
|
92
198
|
this.fetcher = fetcher
|
|
@@ -94,29 +200,32 @@ window.__ModuleLoader__.load({
|
|
|
94
200
|
this.disposed = false
|
|
95
201
|
}
|
|
96
202
|
|
|
203
|
+
// 把服务端返回的完整配置映射进 store
|
|
204
|
+
applyConfig(s, config) {
|
|
205
|
+
s.enabled = config.enabled
|
|
206
|
+
s.nickname = config.nickname || ''
|
|
207
|
+
s.occupation = config.occupation || ''
|
|
208
|
+
s.bio = config.bio || ''
|
|
209
|
+
s.style = config.style
|
|
210
|
+
s.headingLists = config.headingLists || 'default'
|
|
211
|
+
s.emoji = config.emoji || 'default'
|
|
212
|
+
s.language = config.language || 'zh'
|
|
213
|
+
s.customInstructions = config.customInstructions
|
|
214
|
+
s.requireToolConfirmation = config.requireToolConfirmation === true
|
|
215
|
+
s.trailEnabled = config.trailEnabled !== false
|
|
216
|
+
s.trailColor = config.trailColor || TRAIL_COLOR_FALLBACK
|
|
217
|
+
s.trailSpeed = config.trailSpeed || 'slow'
|
|
218
|
+
s.trailWidth = config.trailWidth || 'thin'
|
|
219
|
+
}
|
|
220
|
+
|
|
97
221
|
async loadConfig() {
|
|
98
222
|
if (this.disposed) return
|
|
99
223
|
this.store.update(s => { s.loading = true; s.error = null })
|
|
100
224
|
|
|
101
225
|
try {
|
|
102
|
-
const
|
|
103
|
-
const response = await this.fetcher(url)
|
|
104
|
-
const payload = await response.json().catch(() => ({}))
|
|
105
|
-
|
|
106
|
-
if (!response.ok || payload.ok !== true) {
|
|
107
|
-
throw new Error(payload.error || `HTTP ${response.status}`)
|
|
108
|
-
}
|
|
109
|
-
|
|
226
|
+
const payload = await this.postJSON('/api/soul/config', null, { method: 'GET' })
|
|
110
227
|
this.store.update(s => {
|
|
111
|
-
s
|
|
112
|
-
s.nickname = payload.config.nickname || ''
|
|
113
|
-
s.occupation = payload.config.occupation || ''
|
|
114
|
-
s.bio = payload.config.bio || ''
|
|
115
|
-
s.style = payload.config.style
|
|
116
|
-
s.headingLists = payload.config.headingLists || 'default'
|
|
117
|
-
s.emoji = payload.config.emoji || 'default'
|
|
118
|
-
s.language = payload.config.language || 'zh'
|
|
119
|
-
s.customInstructions = payload.config.customInstructions
|
|
228
|
+
this.applyConfig(s, payload.config)
|
|
120
229
|
s.loading = false
|
|
121
230
|
})
|
|
122
231
|
} catch (error) {
|
|
@@ -133,31 +242,13 @@ window.__ModuleLoader__.load({
|
|
|
133
242
|
this.store.update(s => { s.saving = true; s.error = null })
|
|
134
243
|
|
|
135
244
|
try {
|
|
136
|
-
const
|
|
137
|
-
const response = await this.fetcher(url, {
|
|
138
|
-
method: 'POST',
|
|
139
|
-
headers: { 'content-type': 'application/json' },
|
|
140
|
-
body: JSON.stringify(config)
|
|
141
|
-
})
|
|
142
|
-
const payload = await response.json().catch(() => ({}))
|
|
143
|
-
|
|
144
|
-
if (!response.ok || payload.ok !== true) {
|
|
145
|
-
throw new Error(payload.error || `HTTP ${response.status}`)
|
|
146
|
-
}
|
|
147
|
-
|
|
245
|
+
const payload = await this.postJSON('/api/soul/config', config)
|
|
148
246
|
this.store.update(s => {
|
|
149
|
-
s
|
|
150
|
-
s.nickname = payload.config.nickname || ''
|
|
151
|
-
s.occupation = payload.config.occupation || ''
|
|
152
|
-
s.bio = payload.config.bio || ''
|
|
153
|
-
s.style = payload.config.style
|
|
154
|
-
s.headingLists = payload.config.headingLists || 'default'
|
|
155
|
-
s.emoji = payload.config.emoji || 'default'
|
|
156
|
-
s.language = payload.config.language || 'zh'
|
|
157
|
-
s.customInstructions = payload.config.customInstructions
|
|
247
|
+
this.applyConfig(s, payload.config)
|
|
158
248
|
s.lastChanged = Array.isArray(payload.changed) ? payload.changed : []
|
|
159
249
|
s.saving = false
|
|
160
250
|
})
|
|
251
|
+
return payload
|
|
161
252
|
} catch (error) {
|
|
162
253
|
if (this.disposed) return
|
|
163
254
|
this.store.update(s => {
|
|
@@ -172,27 +263,13 @@ window.__ModuleLoader__.load({
|
|
|
172
263
|
this.store.update(s => { s.saving = true; s.error = null })
|
|
173
264
|
|
|
174
265
|
try {
|
|
175
|
-
const
|
|
176
|
-
const response = await this.fetcher(url, { method: 'POST' })
|
|
177
|
-
const payload = await response.json().catch(() => ({}))
|
|
178
|
-
|
|
179
|
-
if (!response.ok || payload.ok !== true) {
|
|
180
|
-
throw new Error(payload.error || `HTTP ${response.status}`)
|
|
181
|
-
}
|
|
182
|
-
|
|
266
|
+
const payload = await this.postJSON('/api/soul/config/reset')
|
|
183
267
|
this.store.update(s => {
|
|
184
|
-
s
|
|
185
|
-
s.nickname = payload.config.nickname || ''
|
|
186
|
-
s.occupation = payload.config.occupation || ''
|
|
187
|
-
s.bio = payload.config.bio || ''
|
|
188
|
-
s.style = payload.config.style
|
|
189
|
-
s.headingLists = payload.config.headingLists || 'default'
|
|
190
|
-
s.emoji = payload.config.emoji || 'default'
|
|
191
|
-
s.language = payload.config.language || 'zh'
|
|
192
|
-
s.customInstructions = payload.config.customInstructions
|
|
268
|
+
this.applyConfig(s, payload.config)
|
|
193
269
|
s.lastChanged = Array.isArray(payload.changed) ? payload.changed : []
|
|
194
270
|
s.saving = false
|
|
195
271
|
})
|
|
272
|
+
return payload
|
|
196
273
|
} catch (error) {
|
|
197
274
|
if (this.disposed) return
|
|
198
275
|
this.store.update(s => {
|
|
@@ -202,10 +279,39 @@ window.__ModuleLoader__.load({
|
|
|
202
279
|
}
|
|
203
280
|
}
|
|
204
281
|
|
|
205
|
-
//
|
|
206
|
-
async
|
|
207
|
-
|
|
208
|
-
|
|
282
|
+
// 人设预设:列表 / 保存 / 使用 / 删除
|
|
283
|
+
async fetchPersonas() {
|
|
284
|
+
return await this.postJSON('/api/soul/personas', null, { method: 'GET' })
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async savePersona(name) {
|
|
288
|
+
return await this.postJSON('/api/soul/personas/save', { name })
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async usePersona(name) {
|
|
292
|
+
const payload = await this.postJSON('/api/soul/personas/use', { name })
|
|
293
|
+
if (!this.disposed) {
|
|
294
|
+
this.store.update(s => {
|
|
295
|
+
this.applyConfig(s, payload.config)
|
|
296
|
+
s.lastChanged = Array.isArray(payload.changed) ? payload.changed : []
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
return payload
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async deletePersona(name) {
|
|
303
|
+
return await this.postJSON('/api/soul/personas/delete', { name })
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 统一请求:body 为 null 时不携带请求体(GET 用)
|
|
307
|
+
async postJSON(path, body, overrides = {}) {
|
|
308
|
+
const url = new URL(path, hostBase())
|
|
309
|
+
const init = { method: overrides.method || 'POST', ...overrides }
|
|
310
|
+
if (body !== null && body !== undefined) {
|
|
311
|
+
init.headers = { 'content-type': 'application/json' }
|
|
312
|
+
init.body = JSON.stringify(body)
|
|
313
|
+
}
|
|
314
|
+
const response = await this.fetcher(url, init)
|
|
209
315
|
const payload = await response.json().catch(() => ({}))
|
|
210
316
|
if (!response.ok || payload.ok !== true) {
|
|
211
317
|
throw new Error(payload.error || `HTTP ${response.status}`)
|
|
@@ -213,6 +319,12 @@ window.__ModuleLoader__.load({
|
|
|
213
319
|
return payload
|
|
214
320
|
}
|
|
215
321
|
|
|
322
|
+
// 读取当前生效(已保存)的提示词:{ ok, prompt, enabled }
|
|
323
|
+
async fetchPrompt() {
|
|
324
|
+
const payload = await this.postJSON('/api/soul/prompt', null, { method: 'GET' })
|
|
325
|
+
return payload
|
|
326
|
+
}
|
|
327
|
+
|
|
216
328
|
async dispose() {
|
|
217
329
|
this.disposed = true
|
|
218
330
|
}
|
|
@@ -223,32 +335,112 @@ window.__ModuleLoader__.load({
|
|
|
223
335
|
// -------------------------------------------------------------------------
|
|
224
336
|
|
|
225
337
|
const css = [
|
|
226
|
-
'.soul-section{margin:24px 0;padding:20px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-
|
|
338
|
+
'.soul-section{margin:24px 0;padding:20px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}',
|
|
227
339
|
'.soul-section h3{margin:0 0 16px 0;font-size:16px;font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
340
|
+
'.soul-title-row{display:flex;align-items:center;justify-content:space-between;gap:12px;min-width:0}',
|
|
341
|
+
'.soul-version{flex:0 0 auto;color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:400;line-height:1.4;white-space:nowrap}',
|
|
228
342
|
'.soul-field{margin-bottom:16px;width:100%}',
|
|
229
343
|
'.soul-field label{display:block;margin-bottom:6px;font-size:13px;font-weight:500;color:var(--dsw-alias-label-secondary)}',
|
|
230
|
-
'.soul-field select,.soul-field textarea,.soul-field input[type=text]{width:100%;padding:8px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-
|
|
344
|
+
'.soul-field select,.soul-field textarea,.soul-field input[type=text]{width:100%;padding:8px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;box-sizing:border-box}',
|
|
345
|
+
'.soul-field select option{color:var(--dsw-alias-label-primary)}',
|
|
346
|
+
'.soul-section{color-scheme:light}',
|
|
347
|
+
'body[data-ds-dark-theme] .soul-section{color-scheme:dark}',
|
|
348
|
+
'.soul-field select{color-scheme:light}',
|
|
349
|
+
'body[data-ds-dark-theme] .soul-field select{color-scheme:dark}',
|
|
231
350
|
'.soul-field textarea{min-height:100px;resize:vertical}',
|
|
232
351
|
'.soul-field textarea.soul-textarea-sm{min-height:60px}',
|
|
233
352
|
'.soul-group-title{margin:20px 0 12px 0;padding-bottom:8px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:13px;font-weight:600;color:var(--dsw-alias-label-secondary)}',
|
|
234
|
-
'.soul-
|
|
353
|
+
'.soul-accordion{margin:12px 0;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;overflow:visible;background:var(--dsw-alias-bg-layer-1)}',
|
|
354
|
+
'.soul-accordion-open{position:relative;z-index:20}',
|
|
355
|
+
// 展开时标题行下沿改直角:与下方内容区形成一条清晰的水平交界,
|
|
356
|
+
// 避免"标题与内容同为一片底色、分不清哪块是标题"
|
|
357
|
+
'.soul-accordion-open .soul-accordion-trigger{border-radius:5px 5px 0 0}',
|
|
358
|
+
'.soul-accordion-trigger{display:flex;align-items:center;width:100%;min-height:44px;gap:10px;padding:10px 12px;border:0;border-radius:5px;background:var(--dsw-alias-interactive-bg-active,rgba(127,127,127,0.12));color:var(--dsw-alias-label-primary);font:inherit;text-align:left;cursor:pointer;transition:background-color .15s ease}',
|
|
359
|
+
'.soul-accordion-trigger:hover{background:var(--dsw-alias-interactive-bg-hover-accent,rgba(127,127,127,0.2))}',
|
|
360
|
+
'.soul-accordion-title{flex:0 0 auto;font-size:13px;font-weight:600}',
|
|
361
|
+
'.soul-accordion-summary{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400}',
|
|
362
|
+
'.soul-accordion-icon{flex:0 0 18px;color:var(--dsw-alias-label-secondary);font-size:18px;line-height:1;text-align:center}',
|
|
363
|
+
'.soul-accordion-body{padding:12px;border-top:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1)}',
|
|
364
|
+
'.soul-field select:focus,.soul-field textarea:focus,.soul-field input[type=text]:focus{outline:none;border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 2px color-mix(in srgb, var(--dsw-alias-brand-primary) 25%, transparent)}',
|
|
235
365
|
'.soul-toggle{display:flex;align-items:center;gap:8px;margin-bottom:16px}',
|
|
236
366
|
'.soul-toggle label{margin:0;font-size:13px;color:var(--dsw-alias-label-primary)}',
|
|
237
367
|
'.soul-buttons{display:flex;gap:8px;margin-top:16px}',
|
|
238
|
-
'.soul-
|
|
368
|
+
'.soul-quick-toggle{position:relative;isolation:isolate;display:inline-flex;align-items:center;gap:6px;min-height:28px;overflow:hidden;padding:4px 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;line-height:1;cursor:pointer;white-space:nowrap}',
|
|
369
|
+
'.soul-quick-toggle:hover{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}',
|
|
370
|
+
'.soul-quick-toggle[data-enabled="true"]{color:var(--dsw-alias-brand-primary)}',
|
|
371
|
+
'.soul-quick-toggle[data-enabled="true"]:hover{border-color:#ef4444;color:#ef4444;box-shadow:0 0 0 2px rgba(239,68,68,.18)}',
|
|
372
|
+
'.soul-quick-toggle[data-enabled="false"]:hover{border-color:#22c55e;color:#22c55e;box-shadow:0 0 0 2px rgba(34,197,94,.18)}',
|
|
373
|
+
'.soul-quick-toggle[data-enabled="true"]:hover::before{background:rgba(239,68,68,.24);animation:soul-toggle-red-sweep .55s ease-out both}',
|
|
374
|
+
'.soul-quick-toggle[data-enabled="false"]:hover::before{background:rgba(34,197,94,.24);animation:soul-toggle-green-sweep .55s ease-out both}',
|
|
375
|
+
'.soul-quick-toggle::before{content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;transform:translateX(100%)}',
|
|
376
|
+
'.soul-quick-toggle > *{position:relative;z-index:1}',
|
|
377
|
+
'.soul-quick-toggle-dot{width:7px;height:7px;flex:0 0 7px;border-radius:50%;background:var(--dsw-alias-label-tertiary)}',
|
|
378
|
+
'.soul-quick-toggle[data-enabled="true"] .soul-quick-toggle-dot{background:var(--dsw-static-green-500,#22c55e);animation:soul-status-pulse 1.8s ease-in-out infinite}',
|
|
379
|
+
'.soul-quick-toggle:disabled{cursor:wait;opacity:.6}',
|
|
380
|
+
'@keyframes soul-toggle-red-sweep{from{transform:translateX(100%)}to{transform:translateX(-100%)}}',
|
|
381
|
+
'@keyframes soul-toggle-green-sweep{from{transform:translateX(100%)}to{transform:translateX(-100%)}}',
|
|
382
|
+
'@keyframes soul-status-pulse{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--dsw-static-green-500,#22c55e) 35%,transparent);opacity:.75}50%{box-shadow:0 0 0 4px color-mix(in srgb,var(--dsw-static-green-500,#22c55e) 0%,transparent);opacity:1}}',
|
|
383
|
+
'@media (prefers-reduced-motion:reduce){.soul-quick-toggle[data-enabled="true"] .soul-quick-toggle-dot{animation:none}.soul-quick-toggle[data-enabled="true"]:hover::before,.soul-quick-toggle[data-enabled="false"]:hover::before{animation:none;transform:none}}',
|
|
384
|
+
'.soul-error{margin-top:8px;padding:8px 12px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-label-error);border-radius:6px;color:var(--dsw-alias-label-error);font-size:12px;width:100%;box-sizing:border-box}',
|
|
239
385
|
'.soul-hint{position:relative;display:inline-flex;align-items:center;margin-left:6px;color:var(--dsw-alias-label-secondary);cursor:help;vertical-align:middle}',
|
|
240
386
|
'.soul-hint:hover{color:var(--dsw-alias-label-primary)}',
|
|
241
387
|
'.soul-hint-tip{position:absolute;bottom:calc(100% + 8px);left:0;padding:8px 10px;background:#ffffff;border:1px solid rgba(0,0,0,0.1);border-radius:6px;font-size:12px;font-weight:400;line-height:1.5;color:rgba(0,0,0,0.85);white-space:normal;width:max-content;max-width:260px;text-align:left;opacity:0;visibility:hidden;transition:opacity .15s ease;pointer-events:none;z-index:9999;box-shadow:0 4px 12px rgba(0,0,0,0.15)}',
|
|
242
388
|
'.soul-hint:hover .soul-hint-tip{opacity:1;visibility:visible}',
|
|
243
389
|
'.soul-status{margin-top:8px;font-size:12px;color:var(--dsw-alias-label-secondary)}',
|
|
244
390
|
'.soul-dirty{color:#d46b08}',
|
|
245
|
-
'.soul-
|
|
391
|
+
'.soul-persona-row{display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px dashed var(--dsw-alias-border-l2);font-size:12px}',
|
|
392
|
+
'.soul-persona-name{color:var(--dsw-alias-label-primary);font-weight:500;white-space:nowrap}',
|
|
393
|
+
'.soul-persona-meta{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary)}',
|
|
394
|
+
'.soul-persona-actions{display:flex;gap:10px;white-space:nowrap}',
|
|
395
|
+
'.soul-persona-save{display:flex;gap:8px}',
|
|
396
|
+
'.soul-persona-save input[type=text]{flex:1}',
|
|
246
397
|
'.soul-prompt-link{background:none;border:none;padding:0;font-size:12px;color:var(--dsw-alias-label-secondary);cursor:pointer;text-decoration:underline}',
|
|
247
398
|
'.soul-prompt-link:hover{color:var(--dsw-alias-label-primary)}',
|
|
248
|
-
'.soul-
|
|
399
|
+
'.soul-persona-danger{color:var(--dsw-alias-label-error)}',
|
|
400
|
+
'.soul-prompt-pre{margin:8px 0 0 0;padding:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-word;max-height:240px;overflow:auto;font-family:Consolas,Menlo,monospace;text-align:left;width:100%;box-sizing:border-box}',
|
|
249
401
|
'.soul-toast{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);padding:8px 16px;border-radius:6px;font-size:13px;font-weight:500;z-index:10000;box-shadow:0 2px 8px rgba(0,0,0,0.15);pointer-events:none;text-align:center;min-width:auto;white-space:nowrap}',
|
|
250
402
|
'.soul-toast-success{background:#f6ffed;border:1px solid #b7eb8f;color:#52c41a}',
|
|
251
|
-
'
|
|
403
|
+
'.soul-toast-error{background:#fff1f0;border:1px solid #ffa39e;color:#f5222d}',
|
|
404
|
+
'@keyframes fadeInOut{0%{opacity:0;transform:translate(-50%,-50%) scale(0.9)}15%{opacity:1;transform:translate(-50%,-50%) scale(1)}85%{opacity:1;transform:translate(-50%,-50%) scale(1)}100%{opacity:0;transform:translate(-50%,-50%) scale(0.9)}}',
|
|
405
|
+
// —— 输入框光轨 ——
|
|
406
|
+
// 环挂在卡片上:stroke 中心线即卡片边缘,外半圈靠 overflow:visible 显示
|
|
407
|
+
'.soul-trail-svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none}',
|
|
408
|
+
// 速度档位(整体比首版慢一档):normal 3.6s / slow 4.8s / fast 2.4s
|
|
409
|
+
'.soul-trail-layer{fill:none;stroke:var(--soul-trail-color,#679EFE);stroke-width:2.5;stroke-linecap:butt;animation-name:soul-trail-run-0;animation-duration:3.6s;animation-timing-function:linear;animation-iteration-count:infinite}',
|
|
410
|
+
'.soul-trail-layer[data-layer="0"]{opacity:1;stroke-linecap:round}',
|
|
411
|
+
'.soul-trail-layer[data-layer="1"]{animation-name:soul-trail-run-1;opacity:.87}',
|
|
412
|
+
'.soul-trail-layer[data-layer="2"]{animation-name:soul-trail-run-2;opacity:.74}',
|
|
413
|
+
'.soul-trail-layer[data-layer="3"]{animation-name:soul-trail-run-3;opacity:.61}',
|
|
414
|
+
'.soul-trail-layer[data-layer="4"]{animation-name:soul-trail-run-4;opacity:.47}',
|
|
415
|
+
'.soul-trail-layer[data-layer="5"]{animation-name:soul-trail-run-5;opacity:.34}',
|
|
416
|
+
'.soul-trail-layer[data-layer="6"]{animation-name:soul-trail-run-6;opacity:.21}',
|
|
417
|
+
'.soul-trail-layer[data-layer="7"]{animation-name:soul-trail-run-7;opacity:.08}',
|
|
418
|
+
'@keyframes soul-trail-run-0{from{stroke-dashoffset:0px}to{stroke-dashoffset:-100px}}',
|
|
419
|
+
'@keyframes soul-trail-run-1{from{stroke-dashoffset:2.2px}to{stroke-dashoffset:-97.8px}}',
|
|
420
|
+
'@keyframes soul-trail-run-2{from{stroke-dashoffset:4.4px}to{stroke-dashoffset:-95.6px}}',
|
|
421
|
+
'@keyframes soul-trail-run-3{from{stroke-dashoffset:6.6px}to{stroke-dashoffset:-93.4px}}',
|
|
422
|
+
'@keyframes soul-trail-run-4{from{stroke-dashoffset:8.8px}to{stroke-dashoffset:-91.2px}}',
|
|
423
|
+
'@keyframes soul-trail-run-5{from{stroke-dashoffset:11px}to{stroke-dashoffset:-89px}}',
|
|
424
|
+
'@keyframes soul-trail-run-6{from{stroke-dashoffset:13.2px}to{stroke-dashoffset:-86.8px}}',
|
|
425
|
+
'@keyframes soul-trail-run-7{from{stroke-dashoffset:15.4px}to{stroke-dashoffset:-84.6px}}',
|
|
426
|
+
'[data-soul-trail-speed="slow"] .soul-trail-layer{animation-duration:4.8s}',
|
|
427
|
+
'[data-soul-trail-speed="fast"] .soul-trail-layer{animation-duration:2.4s}',
|
|
428
|
+
'[data-soul-trail-width="thin"] .soul-trail-layer{stroke-width:1.5}',
|
|
429
|
+
'[data-soul-trail-width="thick"] .soul-trail-layer{stroke-width:4}',
|
|
430
|
+
'[data-soul-trail="off"] .soul-trail-svg{display:none}',
|
|
431
|
+
// 光轨开启时隐藏宿主输入框自身的细边框(保留 1px 占位,避免布局跳动),
|
|
432
|
+
// 使边界上只存在光轨一条线
|
|
433
|
+
'[data-composer-card][data-soul-trail="on"]{--dsw-elevation-stroke-color:transparent}',
|
|
434
|
+
// 设置页:颜色控件与实时示例
|
|
435
|
+
'.soul-trail-colors{display:flex;align-items:center;gap:8px;flex-wrap:wrap}',
|
|
436
|
+
'.soul-trail-swatch{width:26px;height:26px;padding:0;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer}',
|
|
437
|
+
'.soul-trail-swatch[data-active="true"]{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}',
|
|
438
|
+
'.soul-trail-picker{width:34px;height:26px;padding:0;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:none;cursor:pointer}',
|
|
439
|
+
'.soul-field .soul-trail-colors input.soul-trail-hex{width:104px;padding:5px 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px;line-height:1.4;font-family:Consolas,Menlo,monospace;box-sizing:border-box}',
|
|
440
|
+
'.soul-trail-preview{position:relative;margin-top:6px;padding:16px;border:1px solid var(--dsw-alias-border-l2);border-radius:22px;background:var(--dsw-alias-bg-layer-1)}',
|
|
441
|
+
'.soul-trail-preview[data-soul-trail="on"]{border-color:transparent}',
|
|
442
|
+
'.soul-trail-preview-text{font-size:12px;color:var(--dsw-alias-label-tertiary)}',
|
|
443
|
+
'@media (prefers-reduced-motion:reduce){.soul-trail-layer{animation:none}}'
|
|
252
444
|
].join('')
|
|
253
445
|
|
|
254
446
|
const tagId = 'dsh-soul/styles.css'
|
|
@@ -275,7 +467,16 @@ window.__ModuleLoader__.load({
|
|
|
275
467
|
const zh = {
|
|
276
468
|
'nav.label': '个性化',
|
|
277
469
|
'settings.title': '个性化设置',
|
|
470
|
+
'accordion.filled': '项已填写',
|
|
471
|
+
'accordion.empty': '未填写',
|
|
472
|
+
'accordion.personas': '个预设',
|
|
473
|
+
'accordion.on': '已开启',
|
|
474
|
+
'accordion.off': '未开启',
|
|
278
475
|
'toggle.enabled': '启用个性化设置',
|
|
476
|
+
'quick.enable': '开启个性化',
|
|
477
|
+
'quick.disable': '关闭个性化',
|
|
478
|
+
'quick.statusEnabled': '个性化已启用',
|
|
479
|
+
'quick.statusDisabled': '个性化已关闭',
|
|
279
480
|
'group.aboutYou': '关于你',
|
|
280
481
|
'field.nickname': '用户昵称',
|
|
281
482
|
'field.nicknamePlaceholder': '输入你的昵称,回复时会称呼你',
|
|
@@ -295,6 +496,13 @@ window.__ModuleLoader__.load({
|
|
|
295
496
|
'field.instructions': '自定义指令',
|
|
296
497
|
'hint.instructions': '补充角色、习惯等个性化要求。建议不要重复设置回复风格和语调类似的话术,以免与上方选项冲突。',
|
|
297
498
|
'field.instructionsPlaceholder': '输入自定义指令,可以是其他行为、回复风格和语调等偏好设置',
|
|
499
|
+
'group.personas': '人设预设',
|
|
500
|
+
'personas.save': '保存当前为预设',
|
|
501
|
+
'personas.savePlaceholder': '预设名称(1-30 字符)',
|
|
502
|
+
'personas.use': '使用',
|
|
503
|
+
'personas.delete': '删除',
|
|
504
|
+
'personas.confirmDelete': '确定删除预设「{name}」?',
|
|
505
|
+
'personas.empty': '暂无人设预设,保存当前配置后可一键切换',
|
|
298
506
|
'style.professional': '专业严谨',
|
|
299
507
|
'style.casual': '轻松自然',
|
|
300
508
|
'style.humorous': '幽默风趣',
|
|
@@ -306,6 +514,9 @@ window.__ModuleLoader__.load({
|
|
|
306
514
|
'trait.emoji.default': '默认',
|
|
307
515
|
'trait.emoji.more': '增强(使用较多表情符号)',
|
|
308
516
|
'trait.emoji.less': '减弱(尽量减少使用表情符号)',
|
|
517
|
+
'group.tool': 'Agent 工具',
|
|
518
|
+
'field.toolConfirm': '人设变更需确认',
|
|
519
|
+
'hint.toolConfirm': '开启后,Agent 通过 set_persona 工具做出的修改不会立即生效,需在会话中使用 /soul confirm 确认或 /soul reject 拒绝。',
|
|
309
520
|
'button.save': '保存设置',
|
|
310
521
|
'button.saving': '保存中...',
|
|
311
522
|
'button.reset': '重置默认',
|
|
@@ -314,17 +525,49 @@ window.__ModuleLoader__.load({
|
|
|
314
525
|
'toast.saved': '✅ 设置已保存',
|
|
315
526
|
'toast.noChanges': '✅ 配置无变化',
|
|
316
527
|
'toast.reset': '✅ 已重置为默认值',
|
|
528
|
+
'toast.personaSaved': '✅ 预设已保存',
|
|
529
|
+
'toast.personaUsed': '✅ 预设已应用',
|
|
530
|
+
'toast.personaUnchanged': 'ℹ 预设与当前配置一致',
|
|
531
|
+
'toast.personaDeleted': '✅ 预设已删除',
|
|
317
532
|
'prompt.view': '查看当前生效提示词',
|
|
318
533
|
'prompt.hide': '收起提示词',
|
|
534
|
+
'prompt.title': '当前生效提示词',
|
|
535
|
+
'prompt.summaryEnabled': '已启用',
|
|
536
|
+
'prompt.summaryDisabled': '已禁用',
|
|
537
|
+
'prompt.summaryChars': '{n} 字符',
|
|
319
538
|
'prompt.loading': '提示词加载中...',
|
|
320
539
|
'prompt.empty': '(个性化已禁用,提示词为空)',
|
|
321
|
-
'prompt.chars': '当前字符数:{n}'
|
|
540
|
+
'prompt.chars': '当前字符数:{n}',
|
|
541
|
+
'toast.saveFailed': '❌ 保存失败,请检查配置',
|
|
542
|
+
'trail.title': '输入框光轨',
|
|
543
|
+
'trail.enable': '启用输入框光轨',
|
|
544
|
+
'trail.color': '光轨颜色',
|
|
545
|
+
'trail.speed': '流动速度',
|
|
546
|
+
'trail.speed.slow': '慢',
|
|
547
|
+
'trail.speed.normal': '中',
|
|
548
|
+
'trail.speed.fast': '快',
|
|
549
|
+
'trail.width': '光轨粗细',
|
|
550
|
+
'trail.width.thin': '细',
|
|
551
|
+
'trail.width.normal': '中',
|
|
552
|
+
'trail.width.thick': '粗',
|
|
553
|
+
'trail.preview': '效果示例',
|
|
554
|
+
'trail.previewText': 'Agent 回复时,输入框边框会出现流动的光轨',
|
|
555
|
+
'hint.trail': 'Agent 回复期间,输入框边框显示沿边流动的光轨。点击色块、使用取色器,或直接填写十六进制色值(如 #679EFE)。'
|
|
322
556
|
}
|
|
323
557
|
|
|
324
558
|
const en = {
|
|
325
559
|
'nav.label': 'Personalization',
|
|
326
560
|
'settings.title': 'Personalization Settings',
|
|
561
|
+
'accordion.filled': 'fields filled',
|
|
562
|
+
'accordion.empty': 'No fields set',
|
|
563
|
+
'accordion.personas': 'personas',
|
|
564
|
+
'accordion.on': 'enabled',
|
|
565
|
+
'accordion.off': 'disabled',
|
|
327
566
|
'toggle.enabled': 'Enable personalization',
|
|
567
|
+
'quick.enable': 'Enable personalization',
|
|
568
|
+
'quick.disable': 'Disable personalization',
|
|
569
|
+
'quick.statusEnabled': 'Personalization enabled',
|
|
570
|
+
'quick.statusDisabled': 'Personalization disabled',
|
|
328
571
|
'group.aboutYou': 'About you',
|
|
329
572
|
'field.nickname': 'Nickname',
|
|
330
573
|
'field.nicknamePlaceholder': 'Your nickname — the agent will address you by it',
|
|
@@ -344,6 +587,13 @@ window.__ModuleLoader__.load({
|
|
|
344
587
|
'field.instructions': 'Custom instructions',
|
|
345
588
|
'hint.instructions': 'Extra persona or habit requirements. Avoid repeating wording similar to the style & tone option above to prevent conflicts.',
|
|
346
589
|
'field.instructionsPlaceholder': 'Custom instructions — other behaviors, style or tone preferences',
|
|
590
|
+
'group.personas': 'Personas',
|
|
591
|
+
'personas.save': 'Save current as persona',
|
|
592
|
+
'personas.savePlaceholder': 'Persona name (1-30 chars)',
|
|
593
|
+
'personas.use': 'Use',
|
|
594
|
+
'personas.delete': 'Delete',
|
|
595
|
+
'personas.confirmDelete': 'Delete persona "{name}"?',
|
|
596
|
+
'personas.empty': 'No personas yet — save the current config to switch with one click',
|
|
347
597
|
'style.professional': 'Professional',
|
|
348
598
|
'style.casual': 'Casual',
|
|
349
599
|
'style.humorous': 'Humorous',
|
|
@@ -355,6 +605,9 @@ window.__ModuleLoader__.load({
|
|
|
355
605
|
'trait.emoji.default': 'Default',
|
|
356
606
|
'trait.emoji.more': 'More (frequent emoji usage)',
|
|
357
607
|
'trait.emoji.less': 'Less (minimal emoji usage)',
|
|
608
|
+
'group.tool': 'Agent tools',
|
|
609
|
+
'field.toolConfirm': 'Require confirmation for persona changes',
|
|
610
|
+
'hint.toolConfirm': 'When enabled, changes made by the agent through the set_persona tool do not apply immediately — confirm with /soul confirm or reject with /soul reject in the conversation.',
|
|
358
611
|
'button.save': 'Save Settings',
|
|
359
612
|
'button.saving': 'Saving...',
|
|
360
613
|
'button.reset': 'Reset to Default',
|
|
@@ -363,11 +616,34 @@ window.__ModuleLoader__.load({
|
|
|
363
616
|
'toast.saved': '✅ Settings saved',
|
|
364
617
|
'toast.noChanges': '✅ No changes to save',
|
|
365
618
|
'toast.reset': '✅ Reset to defaults',
|
|
619
|
+
'toast.personaSaved': '✅ Persona saved',
|
|
620
|
+
'toast.personaUsed': '✅ Persona applied',
|
|
621
|
+
'toast.personaUnchanged': 'ℹ Persona matches current config',
|
|
622
|
+
'toast.personaDeleted': '✅ Persona deleted',
|
|
366
623
|
'prompt.view': 'View the active system prompt',
|
|
367
624
|
'prompt.hide': 'Hide the prompt',
|
|
625
|
+
'prompt.title': 'Active system prompt',
|
|
626
|
+
'prompt.summaryEnabled': 'Enabled',
|
|
627
|
+
'prompt.summaryDisabled': 'Disabled',
|
|
628
|
+
'prompt.summaryChars': '{n} chars',
|
|
368
629
|
'prompt.loading': 'Loading prompt...',
|
|
369
630
|
'prompt.empty': '(personalization disabled — the prompt is empty)',
|
|
370
|
-
'prompt.chars': 'Current character count: {n}'
|
|
631
|
+
'prompt.chars': 'Current character count: {n}',
|
|
632
|
+
'toast.saveFailed': '❌ Save failed — please check the config',
|
|
633
|
+
'trail.title': 'Composer light trail',
|
|
634
|
+
'trail.enable': 'Enable composer light trail',
|
|
635
|
+
'trail.color': 'Trail color',
|
|
636
|
+
'trail.speed': 'Flow speed',
|
|
637
|
+
'trail.speed.slow': 'Slow',
|
|
638
|
+
'trail.speed.normal': 'Normal',
|
|
639
|
+
'trail.speed.fast': 'Fast',
|
|
640
|
+
'trail.width': 'Trail thickness',
|
|
641
|
+
'trail.width.thin': 'Thin',
|
|
642
|
+
'trail.width.normal': 'Normal',
|
|
643
|
+
'trail.width.thick': 'Thick',
|
|
644
|
+
'trail.preview': 'Preview',
|
|
645
|
+
'trail.previewText': 'While the agent is replying, a light trail runs along the composer border',
|
|
646
|
+
'hint.trail': 'While the agent is replying, a light trail runs along the composer border. Pick a swatch, use the color picker, or type a hex value (e.g. #679EFE).'
|
|
371
647
|
}
|
|
372
648
|
|
|
373
649
|
// 图标替换匹配的导航文案(词典中 nav.label 的全部语言取值)
|
|
@@ -401,7 +677,7 @@ window.__ModuleLoader__.load({
|
|
|
401
677
|
]
|
|
402
678
|
|
|
403
679
|
// 表单字段与 store 字段的一一对应(dirty 检测与保存载荷共用)
|
|
404
|
-
const FIELD_KEYS = ['enabled', 'nickname', 'occupation', 'bio', 'style', 'headingLists', 'emoji', 'language', 'customInstructions']
|
|
680
|
+
const FIELD_KEYS = ['enabled', 'nickname', 'occupation', 'bio', 'style', 'headingLists', 'emoji', 'language', 'customInstructions', 'requireToolConfirmation', 'trailEnabled', 'trailColor', 'trailSpeed', 'trailWidth']
|
|
405
681
|
|
|
406
682
|
// 提示词小图标:hover 展示说明文字
|
|
407
683
|
function SoulHint(props) {
|
|
@@ -425,12 +701,59 @@ window.__ModuleLoader__.load({
|
|
|
425
701
|
)
|
|
426
702
|
}
|
|
427
703
|
|
|
704
|
+
// 可折叠设置面板:同一时间只展开一个区域,并持久化用户最后的选择。
|
|
705
|
+
function SoulAccordion(props) {
|
|
706
|
+
const { id, title, summary, open, onToggle, children } = props
|
|
707
|
+
return e('section', { className: `soul-accordion${open ? ' soul-accordion-open' : ''}` },
|
|
708
|
+
e('button', {
|
|
709
|
+
type: 'button',
|
|
710
|
+
className: 'soul-accordion-trigger',
|
|
711
|
+
'aria-expanded': open,
|
|
712
|
+
'aria-controls': `${id}-body`,
|
|
713
|
+
onClick: onToggle
|
|
714
|
+
},
|
|
715
|
+
e('span', { className: 'soul-accordion-title' }, title),
|
|
716
|
+
e('span', { className: 'soul-accordion-summary' }, summary),
|
|
717
|
+
e('span', { className: 'soul-accordion-icon', 'aria-hidden': true }, open ? '−' : '+')
|
|
718
|
+
),
|
|
719
|
+
open && e('div', { id: `${id}-body`, className: 'soul-accordion-body' }, children)
|
|
720
|
+
)
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// 输入框工具栏快捷开关:复用全局 soulController,和设置页保持同一份状态。
|
|
724
|
+
function SoulQuickToggle(props) {
|
|
725
|
+
const { useSoulController, controller } = props
|
|
726
|
+
const t = typeof props.t === 'function' ? props.t : FALLBACK_T
|
|
727
|
+
const state = useSoulController((state) => state)
|
|
728
|
+
const enabled = state.enabled === true
|
|
729
|
+
const busy = state.saving === true
|
|
730
|
+
|
|
731
|
+
const toggle = async () => {
|
|
732
|
+
if (busy) return
|
|
733
|
+
await controller.saveConfig({ enabled: !enabled })
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return e('button', {
|
|
737
|
+
type: 'button',
|
|
738
|
+
className: 'soul-quick-toggle',
|
|
739
|
+
'data-enabled': enabled ? 'true' : 'false',
|
|
740
|
+
'aria-pressed': enabled,
|
|
741
|
+
'aria-label': enabled ? t('quick.disable') : t('quick.enable'),
|
|
742
|
+
title: enabled ? t('quick.disable') : t('quick.enable'),
|
|
743
|
+
disabled: busy,
|
|
744
|
+
onClick: toggle
|
|
745
|
+
},
|
|
746
|
+
e('span', { className: 'soul-quick-toggle-dot', 'aria-hidden': true }),
|
|
747
|
+
enabled ? t('quick.statusEnabled') : t('quick.statusDisabled')
|
|
748
|
+
)
|
|
749
|
+
}
|
|
750
|
+
|
|
428
751
|
function SoulSettings(props) {
|
|
429
752
|
const { useSoulController, controller } = props
|
|
430
753
|
// 渲染器按槽位 locale 命名空间注入 t(随语言切换更新);缺失时回退中文
|
|
431
754
|
const t = typeof props.t === 'function' ? props.t : FALLBACK_T
|
|
432
755
|
const state = useSoulController((state) => state)
|
|
433
|
-
const { enabled, nickname, occupation, bio, style, headingLists, emoji, language, customInstructions, loading, saving, error
|
|
756
|
+
const { enabled, nickname, occupation, bio, style, headingLists, emoji, language, customInstructions, requireToolConfirmation, trailEnabled, trailColor, trailSpeed, trailWidth, personas, activePersona, loading, saving, error } = state
|
|
434
757
|
|
|
435
758
|
const [localEnabled, setLocalEnabled] = React.useState(enabled)
|
|
436
759
|
const [localNickname, setLocalNickname] = React.useState(nickname || '')
|
|
@@ -441,12 +764,33 @@ window.__ModuleLoader__.load({
|
|
|
441
764
|
const [localEmoji, setLocalEmoji] = React.useState(emoji)
|
|
442
765
|
const [localLanguage, setLocalLanguage] = React.useState(language || 'zh')
|
|
443
766
|
const [localInstructions, setLocalInstructions] = React.useState(customInstructions)
|
|
444
|
-
const [
|
|
445
|
-
const [
|
|
767
|
+
const [localToolConfirm, setLocalToolConfirm] = React.useState(requireToolConfirmation === true)
|
|
768
|
+
const [localTrailEnabled, setLocalTrailEnabled] = React.useState(trailEnabled === true)
|
|
769
|
+
const [localTrailColor, setLocalTrailColor] = React.useState(trailColor || TRAIL_COLOR_FALLBACK)
|
|
770
|
+
const [localTrailSpeed, setLocalTrailSpeed] = React.useState(trailSpeed || 'slow')
|
|
771
|
+
const [localTrailWidth, setLocalTrailWidth] = React.useState(trailWidth || 'thin')
|
|
772
|
+
const [personaName, setPersonaName] = React.useState('')
|
|
773
|
+
const [toast, setToast] = React.useState(null)
|
|
446
774
|
const [showPrompt, setShowPrompt] = React.useState(false)
|
|
447
775
|
const [promptText, setPromptText] = React.useState('')
|
|
448
776
|
const [promptLoading, setPromptLoading] = React.useState(false)
|
|
449
777
|
const [promptError, setPromptError] = React.useState(null)
|
|
778
|
+
const [openSection, setOpenSection] = React.useState(() => {
|
|
779
|
+
try {
|
|
780
|
+
return globalThis.localStorage?.getItem('dsh-soul:open-section') || 'about'
|
|
781
|
+
} catch {
|
|
782
|
+
return 'about'
|
|
783
|
+
}
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
React.useEffect(() => {
|
|
787
|
+
try {
|
|
788
|
+
if (openSection) globalThis.localStorage?.setItem('dsh-soul:open-section', openSection)
|
|
789
|
+
else globalThis.localStorage?.removeItem('dsh-soul:open-section')
|
|
790
|
+
} catch {
|
|
791
|
+
// localStorage 不可用时不影响设置页使用
|
|
792
|
+
}
|
|
793
|
+
}, [openSection])
|
|
450
794
|
|
|
451
795
|
// 同步状态
|
|
452
796
|
React.useEffect(() => {
|
|
@@ -459,27 +803,24 @@ window.__ModuleLoader__.load({
|
|
|
459
803
|
setLocalEmoji(emoji)
|
|
460
804
|
setLocalLanguage(language || 'zh')
|
|
461
805
|
setLocalInstructions(customInstructions)
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
806
|
+
setLocalToolConfirm(requireToolConfirmation === true)
|
|
807
|
+
setLocalTrailEnabled(trailEnabled === true)
|
|
808
|
+
setLocalTrailColor(trailColor || TRAIL_COLOR_FALLBACK)
|
|
809
|
+
setLocalTrailSpeed(trailSpeed || 'slow')
|
|
810
|
+
setLocalTrailWidth(trailWidth || 'thin')
|
|
811
|
+
}, [enabled, nickname, occupation, bio, style, headingLists, emoji, language, customInstructions, requireToolConfirmation, trailEnabled, trailColor, trailSpeed, trailWidth])
|
|
812
|
+
|
|
813
|
+
// toast 自动消失
|
|
465
814
|
React.useEffect(() => {
|
|
466
|
-
if (
|
|
467
|
-
const timer = setTimeout(() =>
|
|
815
|
+
if (toast) {
|
|
816
|
+
const timer = setTimeout(() => setToast(null), 2000)
|
|
468
817
|
return () => clearTimeout(timer)
|
|
469
818
|
}
|
|
470
|
-
}, [
|
|
471
|
-
|
|
472
|
-
// 重置成功后显示提示,2秒后自动消失
|
|
473
|
-
React.useEffect(() => {
|
|
474
|
-
if (showResetSuccess) {
|
|
475
|
-
const timer = setTimeout(() => setShowResetSuccess(false), 2000)
|
|
476
|
-
return () => clearTimeout(timer)
|
|
477
|
-
}
|
|
478
|
-
}, [showResetSuccess])
|
|
819
|
+
}, [toast])
|
|
479
820
|
|
|
480
821
|
// dirty 检测:本地表单与已保存配置逐字段比较
|
|
481
|
-
const savedMap = { enabled, nickname, occupation, bio, style, headingLists, emoji, language, customInstructions }
|
|
482
|
-
const localMap = { enabled: localEnabled, nickname: localNickname, occupation: localOccupation, bio: localBio, style: localStyle, headingLists: localHeadingLists, emoji: localEmoji, language: localLanguage, customInstructions: localInstructions }
|
|
822
|
+
const savedMap = { enabled, nickname, occupation, bio, style, headingLists, emoji, language, customInstructions, requireToolConfirmation, trailEnabled, trailColor, trailSpeed, trailWidth }
|
|
823
|
+
const localMap = { enabled: localEnabled, nickname: localNickname, occupation: localOccupation, bio: localBio, style: localStyle, headingLists: localHeadingLists, emoji: localEmoji, language: localLanguage, customInstructions: localInstructions, requireToolConfirmation: localToolConfirm, trailEnabled: localTrailEnabled, trailColor: localTrailColor, trailSpeed: localTrailSpeed, trailWidth: localTrailWidth }
|
|
483
824
|
const dirty = FIELD_KEYS.some((key) => savedMap[key] !== localMap[key])
|
|
484
825
|
|
|
485
826
|
const loadPrompt = async () => {
|
|
@@ -501,25 +842,124 @@ window.__ModuleLoader__.load({
|
|
|
501
842
|
if (next) await loadPrompt()
|
|
502
843
|
}
|
|
503
844
|
|
|
845
|
+
const loadPersonas = async () => {
|
|
846
|
+
try {
|
|
847
|
+
const payload = await controller.fetchPersonas()
|
|
848
|
+
controller.store.update((s) => {
|
|
849
|
+
s.personas = payload.personas
|
|
850
|
+
s.activePersona = payload.activeName
|
|
851
|
+
})
|
|
852
|
+
} catch {
|
|
853
|
+
// 预设列表加载失败不打断主配置界面
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// 设置页打开后重新读取宿主配置,补齐 /soul 命令等外部写入路径。
|
|
858
|
+
// 表单 dirty 或正在保存时暂停,避免远端刷新覆盖用户未保存的输入。
|
|
859
|
+
React.useEffect(() => {
|
|
860
|
+
const refresh = () => {
|
|
861
|
+
if (dirty || saving) return
|
|
862
|
+
void controller.loadConfig()
|
|
863
|
+
void loadPersonas()
|
|
864
|
+
}
|
|
865
|
+
const onVisibilityChange = () => {
|
|
866
|
+
if (document.visibilityState === 'visible') refresh()
|
|
867
|
+
}
|
|
868
|
+
refresh()
|
|
869
|
+
window.addEventListener('focus', refresh)
|
|
870
|
+
document.addEventListener('visibilitychange', onVisibilityChange)
|
|
871
|
+
const timer = setInterval(refresh, 2000)
|
|
872
|
+
return () => {
|
|
873
|
+
window.removeEventListener('focus', refresh)
|
|
874
|
+
document.removeEventListener('visibilitychange', onVisibilityChange)
|
|
875
|
+
clearInterval(timer)
|
|
876
|
+
}
|
|
877
|
+
}, [controller, dirty, saving])
|
|
878
|
+
|
|
504
879
|
const handleSave = async () => {
|
|
505
|
-
await controller.saveConfig(localMap)
|
|
506
|
-
|
|
880
|
+
const payload = await controller.saveConfig(localMap)
|
|
881
|
+
// saveConfig 失败时内部已记录 error 并返回 undefined:给出失败提示,而非误报「无变化」
|
|
882
|
+
if (!payload) {
|
|
883
|
+
setToast({ text: t('toast.saveFailed'), kind: 'error' })
|
|
884
|
+
return
|
|
885
|
+
}
|
|
886
|
+
setToast({
|
|
887
|
+
text: Array.isArray(payload.changed) && payload.changed.length > 0 ? t('toast.saved') : t('toast.noChanges'),
|
|
888
|
+
kind: 'success'
|
|
889
|
+
})
|
|
507
890
|
if (showPrompt) await loadPrompt()
|
|
508
891
|
}
|
|
509
892
|
|
|
510
893
|
const handleReset = async () => {
|
|
511
894
|
await controller.resetConfig()
|
|
512
|
-
|
|
895
|
+
setToast({ text: t('toast.reset'), kind: 'success' })
|
|
513
896
|
if (showPrompt) await loadPrompt()
|
|
514
897
|
}
|
|
515
898
|
|
|
899
|
+
const handleSavePersona = async () => {
|
|
900
|
+
const name = personaName.trim()
|
|
901
|
+
if (!name) return
|
|
902
|
+
try {
|
|
903
|
+
await controller.savePersona(name)
|
|
904
|
+
setPersonaName('')
|
|
905
|
+
await loadPersonas()
|
|
906
|
+
setToast({ text: t('toast.personaSaved'), kind: 'success' })
|
|
907
|
+
} catch (err) {
|
|
908
|
+
setToast({ text: messageOf(err), kind: 'error' })
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const handleUsePersona = async (name) => {
|
|
913
|
+
try {
|
|
914
|
+
const payload = await controller.usePersona(name)
|
|
915
|
+
await loadPersonas()
|
|
916
|
+
setToast({ text: payload && payload.unchanged ? t('toast.personaUnchanged') : t('toast.personaUsed'), kind: 'success' })
|
|
917
|
+
if (showPrompt) await loadPrompt()
|
|
918
|
+
} catch (err) {
|
|
919
|
+
setToast({ text: messageOf(err), kind: 'error' })
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const handleDeletePersona = async (name) => {
|
|
924
|
+
if (typeof globalThis.confirm === 'function' && !globalThis.confirm(t('personas.confirmDelete', { name }))) return
|
|
925
|
+
try {
|
|
926
|
+
await controller.deletePersona(name)
|
|
927
|
+
await loadPersonas()
|
|
928
|
+
setToast({ text: t('toast.personaDeleted'), kind: 'success' })
|
|
929
|
+
} catch (err) {
|
|
930
|
+
setToast({ text: messageOf(err), kind: 'error' })
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
516
934
|
const styleOptions = STYLE_VALUES.map((value) => ({ value, label: t(`style.${value}`) }))
|
|
517
935
|
const headingListsOptions = TRAIT_VALUES.map((value) => ({ value, label: t(`trait.headingLists.${value}`) }))
|
|
518
936
|
const emojiOptions = TRAIT_VALUES.map((value) => ({ value, label: t(`trait.emoji.${value}`) }))
|
|
519
|
-
const
|
|
937
|
+
const personaNames = personas ? Object.keys(personas).sort() : null
|
|
938
|
+
const aboutFilled = [localNickname, localOccupation, localBio].filter(Boolean).length
|
|
939
|
+
const aboutSummary = aboutFilled > 0 ? `${aboutFilled}/3 ${t('accordion.filled')}` : t('accordion.empty')
|
|
940
|
+
const traitsSummary = t(`style.${localStyle}`)
|
|
941
|
+
const personasSummary = personas === null ? t('status.loading') : `${Object.keys(personas).length} ${t('accordion.personas')}`
|
|
942
|
+
const toolSummary = localToolConfirm ? t('accordion.on') : t('accordion.off')
|
|
943
|
+
const promptSummary = promptText.length > 0
|
|
944
|
+
? t('prompt.summaryChars', { n: String(promptText.length) })
|
|
945
|
+
: (enabled ? t('prompt.summaryEnabled') : t('prompt.summaryDisabled'))
|
|
946
|
+
const toggleSection = (section) => () => {
|
|
947
|
+
setOpenSection(current => current === section ? null : section)
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const personaRowMeta = (entry) => {
|
|
951
|
+
const parts = []
|
|
952
|
+
if (entry && STYLE_VALUES.includes(entry.style)) parts.push(t(`style.${entry.style}`))
|
|
953
|
+
else if (entry && entry.style) parts.push(entry.style)
|
|
954
|
+
if (entry && entry.nickname) parts.push(entry.nickname)
|
|
955
|
+
return parts.join(' · ')
|
|
956
|
+
}
|
|
520
957
|
|
|
521
958
|
return e('div', { className: 'soul-section' },
|
|
522
|
-
e('
|
|
959
|
+
e('div', { className: 'soul-title-row' },
|
|
960
|
+
e('h3', null, t('settings.title')),
|
|
961
|
+
e('span', { className: 'soul-version' }, `v${VERSION}`)
|
|
962
|
+
),
|
|
523
963
|
e('div', { className: 'soul-toggle' },
|
|
524
964
|
e('input', {
|
|
525
965
|
type: 'checkbox',
|
|
@@ -531,117 +971,272 @@ window.__ModuleLoader__.load({
|
|
|
531
971
|
),
|
|
532
972
|
|
|
533
973
|
localEnabled && e(Fragment, null,
|
|
534
|
-
e(
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
value: localOccupation,
|
|
551
|
-
onChange: (ev) => setLocalOccupation(ev.target.value),
|
|
552
|
-
placeholder: t('field.occupationPlaceholder')
|
|
553
|
-
})
|
|
554
|
-
),
|
|
555
|
-
e('div', { className: 'soul-field' },
|
|
556
|
-
e('label', { htmlFor: 'soul-bio' }, t('field.bio')),
|
|
557
|
-
e('textarea', {
|
|
558
|
-
id: 'soul-bio',
|
|
559
|
-
className: 'soul-textarea-sm',
|
|
560
|
-
value: localBio,
|
|
561
|
-
onChange: (ev) => setLocalBio(ev.target.value),
|
|
562
|
-
placeholder: t('field.bioPlaceholder')
|
|
563
|
-
})
|
|
564
|
-
),
|
|
565
|
-
|
|
566
|
-
e('div', { className: 'soul-group-title' }, t('group.traits')),
|
|
567
|
-
e('div', { className: 'soul-field' },
|
|
568
|
-
e('label', { htmlFor: 'soul-style' },
|
|
569
|
-
t('field.style'),
|
|
570
|
-
e(SoulHint, { text: t('hint.style') })
|
|
974
|
+
e(SoulAccordion, {
|
|
975
|
+
id: 'soul-about',
|
|
976
|
+
title: t('group.aboutYou'),
|
|
977
|
+
summary: aboutSummary,
|
|
978
|
+
open: openSection === 'about',
|
|
979
|
+
onToggle: toggleSection('about')
|
|
980
|
+
},
|
|
981
|
+
e('div', { className: 'soul-field' },
|
|
982
|
+
e('label', { htmlFor: 'soul-nickname' }, t('field.nickname')),
|
|
983
|
+
e('input', {
|
|
984
|
+
type: 'text',
|
|
985
|
+
id: 'soul-nickname',
|
|
986
|
+
value: localNickname,
|
|
987
|
+
onChange: (ev) => setLocalNickname(ev.target.value),
|
|
988
|
+
placeholder: t('field.nicknamePlaceholder')
|
|
989
|
+
})
|
|
571
990
|
),
|
|
572
|
-
e('
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
991
|
+
e('div', { className: 'soul-field' },
|
|
992
|
+
e('label', { htmlFor: 'soul-occupation' }, t('field.occupation')),
|
|
993
|
+
e('input', {
|
|
994
|
+
type: 'text',
|
|
995
|
+
id: 'soul-occupation',
|
|
996
|
+
value: localOccupation,
|
|
997
|
+
onChange: (ev) => setLocalOccupation(ev.target.value),
|
|
998
|
+
placeholder: t('field.occupationPlaceholder')
|
|
999
|
+
})
|
|
1000
|
+
),
|
|
1001
|
+
e('div', { className: 'soul-field' },
|
|
1002
|
+
e('label', { htmlFor: 'soul-bio' }, t('field.bio')),
|
|
1003
|
+
e('textarea', {
|
|
1004
|
+
id: 'soul-bio',
|
|
1005
|
+
className: 'soul-textarea-sm',
|
|
1006
|
+
value: localBio,
|
|
1007
|
+
onChange: (ev) => setLocalBio(ev.target.value),
|
|
1008
|
+
placeholder: t('field.bioPlaceholder')
|
|
1009
|
+
})
|
|
580
1010
|
)
|
|
581
1011
|
),
|
|
582
1012
|
|
|
583
|
-
e(
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
1013
|
+
e(SoulAccordion, {
|
|
1014
|
+
id: 'soul-traits',
|
|
1015
|
+
title: t('group.traits'),
|
|
1016
|
+
summary: traitsSummary,
|
|
1017
|
+
open: openSection === 'traits',
|
|
1018
|
+
onToggle: toggleSection('traits')
|
|
1019
|
+
},
|
|
1020
|
+
e('div', { className: 'soul-field' },
|
|
1021
|
+
e('label', { htmlFor: 'soul-style' },
|
|
1022
|
+
t('field.style'),
|
|
1023
|
+
e(SoulHint, { text: t('hint.style') })
|
|
1024
|
+
),
|
|
1025
|
+
e('select', {
|
|
1026
|
+
id: 'soul-style',
|
|
1027
|
+
value: localStyle,
|
|
1028
|
+
onChange: (ev) => setLocalStyle(ev.target.value)
|
|
1029
|
+
},
|
|
1030
|
+
...styleOptions.map(opt => e('option', { key: opt.value, value: opt.value }, opt.label))
|
|
1031
|
+
)
|
|
587
1032
|
),
|
|
588
|
-
e('
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
1033
|
+
e('div', { className: 'soul-field' },
|
|
1034
|
+
e('label', { htmlFor: 'soul-headingLists' },
|
|
1035
|
+
t('field.headingLists'),
|
|
1036
|
+
e(SoulHint, { text: t('hint.headingLists') })
|
|
1037
|
+
),
|
|
1038
|
+
e('select', {
|
|
1039
|
+
id: 'soul-headingLists',
|
|
1040
|
+
value: localHeadingLists,
|
|
1041
|
+
onChange: (ev) => setLocalHeadingLists(ev.target.value)
|
|
1042
|
+
},
|
|
1043
|
+
...headingListsOptions.map(opt => e('option', { key: opt.value, value: opt.value }, opt.label))
|
|
1044
|
+
)
|
|
1045
|
+
),
|
|
1046
|
+
e('div', { className: 'soul-field' },
|
|
1047
|
+
e('label', { htmlFor: 'soul-emoji' },
|
|
1048
|
+
t('field.emoji'),
|
|
1049
|
+
e(SoulHint, { text: t('hint.emoji') })
|
|
1050
|
+
),
|
|
1051
|
+
e('select', {
|
|
1052
|
+
id: 'soul-emoji',
|
|
1053
|
+
value: localEmoji,
|
|
1054
|
+
onChange: (ev) => setLocalEmoji(ev.target.value)
|
|
1055
|
+
},
|
|
1056
|
+
...emojiOptions.map(opt => e('option', { key: opt.value, value: opt.value }, opt.label))
|
|
595
1057
|
)
|
|
596
|
-
)
|
|
597
|
-
),
|
|
598
|
-
|
|
599
|
-
e('div', { className: 'soul-field' },
|
|
600
|
-
e('label', { htmlFor: 'soul-emoji' },
|
|
601
|
-
t('field.emoji'),
|
|
602
|
-
e(SoulHint, { text: t('hint.emoji') })
|
|
603
1058
|
),
|
|
604
|
-
e('
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
1059
|
+
e('div', { className: 'soul-field' },
|
|
1060
|
+
e('label', { htmlFor: 'soul-language' },
|
|
1061
|
+
t('field.language'),
|
|
1062
|
+
e(SoulHint, { text: t('hint.language') })
|
|
1063
|
+
),
|
|
1064
|
+
e('select', {
|
|
1065
|
+
id: 'soul-language',
|
|
1066
|
+
value: localLanguage,
|
|
1067
|
+
onChange: (ev) => setLocalLanguage(ev.target.value)
|
|
1068
|
+
},
|
|
1069
|
+
...LANGUAGE_OPTIONS.map(opt => e('option', { key: opt.value, value: opt.value }, opt.label))
|
|
611
1070
|
)
|
|
1071
|
+
),
|
|
1072
|
+
e('div', { className: 'soul-field' },
|
|
1073
|
+
e('label', { htmlFor: 'soul-instructions' },
|
|
1074
|
+
t('field.instructions'),
|
|
1075
|
+
e(SoulHint, { text: t('hint.instructions') })
|
|
1076
|
+
),
|
|
1077
|
+
e('textarea', {
|
|
1078
|
+
id: 'soul-instructions',
|
|
1079
|
+
value: localInstructions,
|
|
1080
|
+
onChange: (ev) => setLocalInstructions(ev.target.value),
|
|
1081
|
+
placeholder: t('field.instructionsPlaceholder')
|
|
1082
|
+
})
|
|
612
1083
|
)
|
|
613
1084
|
),
|
|
614
1085
|
|
|
615
|
-
e(
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
1086
|
+
e(SoulAccordion, {
|
|
1087
|
+
id: 'soul-personas',
|
|
1088
|
+
title: t('group.personas'),
|
|
1089
|
+
summary: personasSummary,
|
|
1090
|
+
open: openSection === 'personas',
|
|
1091
|
+
onToggle: toggleSection('personas')
|
|
1092
|
+
},
|
|
1093
|
+
e('div', { className: 'soul-field' },
|
|
1094
|
+
personaNames === null
|
|
1095
|
+
? e('div', { className: 'soul-status' }, t('status.loading'))
|
|
1096
|
+
: (personaNames.length === 0
|
|
1097
|
+
? e('div', { className: 'soul-status' }, t('personas.empty'))
|
|
1098
|
+
: e(Fragment, null,
|
|
1099
|
+
...personaNames.map((name) => {
|
|
1100
|
+
const entry = personas[name] || {}
|
|
1101
|
+
return e('div', { className: 'soul-persona-row', key: name },
|
|
1102
|
+
e('span', { className: 'soul-persona-name' }, name === activePersona ? `★ ${name}` : name),
|
|
1103
|
+
e('span', { className: 'soul-persona-meta' }, personaRowMeta(entry)),
|
|
1104
|
+
e('span', { className: 'soul-persona-actions' },
|
|
1105
|
+
e('button', { type: 'button', className: 'soul-prompt-link', onClick: () => handleUsePersona(name) }, t('personas.use')),
|
|
1106
|
+
e('button', { type: 'button', className: 'soul-prompt-link soul-persona-danger', onClick: () => handleDeletePersona(name) }, t('personas.delete'))
|
|
1107
|
+
)
|
|
1108
|
+
)
|
|
1109
|
+
})
|
|
1110
|
+
))
|
|
619
1111
|
),
|
|
620
|
-
e('
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
)
|
|
1112
|
+
e('div', { className: 'soul-field soul-persona-save' },
|
|
1113
|
+
e('input', {
|
|
1114
|
+
type: 'text',
|
|
1115
|
+
value: personaName,
|
|
1116
|
+
maxLength: 30,
|
|
1117
|
+
onChange: (ev) => setPersonaName(ev.target.value),
|
|
1118
|
+
placeholder: t('personas.savePlaceholder')
|
|
1119
|
+
}),
|
|
1120
|
+
e(ui.Button, {
|
|
1121
|
+
onClick: handleSavePersona,
|
|
1122
|
+
disabled: saving || !personaName.trim()
|
|
1123
|
+
}, t('personas.save'))
|
|
628
1124
|
)
|
|
629
1125
|
),
|
|
630
1126
|
|
|
631
|
-
e(
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
1127
|
+
e(SoulAccordion, {
|
|
1128
|
+
id: 'soul-tools',
|
|
1129
|
+
title: t('group.tool'),
|
|
1130
|
+
summary: toolSummary,
|
|
1131
|
+
open: openSection === 'tools',
|
|
1132
|
+
onToggle: toggleSection('tools')
|
|
1133
|
+
},
|
|
1134
|
+
e('div', { className: 'soul-toggle' },
|
|
1135
|
+
e('input', {
|
|
1136
|
+
type: 'checkbox',
|
|
1137
|
+
id: 'soul-toolConfirm',
|
|
1138
|
+
checked: localToolConfirm,
|
|
1139
|
+
onChange: (ev) => setLocalToolConfirm(ev.target.checked)
|
|
1140
|
+
}),
|
|
1141
|
+
e('label', { htmlFor: 'soul-toolConfirm' }, t('field.toolConfirm')),
|
|
1142
|
+
e(SoulHint, { text: t('hint.toolConfirm') })
|
|
1143
|
+
),
|
|
1144
|
+
|
|
1145
|
+
e('div', { className: 'soul-group-title' }, t('trail.title')),
|
|
1146
|
+
e('div', { className: 'soul-toggle' },
|
|
1147
|
+
e('input', {
|
|
1148
|
+
type: 'checkbox',
|
|
1149
|
+
id: 'soul-trailEnabled',
|
|
1150
|
+
checked: localTrailEnabled,
|
|
1151
|
+
onChange: (ev) => setLocalTrailEnabled(ev.target.checked)
|
|
1152
|
+
}),
|
|
1153
|
+
e('label', { htmlFor: 'soul-trailEnabled' }, t('trail.enable')),
|
|
1154
|
+
e(SoulHint, { text: t('hint.trail') })
|
|
635
1155
|
),
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
1156
|
+
|
|
1157
|
+
localTrailEnabled && e(Fragment, null,
|
|
1158
|
+
e('div', { className: 'soul-field' },
|
|
1159
|
+
e('label', { htmlFor: 'soul-trailColor' }, t('trail.color')),
|
|
1160
|
+
e('div', { className: 'soul-trail-colors' },
|
|
1161
|
+
...TRAIL_PRESET_COLORS.map((value) => e('button', {
|
|
1162
|
+
key: value,
|
|
1163
|
+
type: 'button',
|
|
1164
|
+
className: 'soul-trail-swatch',
|
|
1165
|
+
'data-active': safeTrailColor(localTrailColor) === value ? 'true' : 'false',
|
|
1166
|
+
style: { background: value },
|
|
1167
|
+
'aria-label': value,
|
|
1168
|
+
title: value,
|
|
1169
|
+
onClick: () => setLocalTrailColor(value)
|
|
1170
|
+
})),
|
|
1171
|
+
e('input', {
|
|
1172
|
+
type: 'color',
|
|
1173
|
+
id: 'soul-trailColor',
|
|
1174
|
+
className: 'soul-trail-picker',
|
|
1175
|
+
value: safeTrailColor(localTrailColor),
|
|
1176
|
+
onChange: (ev) => setLocalTrailColor(safeTrailColor(ev.target.value))
|
|
1177
|
+
}),
|
|
1178
|
+
e('input', {
|
|
1179
|
+
type: 'text',
|
|
1180
|
+
className: 'soul-trail-hex',
|
|
1181
|
+
value: localTrailColor,
|
|
1182
|
+
maxLength: 7,
|
|
1183
|
+
spellCheck: false,
|
|
1184
|
+
onChange: (ev) => setLocalTrailColor(ev.target.value),
|
|
1185
|
+
placeholder: TRAIL_COLOR_FALLBACK
|
|
1186
|
+
})
|
|
1187
|
+
)
|
|
1188
|
+
),
|
|
1189
|
+
e('div', { className: 'soul-field' },
|
|
1190
|
+
e('label', { htmlFor: 'soul-trailSpeed' }, t('trail.speed')),
|
|
1191
|
+
e('select', {
|
|
1192
|
+
id: 'soul-trailSpeed',
|
|
1193
|
+
value: localTrailSpeed,
|
|
1194
|
+
onChange: (ev) => setLocalTrailSpeed(ev.target.value)
|
|
1195
|
+
},
|
|
1196
|
+
...TRAIL_SPEED_VALUES.map((value) => e('option', { key: value, value }, t(`trail.speed.${value}`)))
|
|
1197
|
+
)
|
|
1198
|
+
),
|
|
1199
|
+
e('div', { className: 'soul-field' },
|
|
1200
|
+
e('label', { htmlFor: 'soul-trailWidth' }, t('trail.width')),
|
|
1201
|
+
e('select', {
|
|
1202
|
+
id: 'soul-trailWidth',
|
|
1203
|
+
value: localTrailWidth,
|
|
1204
|
+
onChange: (ev) => setLocalTrailWidth(ev.target.value)
|
|
1205
|
+
},
|
|
1206
|
+
...TRAIL_WIDTH_VALUES.map((value) => e('option', { key: value, value }, t(`trail.width.${value}`)))
|
|
1207
|
+
)
|
|
1208
|
+
),
|
|
1209
|
+
e('div', { className: 'soul-field' },
|
|
1210
|
+
e('label', null, t('trail.preview')),
|
|
1211
|
+
e(SoulTrailPreview, {
|
|
1212
|
+
enabled: localTrailEnabled,
|
|
1213
|
+
color: localTrailColor,
|
|
1214
|
+
speed: localTrailSpeed,
|
|
1215
|
+
width: localTrailWidth,
|
|
1216
|
+
t
|
|
1217
|
+
})
|
|
1218
|
+
)
|
|
1219
|
+
)
|
|
642
1220
|
)
|
|
643
1221
|
),
|
|
644
1222
|
|
|
1223
|
+
e(SoulAccordion, {
|
|
1224
|
+
id: 'soul-prompt',
|
|
1225
|
+
title: t('prompt.title'),
|
|
1226
|
+
summary: promptSummary,
|
|
1227
|
+
open: showPrompt,
|
|
1228
|
+
onToggle: togglePrompt
|
|
1229
|
+
},
|
|
1230
|
+
promptLoading
|
|
1231
|
+
? e('div', { className: 'soul-status' }, t('prompt.loading'))
|
|
1232
|
+
: (promptError
|
|
1233
|
+
? e('div', { className: 'soul-error' }, promptError)
|
|
1234
|
+
: e(Fragment, null,
|
|
1235
|
+
e('pre', { className: 'soul-prompt-pre' }, promptText || t('prompt.empty')),
|
|
1236
|
+
e('div', { className: 'soul-status' }, t('prompt.chars', { n: String(promptText.length) }))
|
|
1237
|
+
))
|
|
1238
|
+
),
|
|
1239
|
+
|
|
645
1240
|
error && e('div', { className: 'soul-error' }, error),
|
|
646
1241
|
|
|
647
1242
|
e('div', { className: 'soul-buttons' },
|
|
@@ -658,42 +1253,95 @@ window.__ModuleLoader__.load({
|
|
|
658
1253
|
|
|
659
1254
|
dirty && !saving && e('div', { className: 'soul-status soul-dirty' }, t('status.unsaved')),
|
|
660
1255
|
|
|
661
|
-
e('div', {
|
|
662
|
-
|
|
663
|
-
type: 'button',
|
|
664
|
-
className: 'soul-prompt-link',
|
|
665
|
-
onClick: togglePrompt
|
|
666
|
-
}, showPrompt ? t('prompt.hide') : t('prompt.view'))
|
|
667
|
-
),
|
|
668
|
-
showPrompt && e('div', { className: 'soul-prompt-panel' },
|
|
669
|
-
promptLoading
|
|
670
|
-
? e('div', { className: 'soul-status' }, t('prompt.loading'))
|
|
671
|
-
: (promptError
|
|
672
|
-
? e('div', { className: 'soul-error' }, promptError)
|
|
673
|
-
: e(Fragment, null,
|
|
674
|
-
e('pre', { className: 'soul-prompt-pre' }, promptText || t('prompt.empty')),
|
|
675
|
-
e('div', { className: 'soul-status' }, t('prompt.chars', { n: String(promptText.length) }))
|
|
676
|
-
))
|
|
677
|
-
),
|
|
678
|
-
|
|
679
|
-
loading && e('div', { className: 'soul-status' }, t('status.loading')),
|
|
680
|
-
|
|
681
|
-
showSuccess && e('div', {
|
|
682
|
-
className: 'soul-toast soul-toast-success',
|
|
1256
|
+
toast && e('div', {
|
|
1257
|
+
className: `soul-toast ${toast.kind === 'error' ? 'soul-toast-error' : 'soul-toast-success'}`,
|
|
683
1258
|
style: {
|
|
684
1259
|
animation: 'fadeInOut 2s ease-in-out'
|
|
685
1260
|
}
|
|
686
|
-
},
|
|
1261
|
+
}, toast.text)
|
|
1262
|
+
)
|
|
1263
|
+
}
|
|
687
1264
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
1265
|
+
// -------------------------------------------------------------------------
|
|
1266
|
+
// 输入框光轨:组件
|
|
1267
|
+
// -------------------------------------------------------------------------
|
|
1268
|
+
|
|
1269
|
+
// 设置页内的实时示例:与线上光轨共用同一渲染层与 CSS,
|
|
1270
|
+
// 因此「示例所见」即「回复时所得」;颜色/速度/粗细随表单实时联动。
|
|
1271
|
+
function SoulTrailPreview(props) {
|
|
1272
|
+
const t = typeof props.t === 'function' ? props.t : FALLBACK_T
|
|
1273
|
+
const hostRef = React.useRef(null)
|
|
1274
|
+
|
|
1275
|
+
React.useEffect(() => {
|
|
1276
|
+
const host = hostRef.current
|
|
1277
|
+
if (!host) return undefined
|
|
1278
|
+
return mountTrailRing(host)
|
|
1279
|
+
}, [])
|
|
1280
|
+
|
|
1281
|
+
return e('div', {
|
|
1282
|
+
ref: hostRef,
|
|
1283
|
+
className: 'soul-trail-preview',
|
|
1284
|
+
'data-soul-trail': props.enabled ? 'on' : 'off',
|
|
1285
|
+
'data-soul-trail-speed': props.speed || 'slow',
|
|
1286
|
+
'data-soul-trail-width': props.width || 'thin',
|
|
1287
|
+
style: { '--soul-trail-color': safeTrailColor(props.color) }
|
|
1288
|
+
},
|
|
1289
|
+
e('span', { className: 'soul-trail-preview-text' }, t('trail.previewText'))
|
|
694
1290
|
)
|
|
695
1291
|
}
|
|
696
1292
|
|
|
1293
|
+
// 输入框光轨运行时:注册在 conversation.input.overlay(输入框卡片内部)。
|
|
1294
|
+
// 组件本身不渲染可见内容——它在最近的卡片上挂一层 SVG 环,
|
|
1295
|
+
// 并用会话运行态控制显隐。运行状态来自 DSH 标准 prop useSession
|
|
1296
|
+
// (SessionSnapshot.running,等价于 Agent 状态为 running)。
|
|
1297
|
+
function SoulTrail(props) {
|
|
1298
|
+
const useSession = props.useSession
|
|
1299
|
+
const useStore = props.useSoulController
|
|
1300
|
+
const running = typeof useSession === 'function'
|
|
1301
|
+
? useSession((snapshot) => snapshot.running === true)
|
|
1302
|
+
: false
|
|
1303
|
+
const state = typeof useStore === 'function' ? useStore((snapshot) => snapshot) : null
|
|
1304
|
+
const anchorRef = React.useRef(null)
|
|
1305
|
+
const mountedRef = React.useRef(null)
|
|
1306
|
+
|
|
1307
|
+
React.useEffect(() => {
|
|
1308
|
+
const anchor = anchorRef.current
|
|
1309
|
+
// 组件位于卡片内的 overlay 锚点中,closest 定位可避免全局查询误命中
|
|
1310
|
+
const host = anchor && typeof anchor.closest === 'function'
|
|
1311
|
+
? anchor.closest('[data-composer-card]')
|
|
1312
|
+
: null
|
|
1313
|
+
if (!host) return undefined
|
|
1314
|
+
|
|
1315
|
+
mountedRef.current = { host, unmount: mountTrailRing(host) }
|
|
1316
|
+
// 先置为隐藏:效果在首帧绘制后执行,先写 off 可避免挂载瞬间闪出一帧光轨
|
|
1317
|
+
host.setAttribute('data-soul-trail', 'off')
|
|
1318
|
+
return () => {
|
|
1319
|
+
const mounted = mountedRef.current
|
|
1320
|
+
mountedRef.current = null
|
|
1321
|
+
if (!mounted) return
|
|
1322
|
+
mounted.unmount()
|
|
1323
|
+
mounted.host.removeAttribute('data-soul-trail')
|
|
1324
|
+
mounted.host.removeAttribute('data-soul-trail-speed')
|
|
1325
|
+
mounted.host.removeAttribute('data-soul-trail-width')
|
|
1326
|
+
mounted.host.style.removeProperty('--soul-trail-color')
|
|
1327
|
+
}
|
|
1328
|
+
}, [])
|
|
1329
|
+
|
|
1330
|
+
// 显隐与外观同步:仅「Agent 回复中 + 光轨开关开启」时显示
|
|
1331
|
+
React.useEffect(() => {
|
|
1332
|
+
const mounted = mountedRef.current
|
|
1333
|
+
if (!mounted) return
|
|
1334
|
+
const { host } = mounted
|
|
1335
|
+
const enabled = state !== null && state.trailEnabled === true
|
|
1336
|
+
host.setAttribute('data-soul-trail', running && enabled ? 'on' : 'off')
|
|
1337
|
+
host.setAttribute('data-soul-trail-speed', (state && state.trailSpeed) || 'slow')
|
|
1338
|
+
host.setAttribute('data-soul-trail-width', (state && state.trailWidth) || 'thin')
|
|
1339
|
+
host.style.setProperty('--soul-trail-color', safeTrailColor(state && state.trailColor))
|
|
1340
|
+
}, [running, state])
|
|
1341
|
+
|
|
1342
|
+
return e('span', { ref: anchorRef, hidden: true, 'aria-hidden': true })
|
|
1343
|
+
}
|
|
1344
|
+
|
|
697
1345
|
// -------------------------------------------------------------------------
|
|
698
1346
|
// 插件装配
|
|
699
1347
|
// -------------------------------------------------------------------------
|
|
@@ -708,6 +1356,11 @@ window.__ModuleLoader__.load({
|
|
|
708
1356
|
}, 'dsh-soul: browser lifecycle')
|
|
709
1357
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-soul: dictionaries')
|
|
710
1358
|
|
|
1359
|
+
// 暗色适配说明:原生 <select> 的弹出选项列表由浏览器按 color-scheme 渲染。
|
|
1360
|
+
// 宿主主题呈现器会把 html 的 color-scheme 与 body[data-ds-dark-theme] 随主题
|
|
1361
|
+
// 投影到 document(见 dsh-client-ui-layout ThemePresenter),因此这里用纯 CSS
|
|
1362
|
+
// 属性选择器跟随即可,无需 JS 监听;选项行另用主题变量显式着色兜底。
|
|
1363
|
+
|
|
711
1364
|
// 导航 label / 图标替换用的翻译函数(bind 返回的函数在调用时读取当前语言)
|
|
712
1365
|
const navT = ctx.locale.bind(NS)
|
|
713
1366
|
|
|
@@ -793,6 +1446,34 @@ window.__ModuleLoader__.load({
|
|
|
793
1446
|
// 注册图标替换效果
|
|
794
1447
|
ctx.effect(() => registerSettingsNavIcon(), 'dsh-soul: settings navigation icon')
|
|
795
1448
|
|
|
1449
|
+
// 注册到输入框工具栏:宿主会把 input.left 放在「操作权限」控件之后。
|
|
1450
|
+
ctx.slots.inject('conversation.input.left', () => ctx.slots.register({
|
|
1451
|
+
name: 'conversation.input.left',
|
|
1452
|
+
id: 'soul-quick-toggle',
|
|
1453
|
+
order: 50,
|
|
1454
|
+
locale: NS,
|
|
1455
|
+
inject: () => ({
|
|
1456
|
+
hooks: {
|
|
1457
|
+
soulController: controller.store
|
|
1458
|
+
},
|
|
1459
|
+
controller
|
|
1460
|
+
})
|
|
1461
|
+
}, SoulQuickToggle))
|
|
1462
|
+
|
|
1463
|
+
// 输入框光轨:注册到输入框卡片内部的 overlay 槽位。
|
|
1464
|
+
// 组件不渲染可见内容——它按会话运行态在最近的输入框卡片上挂载/隐藏 SVG 环;
|
|
1465
|
+
// 同一份 soulController store 保证与设置页的开关、颜色实时一致。
|
|
1466
|
+
ctx.slots.inject('conversation.input.overlay', () => ctx.slots.register({
|
|
1467
|
+
name: 'conversation.input.overlay',
|
|
1468
|
+
id: 'soul-trail',
|
|
1469
|
+
order: 50,
|
|
1470
|
+
inject: () => ({
|
|
1471
|
+
hooks: {
|
|
1472
|
+
soulController: controller.store
|
|
1473
|
+
}
|
|
1474
|
+
})
|
|
1475
|
+
}, SoulTrail))
|
|
1476
|
+
|
|
796
1477
|
// 注册到设置页面(label 用 navT 实时解析;壳层在语言切换时会重新解析 label)
|
|
797
1478
|
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
798
1479
|
name: 'settings.section',
|