@goodandready/dsh-goal 0.1.1 → 0.1.2
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 +14 -0
- package/README.ru.md +14 -0
- package/docs/design/DESIGN.md +9 -5
- package/lib/card-form-state.js +110 -0
- package/lib/client.js +700 -159
- package/lib/command-handler.js +189 -0
- package/lib/goal-engine.js +86 -6
- package/lib/index.js +279 -13
- package/package.json +10 -6
package/lib/client.js
CHANGED
|
@@ -17,7 +17,7 @@ window.__ModuleLoader__.load({
|
|
|
17
17
|
|
|
18
18
|
// --- SVG ИКОНКИ ---
|
|
19
19
|
|
|
20
|
-
function IconTarget({ size =
|
|
20
|
+
function IconTarget({ size = 15, className = '' }) {
|
|
21
21
|
return React.createElement(
|
|
22
22
|
'svg',
|
|
23
23
|
{
|
|
@@ -37,7 +37,7 @@ window.__ModuleLoader__.load({
|
|
|
37
37
|
);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function IconTrash({ size =
|
|
40
|
+
function IconTrash({ size = 14, className = '' }) {
|
|
41
41
|
return React.createElement(
|
|
42
42
|
'svg',
|
|
43
43
|
{
|
|
@@ -57,7 +57,7 @@ window.__ModuleLoader__.load({
|
|
|
57
57
|
);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
function IconPause({ size =
|
|
60
|
+
function IconPause({ size = 14, className = '' }) {
|
|
61
61
|
return React.createElement(
|
|
62
62
|
'svg',
|
|
63
63
|
{
|
|
@@ -77,7 +77,7 @@ window.__ModuleLoader__.load({
|
|
|
77
77
|
);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
function IconPlay({ size =
|
|
80
|
+
function IconPlay({ size = 14, className = '' }) {
|
|
81
81
|
return React.createElement(
|
|
82
82
|
'svg',
|
|
83
83
|
{
|
|
@@ -96,7 +96,26 @@ window.__ModuleLoader__.load({
|
|
|
96
96
|
);
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
function
|
|
99
|
+
function IconCheck({ size = 15, className = '' }) {
|
|
100
|
+
return React.createElement(
|
|
101
|
+
'svg',
|
|
102
|
+
{
|
|
103
|
+
width: size,
|
|
104
|
+
height: size,
|
|
105
|
+
viewBox: '0 0 24 24',
|
|
106
|
+
fill: 'none',
|
|
107
|
+
stroke: 'currentColor',
|
|
108
|
+
strokeWidth: 2.5,
|
|
109
|
+
strokeLinecap: 'round',
|
|
110
|
+
strokeLinejoin: 'round',
|
|
111
|
+
style: { color: 'var(--dsw-alias-status-success)' },
|
|
112
|
+
className,
|
|
113
|
+
},
|
|
114
|
+
React.createElement('polyline', { points: '20 6 9 17 4 12' }),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function IconExpand({ size = 14, className = '' }) {
|
|
100
119
|
return React.createElement(
|
|
101
120
|
'svg',
|
|
102
121
|
{
|
|
@@ -117,6 +136,25 @@ window.__ModuleLoader__.load({
|
|
|
117
136
|
);
|
|
118
137
|
}
|
|
119
138
|
|
|
139
|
+
function IconRotateCcw({ size = 13, className = '' }) {
|
|
140
|
+
return React.createElement(
|
|
141
|
+
'svg',
|
|
142
|
+
{
|
|
143
|
+
width: size,
|
|
144
|
+
height: size,
|
|
145
|
+
viewBox: '0 0 24 24',
|
|
146
|
+
fill: 'none',
|
|
147
|
+
stroke: 'currentColor',
|
|
148
|
+
strokeWidth: 2,
|
|
149
|
+
strokeLinecap: 'round',
|
|
150
|
+
strokeLinejoin: 'round',
|
|
151
|
+
className,
|
|
152
|
+
},
|
|
153
|
+
React.createElement('polyline', { points: '1 4 1 10 7 10' }),
|
|
154
|
+
React.createElement('path', { d: 'M3.51 15a9 9 0 1 0 2.13-9.36L1 10' }),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
120
158
|
function IconChevronDown({ size = 14, className = '' }) {
|
|
121
159
|
return React.createElement(
|
|
122
160
|
'svg',
|
|
@@ -135,22 +173,31 @@ window.__ModuleLoader__.load({
|
|
|
135
173
|
);
|
|
136
174
|
}
|
|
137
175
|
|
|
138
|
-
// --- СТИЛИ И CSS ПРАВИЛА ---
|
|
176
|
+
// --- СТИЛИ И CSS ПРАВИЛА (ЧИСТЫЕ DSH ТОКЕНЫ БЕЗ ХАРДКОД-ФОЛЛБЕКОВ) ---
|
|
139
177
|
const CSS_STYLES = `
|
|
178
|
+
.dsh-goal-dock {
|
|
179
|
+
box-sizing: border-box;
|
|
180
|
+
width: calc(100% - var(--dsh-composer-side-clearance, 0px) * 2 - var(--dsh-composer-dock-inset, 0px) * 4);
|
|
181
|
+
max-width: calc(var(--dsh-composer-card-max-width, 768px) - var(--dsh-composer-dock-inset, 0px) * 4);
|
|
182
|
+
margin: 0 auto 8px auto;
|
|
183
|
+
display: flex;
|
|
184
|
+
justify-content: center;
|
|
185
|
+
}
|
|
140
186
|
.dsh-goal-banner {
|
|
187
|
+
box-sizing: border-box;
|
|
188
|
+
width: 100%;
|
|
141
189
|
display: flex;
|
|
142
190
|
align-items: center;
|
|
143
191
|
justify-content: space-between;
|
|
144
|
-
background: var(--dsw-alias-bg-layer-
|
|
145
|
-
border: 1px solid var(--dsw-alias-border-l2
|
|
146
|
-
border-radius:
|
|
147
|
-
padding:
|
|
148
|
-
margin: 6px 12px;
|
|
192
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
193
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
194
|
+
border-radius: 12px;
|
|
195
|
+
padding: 6px 14px;
|
|
149
196
|
font-family: inherit;
|
|
150
197
|
font-size: 13px;
|
|
151
|
-
color: var(--dsw-alias-label-primary
|
|
198
|
+
color: var(--dsw-alias-label-primary);
|
|
152
199
|
user-select: none;
|
|
153
|
-
box-shadow: 0
|
|
200
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
|
154
201
|
transition: all 0.2s ease;
|
|
155
202
|
}
|
|
156
203
|
.dsh-goal-left {
|
|
@@ -161,33 +208,43 @@ window.__ModuleLoader__.load({
|
|
|
161
208
|
flex: 1;
|
|
162
209
|
}
|
|
163
210
|
.dsh-goal-icon {
|
|
164
|
-
color: var(--dsw-alias-label-secondary
|
|
211
|
+
color: var(--dsw-alias-label-secondary);
|
|
165
212
|
flex-shrink: 0;
|
|
166
213
|
display: flex;
|
|
167
214
|
align-items: center;
|
|
168
215
|
}
|
|
169
216
|
.dsh-goal-label {
|
|
170
217
|
font-weight: 600;
|
|
171
|
-
color: var(--dsw-alias-label-primary
|
|
218
|
+
color: var(--dsw-alias-label-primary);
|
|
172
219
|
white-space: nowrap;
|
|
173
220
|
}
|
|
221
|
+
.dsh-goal-label.completed {
|
|
222
|
+
color: var(--dsw-alias-status-success);
|
|
223
|
+
}
|
|
174
224
|
.dsh-goal-title {
|
|
175
|
-
color: var(--dsw-alias-label-secondary
|
|
225
|
+
color: var(--dsw-alias-label-secondary);
|
|
176
226
|
overflow: hidden;
|
|
177
227
|
text-overflow: ellipsis;
|
|
178
228
|
white-space: nowrap;
|
|
179
229
|
font-weight: 400;
|
|
230
|
+
flex: 1;
|
|
231
|
+
min-width: 0;
|
|
180
232
|
}
|
|
181
233
|
.dsh-goal-time {
|
|
182
|
-
color: var(--dsw-alias-label-
|
|
234
|
+
color: var(--dsw-alias-label-secondary);
|
|
183
235
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
184
236
|
font-size: 12px;
|
|
237
|
+
font-weight: 500;
|
|
185
238
|
white-space: nowrap;
|
|
239
|
+
display: inline-flex;
|
|
240
|
+
align-items: center;
|
|
241
|
+
gap: 4px;
|
|
242
|
+
flex-shrink: 0;
|
|
186
243
|
}
|
|
187
244
|
.dsh-goal-right {
|
|
188
245
|
display: flex;
|
|
189
246
|
align-items: center;
|
|
190
|
-
gap:
|
|
247
|
+
gap: 8px;
|
|
191
248
|
margin-left: 12px;
|
|
192
249
|
flex-shrink: 0;
|
|
193
250
|
}
|
|
@@ -196,7 +253,7 @@ window.__ModuleLoader__.load({
|
|
|
196
253
|
border: 0;
|
|
197
254
|
padding: 4px;
|
|
198
255
|
margin: 0;
|
|
199
|
-
color: var(--dsw-alias-label-tertiary
|
|
256
|
+
color: var(--dsw-alias-label-tertiary);
|
|
200
257
|
cursor: pointer;
|
|
201
258
|
display: flex;
|
|
202
259
|
align-items: center;
|
|
@@ -205,11 +262,19 @@ window.__ModuleLoader__.load({
|
|
|
205
262
|
transition: all 0.15s ease;
|
|
206
263
|
}
|
|
207
264
|
.dsh-goal-btn:hover {
|
|
208
|
-
color: var(--dsw-alias-label-primary
|
|
209
|
-
background: var(--dsw-alias-bg-layer-
|
|
265
|
+
color: var(--dsw-alias-label-primary);
|
|
266
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
267
|
+
}
|
|
268
|
+
.dsh-goal-btn.close {
|
|
269
|
+
font-size: 13px;
|
|
270
|
+
font-weight: bold;
|
|
271
|
+
padding: 3px 6px;
|
|
272
|
+
}
|
|
273
|
+
.dsh-goal-btn.close:hover {
|
|
274
|
+
color: var(--dsw-alias-status-danger);
|
|
210
275
|
}
|
|
211
276
|
.dsh-goal-btn.danger:hover {
|
|
212
|
-
color: var(--dsw-alias-status-danger
|
|
277
|
+
color: var(--dsw-alias-status-danger);
|
|
213
278
|
}
|
|
214
279
|
|
|
215
280
|
/* Modal Details */
|
|
@@ -224,15 +289,15 @@ window.__ModuleLoader__.load({
|
|
|
224
289
|
backdrop-filter: blur(4px);
|
|
225
290
|
}
|
|
226
291
|
.dsh-goal-modal {
|
|
227
|
-
background: var(--dsw-alias-bg-layer-3
|
|
228
|
-
border: 1px solid var(--dsw-alias-border-l2
|
|
292
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
293
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
229
294
|
border-radius: 12px;
|
|
230
295
|
width: 520px;
|
|
231
296
|
max-width: 90vw;
|
|
232
297
|
max-height: 80vh;
|
|
233
298
|
display: flex;
|
|
234
299
|
flex-direction: column;
|
|
235
|
-
box-shadow: 0 16px 36px rgba(0,0,0,0.4);
|
|
300
|
+
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.4);
|
|
236
301
|
}
|
|
237
302
|
.dsh-goal-modal-head {
|
|
238
303
|
padding: 14px 18px;
|
|
@@ -253,17 +318,6 @@ window.__ModuleLoader__.load({
|
|
|
253
318
|
flex-direction: column;
|
|
254
319
|
gap: 12px;
|
|
255
320
|
}
|
|
256
|
-
.dsh-goal-progress-bar {
|
|
257
|
-
height: 6px;
|
|
258
|
-
background: var(--dsw-alias-bg-layer-2, #2c2d30);
|
|
259
|
-
border-radius: 3px;
|
|
260
|
-
overflow: hidden;
|
|
261
|
-
}
|
|
262
|
-
.dsh-goal-progress-fill {
|
|
263
|
-
height: 100%;
|
|
264
|
-
background: var(--dsw-alias-status-success, #22c55e);
|
|
265
|
-
transition: width 0.3s ease;
|
|
266
|
-
}
|
|
267
321
|
.dsh-goal-milestone-item {
|
|
268
322
|
display: flex;
|
|
269
323
|
align-items: flex-start;
|
|
@@ -325,6 +379,27 @@ window.__ModuleLoader__.load({
|
|
|
325
379
|
gap: 6px;
|
|
326
380
|
padding: 12px 0;
|
|
327
381
|
}
|
|
382
|
+
.dsh-goal-field-label-row {
|
|
383
|
+
display: flex;
|
|
384
|
+
align-items: center;
|
|
385
|
+
justify-content: space-between;
|
|
386
|
+
font-size: 13px;
|
|
387
|
+
font-weight: 500;
|
|
388
|
+
}
|
|
389
|
+
.dsh-goal-field-meta {
|
|
390
|
+
display: flex;
|
|
391
|
+
align-items: center;
|
|
392
|
+
gap: 6px;
|
|
393
|
+
font-size: 11px;
|
|
394
|
+
color: var(--dsw-alias-label-tertiary);
|
|
395
|
+
}
|
|
396
|
+
.dsh-goal-override-tag {
|
|
397
|
+
font-size: 11px;
|
|
398
|
+
padding: 1px 6px;
|
|
399
|
+
border-radius: 4px;
|
|
400
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
401
|
+
color: var(--dsw-alias-label-secondary);
|
|
402
|
+
}
|
|
328
403
|
.dsh-goal-card-input {
|
|
329
404
|
height: 34px;
|
|
330
405
|
border: 1px solid var(--dsw-alias-border-l2);
|
|
@@ -334,6 +409,9 @@ window.__ModuleLoader__.load({
|
|
|
334
409
|
padding: 0 12px;
|
|
335
410
|
font-size: 13px;
|
|
336
411
|
}
|
|
412
|
+
.dsh-goal-card-input[aria-invalid="true"] {
|
|
413
|
+
border-color: var(--dsw-alias-status-danger);
|
|
414
|
+
}
|
|
337
415
|
.dsh-goal-chev {
|
|
338
416
|
margin-left: auto;
|
|
339
417
|
flex: none;
|
|
@@ -343,9 +421,90 @@ window.__ModuleLoader__.load({
|
|
|
343
421
|
.dsh-goal-chev-open {
|
|
344
422
|
transform: rotate(180deg);
|
|
345
423
|
}
|
|
424
|
+
.dsh-goal-check-row {
|
|
425
|
+
display: flex;
|
|
426
|
+
align-items: center;
|
|
427
|
+
justify-content: space-between;
|
|
428
|
+
padding: 8px 0;
|
|
429
|
+
}
|
|
430
|
+
.dsh-goal-check {
|
|
431
|
+
display: flex;
|
|
432
|
+
align-items: center;
|
|
433
|
+
gap: 8px;
|
|
434
|
+
font-size: 13px;
|
|
435
|
+
color: var(--dsw-alias-label-primary);
|
|
436
|
+
cursor: pointer;
|
|
437
|
+
}
|
|
438
|
+
.dsh-goal-foot {
|
|
439
|
+
border-top: 1px solid var(--dsw-alias-border-l2);
|
|
440
|
+
display: flex;
|
|
441
|
+
justify-content: flex-end;
|
|
442
|
+
align-items: center;
|
|
443
|
+
gap: 8px;
|
|
444
|
+
padding: 12px 0 4px;
|
|
445
|
+
}
|
|
446
|
+
.dsh-goal-foot-note {
|
|
447
|
+
color: var(--dsw-alias-status-danger);
|
|
448
|
+
font-size: 12px;
|
|
449
|
+
margin-right: auto;
|
|
450
|
+
}
|
|
451
|
+
.dsh-goal-btn-inline-reset {
|
|
452
|
+
background: transparent;
|
|
453
|
+
border: 0;
|
|
454
|
+
cursor: pointer;
|
|
455
|
+
padding: 2px 4px;
|
|
456
|
+
display: flex;
|
|
457
|
+
align-items: center;
|
|
458
|
+
gap: 4px;
|
|
459
|
+
border-radius: 4px;
|
|
460
|
+
color: var(--dsw-alias-label-tertiary);
|
|
461
|
+
font-size: 11px;
|
|
462
|
+
}
|
|
463
|
+
.dsh-goal-btn-inline-reset:hover {
|
|
464
|
+
color: var(--dsw-alias-label-primary);
|
|
465
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
466
|
+
}
|
|
467
|
+
.dsh-goal-discard {
|
|
468
|
+
appearance: none;
|
|
469
|
+
font: inherit;
|
|
470
|
+
cursor: pointer;
|
|
471
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
472
|
+
border-radius: 8px;
|
|
473
|
+
padding: 5px 14px;
|
|
474
|
+
font-size: 13px;
|
|
475
|
+
background: transparent;
|
|
476
|
+
color: var(--dsw-alias-label-secondary);
|
|
477
|
+
transition: all 0.15s ease;
|
|
478
|
+
}
|
|
479
|
+
.dsh-goal-discard:hover:not(:disabled) {
|
|
480
|
+
color: var(--dsw-alias-label-primary);
|
|
481
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
482
|
+
}
|
|
483
|
+
.dsh-goal-discard:disabled {
|
|
484
|
+
opacity: 0.4;
|
|
485
|
+
cursor: default;
|
|
486
|
+
}
|
|
487
|
+
.dsh-goal-save {
|
|
488
|
+
appearance: none;
|
|
489
|
+
font: inherit;
|
|
490
|
+
cursor: pointer;
|
|
491
|
+
border: 1px solid transparent;
|
|
492
|
+
border-radius: 8px;
|
|
493
|
+
padding: 5px 14px;
|
|
494
|
+
font-size: 13px;
|
|
495
|
+
background: var(--dsw-alias-label-primary);
|
|
496
|
+
color: var(--dsw-alias-bg-layer-3);
|
|
497
|
+
font-weight: 500;
|
|
498
|
+
transition: opacity 0.15s ease;
|
|
499
|
+
}
|
|
500
|
+
.dsh-goal-save:disabled {
|
|
501
|
+
opacity: 0.4;
|
|
502
|
+
cursor: default;
|
|
503
|
+
}
|
|
346
504
|
`;
|
|
347
505
|
|
|
348
506
|
function injectStylesOnce() {
|
|
507
|
+
if (typeof document === 'undefined') return;
|
|
349
508
|
if (document.getElementById('dsh-goal-styles')) return;
|
|
350
509
|
const style = document.createElement('style');
|
|
351
510
|
style.id = 'dsh-goal-styles';
|
|
@@ -358,8 +517,7 @@ window.__ModuleLoader__.load({
|
|
|
358
517
|
if (!state) return null;
|
|
359
518
|
|
|
360
519
|
const milestones = state.milestones || [];
|
|
361
|
-
const
|
|
362
|
-
const totalCount = milestones.length;
|
|
520
|
+
const isCompleted = state.state === 'COMPLETED';
|
|
363
521
|
|
|
364
522
|
return React.createElement(
|
|
365
523
|
'div',
|
|
@@ -378,12 +536,16 @@ window.__ModuleLoader__.load({
|
|
|
378
536
|
React.createElement(
|
|
379
537
|
'div',
|
|
380
538
|
{ style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
381
|
-
React.createElement(IconTarget, { size: 18 }),
|
|
382
|
-
React.createElement(
|
|
539
|
+
isCompleted ? React.createElement(IconCheck, { size: 18 }) : React.createElement(IconTarget, { size: 18 }),
|
|
540
|
+
React.createElement(
|
|
541
|
+
'span',
|
|
542
|
+
{ className: 'dsh-goal-modal-title' },
|
|
543
|
+
isCompleted ? (t('modalTitleCompleted') || 'Цель выполнена: план и результаты') : (t('modalTitle') || 'План и статус цели'),
|
|
544
|
+
),
|
|
383
545
|
),
|
|
384
546
|
React.createElement(
|
|
385
547
|
'button',
|
|
386
|
-
{ className: 'dsh-goal-btn', onClick: onClose },
|
|
548
|
+
{ className: 'dsh-goal-btn', onClick: onClose, title: t('close') || 'Закрыть' },
|
|
387
549
|
'✕',
|
|
388
550
|
),
|
|
389
551
|
),
|
|
@@ -393,36 +555,40 @@ window.__ModuleLoader__.load({
|
|
|
393
555
|
React.createElement(
|
|
394
556
|
'div',
|
|
395
557
|
null,
|
|
396
|
-
React.createElement('div', { style: { fontWeight: 600, marginBottom: 4 } }, state.title),
|
|
397
|
-
state.description ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 13 } }, state.description) : null,
|
|
558
|
+
React.createElement('div', { style: { fontWeight: 600, fontSize: 15, marginBottom: 4 } }, state.title),
|
|
559
|
+
state.description ? React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 13, marginBottom: 4 } }, state.description) : null,
|
|
398
560
|
),
|
|
399
561
|
React.createElement(
|
|
400
562
|
'div',
|
|
401
|
-
{ style: { display: 'flex', justifyContent: '
|
|
402
|
-
React.createElement('span', null, `${t('progress') || 'Прогресс'}: ${completedCount}/${totalCount} (${state.progressPercent || 0}%)`),
|
|
563
|
+
{ style: { display: 'flex', justifyContent: 'flex-end', fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } },
|
|
403
564
|
React.createElement('span', null, `${t('time') || 'Время'}: ${state.formattedElapsed || '0s'}`),
|
|
404
565
|
),
|
|
405
566
|
React.createElement(
|
|
406
567
|
'div',
|
|
407
|
-
{
|
|
408
|
-
React.createElement('div', {
|
|
409
|
-
),
|
|
410
|
-
React.createElement(
|
|
411
|
-
'div',
|
|
412
|
-
{ style: { display: 'flex', flexDirection: 'column', gap: 8, marginTop: 8 } },
|
|
413
|
-
React.createElement('div', { style: { fontWeight: 600, fontSize: 13 } }, t('milestones') || 'Шаги выполнения:'),
|
|
568
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 8, marginTop: 4 } },
|
|
569
|
+
React.createElement('div', { style: { fontWeight: 600, fontSize: 13 } }, t('milestones') || 'План работ:'),
|
|
414
570
|
milestones.length === 0
|
|
415
|
-
? React.createElement('div', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 12 } }, t('noMilestones') || 'Агент
|
|
571
|
+
? React.createElement('div', { style: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 12, padding: '8px 0' } }, t('noMilestones') || 'Агент формирует план работ...')
|
|
416
572
|
: milestones.map((m, i) =>
|
|
417
573
|
React.createElement(
|
|
418
574
|
'div',
|
|
419
575
|
{ key: m.id || i, className: 'dsh-goal-milestone-item' },
|
|
420
|
-
React.createElement('span',
|
|
576
|
+
React.createElement('span', { style: { fontSize: 14 } }, m.status === 'completed' ? '✅' : m.status === 'in_progress' ? '🔄' : '⏳'),
|
|
421
577
|
React.createElement(
|
|
422
578
|
'div',
|
|
423
|
-
{ style: { flex: 1 } },
|
|
424
|
-
React.createElement(
|
|
425
|
-
|
|
579
|
+
{ style: { flex: 1, minWidth: 0 } },
|
|
580
|
+
React.createElement(
|
|
581
|
+
'div',
|
|
582
|
+
{
|
|
583
|
+
style: {
|
|
584
|
+
textDecoration: m.status === 'completed' ? 'line-through' : 'none',
|
|
585
|
+
color: m.status === 'completed' ? 'var(--dsw-alias-label-tertiary)' : 'inherit',
|
|
586
|
+
fontWeight: m.status === 'in_progress' ? 600 : 400,
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
`${i + 1}. ${m.title}`,
|
|
590
|
+
),
|
|
591
|
+
m.notes ? React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary)', marginTop: 2 } }, m.notes) : null,
|
|
426
592
|
),
|
|
427
593
|
),
|
|
428
594
|
),
|
|
@@ -431,12 +597,26 @@ window.__ModuleLoader__.load({
|
|
|
431
597
|
React.createElement(
|
|
432
598
|
'div',
|
|
433
599
|
{ className: 'dsh-goal-modal-foot' },
|
|
600
|
+
isCompleted
|
|
601
|
+
? React.createElement(
|
|
602
|
+
'button',
|
|
603
|
+
{
|
|
604
|
+
className: 'dsh-goal-btn danger',
|
|
605
|
+
onClick: () => {
|
|
606
|
+
onAction('clear');
|
|
607
|
+
onClose();
|
|
608
|
+
},
|
|
609
|
+
style: { padding: '6px 12px', fontSize: 13 },
|
|
610
|
+
},
|
|
611
|
+
t('closeBanner') || 'Закрыть плашку цели',
|
|
612
|
+
)
|
|
613
|
+
: null,
|
|
434
614
|
React.createElement(
|
|
435
615
|
'button',
|
|
436
616
|
{
|
|
437
|
-
className: 'dsh-goal-
|
|
438
|
-
style: { padding: '6px 14px', background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-primary)' },
|
|
617
|
+
className: 'dsh-goal-save',
|
|
439
618
|
onClick: onClose,
|
|
619
|
+
style: { padding: '6px 16px' },
|
|
440
620
|
},
|
|
441
621
|
t('close') || 'Закрыть',
|
|
442
622
|
),
|
|
@@ -445,11 +625,33 @@ window.__ModuleLoader__.load({
|
|
|
445
625
|
);
|
|
446
626
|
}
|
|
447
627
|
|
|
448
|
-
// ---
|
|
628
|
+
// --- ПАНЕЛЬ ЦЕЛИ НАД ПОЛЕМ ВВОДА (COMPOSER DOCK WIDGET) ---
|
|
449
629
|
function GoalTopBanner(props) {
|
|
630
|
+
const { ctx } = props || {};
|
|
450
631
|
const [state, setState] = useState(null);
|
|
451
632
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
452
|
-
const
|
|
633
|
+
const [now, setNow] = useState(() => Date.now());
|
|
634
|
+
|
|
635
|
+
const t = (key) => {
|
|
636
|
+
if (ctx?.locale?.getSnapshot) {
|
|
637
|
+
const snap = ctx.locale.getSnapshot();
|
|
638
|
+
const active = snap?.active || 'ru';
|
|
639
|
+
return LOCALES[active]?.[key] || LOCALES.ru[key] || LOCALES.en[key] || key;
|
|
640
|
+
}
|
|
641
|
+
return LOCALES.ru[key] || LOCALES.en[key] || key;
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
useEffect(() => {
|
|
645
|
+
injectStylesOnce();
|
|
646
|
+
}, []);
|
|
647
|
+
|
|
648
|
+
// Локальный секундный интервал для плавного и бесперебойного тикания таймера
|
|
649
|
+
useEffect(() => {
|
|
650
|
+
const ticker = setInterval(() => {
|
|
651
|
+
setNow(Date.now());
|
|
652
|
+
}, 1000);
|
|
653
|
+
return () => clearInterval(ticker);
|
|
654
|
+
}, []);
|
|
453
655
|
|
|
454
656
|
const fetchState = useCallback(async () => {
|
|
455
657
|
try {
|
|
@@ -462,89 +664,105 @@ window.__ModuleLoader__.load({
|
|
|
462
664
|
}, []);
|
|
463
665
|
|
|
464
666
|
useEffect(() => {
|
|
465
|
-
injectStylesOnce();
|
|
466
667
|
fetchState();
|
|
467
|
-
const
|
|
468
|
-
return () => clearInterval(
|
|
668
|
+
const timer = setInterval(fetchState, 1500);
|
|
669
|
+
return () => clearInterval(timer);
|
|
469
670
|
}, [fetchState]);
|
|
470
671
|
|
|
471
672
|
const handleAction = async (action, extra = {}) => {
|
|
472
673
|
try {
|
|
473
|
-
await fetch('/dsh-goal/action', {
|
|
674
|
+
const res = await fetch('/dsh-goal/action', {
|
|
474
675
|
method: 'POST',
|
|
475
676
|
headers: { 'Content-Type': 'application/json' },
|
|
476
677
|
body: JSON.stringify({ action, ...extra }),
|
|
477
678
|
});
|
|
478
|
-
|
|
679
|
+
if (res.ok) {
|
|
680
|
+
const data = await res.json();
|
|
681
|
+
if (data.state) setState(data.state);
|
|
682
|
+
}
|
|
479
683
|
} catch (_) {}
|
|
480
684
|
};
|
|
481
685
|
|
|
482
|
-
// Все хуки объявлены выше!
|
|
483
686
|
if (!state || !state.hasActiveGoal) {
|
|
484
|
-
return null;
|
|
687
|
+
return null;
|
|
485
688
|
}
|
|
486
689
|
|
|
487
690
|
const isPaused = state.state === 'PAUSED';
|
|
691
|
+
const isCompleted = state.state === 'COMPLETED';
|
|
692
|
+
|
|
693
|
+
const formatLiveElapsed = () => {
|
|
694
|
+
if (!state) return '0s';
|
|
695
|
+
if (!state.startedAt) return state.formattedElapsed || '0s';
|
|
696
|
+
const endTime = state.completedAt || (state.pausedAt || now);
|
|
697
|
+
const elapsedSec = Math.max(0, Math.floor((endTime - state.startedAt - (state.totalPausedDurationMs || 0)) / 1000));
|
|
698
|
+
return formatElapsed(elapsedSec);
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
const liveElapsedStr = formatLiveElapsed();
|
|
488
702
|
|
|
489
703
|
return React.createElement(
|
|
490
704
|
React.Fragment,
|
|
491
705
|
null,
|
|
492
706
|
React.createElement(
|
|
493
707
|
'div',
|
|
494
|
-
{ className: 'dsh-goal-
|
|
495
|
-
React.createElement(
|
|
496
|
-
'div',
|
|
497
|
-
{ className: 'dsh-goal-left' },
|
|
498
|
-
React.createElement(
|
|
499
|
-
'span',
|
|
500
|
-
{ className: 'dsh-goal-icon' },
|
|
501
|
-
React.createElement(IconTarget, { size: 16 }),
|
|
502
|
-
),
|
|
503
|
-
React.createElement(
|
|
504
|
-
'span',
|
|
505
|
-
{ className: 'dsh-goal-label' },
|
|
506
|
-
t('goalLabel') || 'Текущая цель',
|
|
507
|
-
),
|
|
508
|
-
React.createElement(
|
|
509
|
-
'span',
|
|
510
|
-
{ className: 'dsh-goal-title', title: state.title },
|
|
511
|
-
state.title,
|
|
512
|
-
),
|
|
513
|
-
React.createElement(
|
|
514
|
-
'span',
|
|
515
|
-
{ className: 'dsh-goal-time' },
|
|
516
|
-
`• ${state.formattedElapsed || '0s'}`,
|
|
517
|
-
),
|
|
518
|
-
),
|
|
708
|
+
{ className: 'dsh-goal-dock', 'data-goal-dock': 'true' },
|
|
519
709
|
React.createElement(
|
|
520
710
|
'div',
|
|
521
|
-
{ className:
|
|
711
|
+
{ className: `dsh-goal-banner${isCompleted ? ' completed' : ''}` },
|
|
522
712
|
React.createElement(
|
|
523
|
-
'
|
|
524
|
-
{
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
onClick: () => handleAction(isPaused ? 'resume' : 'pause'),
|
|
537
|
-
},
|
|
538
|
-
isPaused ? React.createElement(IconPlay, { size: 15 }) : React.createElement(IconPause, { size: 15 }),
|
|
713
|
+
'div',
|
|
714
|
+
{ className: 'dsh-goal-left' },
|
|
715
|
+
React.createElement(
|
|
716
|
+
'span',
|
|
717
|
+
{ className: 'dsh-goal-icon' },
|
|
718
|
+
isCompleted ? React.createElement(IconCheck, { size: 16 }) : React.createElement(IconTarget, { size: 15 }),
|
|
719
|
+
),
|
|
720
|
+
React.createElement(
|
|
721
|
+
'span',
|
|
722
|
+
{ className: `dsh-goal-label${isCompleted ? ' completed' : ''}` },
|
|
723
|
+
isCompleted ? (t('goalCompleted') || 'Цель выполнена') : (t('goalLabel') || 'Текущая цель'),
|
|
724
|
+
),
|
|
725
|
+
React.createElement('span', { className: 'dsh-goal-title', title: state.title }, state.title),
|
|
539
726
|
),
|
|
540
727
|
React.createElement(
|
|
541
|
-
'
|
|
542
|
-
{
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
728
|
+
'div',
|
|
729
|
+
{ className: 'dsh-goal-right' },
|
|
730
|
+
React.createElement(
|
|
731
|
+
'span',
|
|
732
|
+
{ className: 'dsh-goal-time' },
|
|
733
|
+
`⏱ ${liveElapsedStr}`,
|
|
734
|
+
),
|
|
735
|
+
!isCompleted
|
|
736
|
+
? React.createElement(
|
|
737
|
+
'button',
|
|
738
|
+
{
|
|
739
|
+
className: 'dsh-goal-btn',
|
|
740
|
+
title: isPaused ? (t('resume') || 'Возобновить') : (t('pause') || 'Приостановить'),
|
|
741
|
+
onClick: () => handleAction(isPaused ? 'resume' : 'pause'),
|
|
742
|
+
},
|
|
743
|
+
isPaused ? React.createElement(IconPlay, { size: 14 }) : React.createElement(IconPause, { size: 14 }),
|
|
744
|
+
)
|
|
745
|
+
: null,
|
|
746
|
+
React.createElement(
|
|
747
|
+
'button',
|
|
748
|
+
{
|
|
749
|
+
className: 'dsh-goal-btn',
|
|
750
|
+
title: t('details') || 'Развернуть детали цели',
|
|
751
|
+
onClick: () => setIsModalOpen(true),
|
|
752
|
+
},
|
|
753
|
+
React.createElement(IconExpand, { size: 14 }),
|
|
754
|
+
),
|
|
755
|
+
isCompleted
|
|
756
|
+
? React.createElement(
|
|
757
|
+
'button',
|
|
758
|
+
{
|
|
759
|
+
className: 'dsh-goal-btn close',
|
|
760
|
+
title: t('closeBanner') || 'Закрыть плашку цели',
|
|
761
|
+
onClick: () => handleAction('clear'),
|
|
762
|
+
},
|
|
763
|
+
'✕',
|
|
764
|
+
)
|
|
765
|
+
: null,
|
|
548
766
|
),
|
|
549
767
|
),
|
|
550
768
|
),
|
|
@@ -559,17 +777,191 @@ window.__ModuleLoader__.load({
|
|
|
559
777
|
);
|
|
560
778
|
}
|
|
561
779
|
|
|
562
|
-
// ---
|
|
780
|
+
// --- ЛОГИКА ФОРМЫ НАСТРОЕК ---
|
|
781
|
+
// Архитектурное решение (Issue #23):
|
|
782
|
+
// Чистая логика состояния формы настроек вынесена в `lib/card-form-state.js` для запуска
|
|
783
|
+
// быстрых изолированных unit-тестов через `node --test`. В `lib/client.js` эти функции
|
|
784
|
+
// продублированы встроенным образом, поскольку клиентский бандл загружается браузерным
|
|
785
|
+
// загрузчиком DSH (`window.__ModuleLoader__`) как самодостаточный автономный скрипт
|
|
786
|
+
// без этапа сборки (bundler / webpack) и не может динамически импортировать локальные файлы.
|
|
787
|
+
const DEFAULT_SETTINGS = {
|
|
788
|
+
maxIterations: 25,
|
|
789
|
+
autoDrive: true,
|
|
790
|
+
enableSound: true,
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
function parseNumberField(raw) {
|
|
794
|
+
if (raw === '' || raw === null || raw === undefined) {
|
|
795
|
+
return undefined;
|
|
796
|
+
}
|
|
797
|
+
const n = Number(raw);
|
|
798
|
+
if (!Number.isFinite(n) || n < 1 || !Number.isInteger(n)) {
|
|
799
|
+
return NaN;
|
|
800
|
+
}
|
|
801
|
+
return n;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function getFieldStatus(field, draftValue, snap) {
|
|
805
|
+
const baseVal = snap?.base?.[field] ?? DEFAULT_SETTINGS[field];
|
|
806
|
+
const userVal = snap?.user?.[field];
|
|
807
|
+
const effectiveVal = snap?.value?.[field] ?? baseVal;
|
|
808
|
+
|
|
809
|
+
const isOverridden = userVal !== undefined && userVal !== null;
|
|
810
|
+
const currentVal = draftValue !== undefined ? draftValue : effectiveVal;
|
|
811
|
+
|
|
812
|
+
let invalid = false;
|
|
813
|
+
let isDirty = false;
|
|
814
|
+
|
|
815
|
+
if (field === 'maxIterations') {
|
|
816
|
+
if (draftValue !== undefined) {
|
|
817
|
+
if (draftValue === '') {
|
|
818
|
+
invalid = false;
|
|
819
|
+
isDirty = userVal !== undefined;
|
|
820
|
+
} else {
|
|
821
|
+
const parsed = parseNumberField(draftValue);
|
|
822
|
+
if (Number.isNaN(parsed)) {
|
|
823
|
+
invalid = true;
|
|
824
|
+
isDirty = true;
|
|
825
|
+
} else {
|
|
826
|
+
isDirty = parsed !== (userVal !== undefined ? userVal : baseVal);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
} else {
|
|
831
|
+
if (draftValue !== undefined) {
|
|
832
|
+
isDirty = draftValue !== (userVal !== undefined ? userVal : baseVal);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return {
|
|
837
|
+
value: currentVal,
|
|
838
|
+
baseValue: baseVal,
|
|
839
|
+
userValue: userVal,
|
|
840
|
+
isOverridden,
|
|
841
|
+
isDirty,
|
|
842
|
+
invalid,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function computeSavePlan(draft, snap) {
|
|
847
|
+
if (!draft) return [];
|
|
848
|
+
|
|
849
|
+
const writes = [];
|
|
850
|
+
const fields = ['maxIterations', 'autoDrive', 'enableSound'];
|
|
851
|
+
|
|
852
|
+
for (const f of fields) {
|
|
853
|
+
if (draft[f] === undefined) continue;
|
|
854
|
+
|
|
855
|
+
const status = getFieldStatus(f, draft[f], snap);
|
|
856
|
+
if (status.invalid) continue;
|
|
857
|
+
|
|
858
|
+
if (status.isDirty) {
|
|
859
|
+
if (f === 'maxIterations' && draft[f] === '') {
|
|
860
|
+
writes.push([f, undefined]);
|
|
861
|
+
} else if (f === 'maxIterations') {
|
|
862
|
+
writes.push([f, parseNumberField(draft[f])]);
|
|
863
|
+
} else {
|
|
864
|
+
writes.push([f, Boolean(draft[f])]);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return writes;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// --- КАРТОЧКА НАСТРОЕК (SETTINGS CARD) ---
|
|
563
873
|
function GoalSettingsCard(props) {
|
|
874
|
+
const { scope, ctx } = props || {};
|
|
564
875
|
const [isOpen, setIsOpen] = useState(false);
|
|
565
|
-
const [
|
|
566
|
-
const
|
|
567
|
-
const
|
|
876
|
+
const [draft, setDraft] = useState(null);
|
|
877
|
+
const [saving, setSaving] = useState(false);
|
|
878
|
+
const [failed, setFailed] = useState(false);
|
|
879
|
+
|
|
880
|
+
const [snap, setSnap] = useState(() => (scope ? scope.getSnapshot() : { value: DEFAULT_SETTINGS, base: DEFAULT_SETTINGS, user: {} }));
|
|
568
881
|
|
|
569
882
|
useEffect(() => {
|
|
570
883
|
injectStylesOnce();
|
|
571
884
|
}, []);
|
|
572
885
|
|
|
886
|
+
useEffect(() => {
|
|
887
|
+
if (!scope || typeof scope.subscribe !== 'function') return;
|
|
888
|
+
return scope.subscribe((next) => {
|
|
889
|
+
setSnap(next || scope.getSnapshot());
|
|
890
|
+
});
|
|
891
|
+
}, [scope]);
|
|
892
|
+
|
|
893
|
+
const t = (key) => {
|
|
894
|
+
if (ctx?.locale?.getSnapshot) {
|
|
895
|
+
const snapLoc = ctx.locale.getSnapshot();
|
|
896
|
+
const active = snapLoc?.active || 'ru';
|
|
897
|
+
return LOCALES[active]?.[key] || LOCALES.ru[key] || LOCALES.en[key] || key;
|
|
898
|
+
}
|
|
899
|
+
return LOCALES.ru[key] || LOCALES.en[key] || key;
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
const status = (snap && snap.status) || 'ready';
|
|
903
|
+
const maxIterStatus = getFieldStatus('maxIterations', draft?.maxIterations, snap);
|
|
904
|
+
const autoDriveStatus = getFieldStatus('autoDrive', draft?.autoDrive, snap);
|
|
905
|
+
const enableSoundStatus = getFieldStatus('enableSound', draft?.enableSound, snap);
|
|
906
|
+
|
|
907
|
+
const dirty = maxIterStatus.isDirty || autoDriveStatus.isDirty || enableSoundStatus.isDirty;
|
|
908
|
+
const invalid = maxIterStatus.invalid;
|
|
909
|
+
const disabled = !scope || saving || snap?.writable === false;
|
|
910
|
+
|
|
911
|
+
const edit = (field, val) => {
|
|
912
|
+
setFailed(false);
|
|
913
|
+
setDraft((prev) => ({
|
|
914
|
+
maxIterations: prev?.maxIterations !== undefined ? prev.maxIterations : maxIterStatus.value,
|
|
915
|
+
autoDrive: prev?.autoDrive !== undefined ? prev.autoDrive : autoDriveStatus.value,
|
|
916
|
+
enableSound: prev?.enableSound !== undefined ? prev.enableSound : enableSoundStatus.value,
|
|
917
|
+
[field]: val,
|
|
918
|
+
}));
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
const resetFieldToDefault = (field) => {
|
|
922
|
+
if (field === 'maxIterations') {
|
|
923
|
+
edit(field, '');
|
|
924
|
+
} else {
|
|
925
|
+
edit(field, snap?.base?.[field] ?? DEFAULT_SETTINGS[field]);
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
const save = async () => {
|
|
930
|
+
if (!scope || !dirty || invalid) return;
|
|
931
|
+
setSaving(true);
|
|
932
|
+
setFailed(false);
|
|
933
|
+
|
|
934
|
+
const writes = computeSavePlan(draft, snap);
|
|
935
|
+
let landed = true;
|
|
936
|
+
|
|
937
|
+
for (const [field, fieldValue] of writes) {
|
|
938
|
+
try {
|
|
939
|
+
await scope.set(field, fieldValue);
|
|
940
|
+
} catch (err) {
|
|
941
|
+
console.error('[dsh-goal] failed to save setting', field, err);
|
|
942
|
+
landed = false;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const fresh = scope.getSnapshot();
|
|
947
|
+
const user = fresh.user || {};
|
|
948
|
+
for (const [field, fieldValue] of writes) {
|
|
949
|
+
if (fieldValue === undefined) {
|
|
950
|
+
if (user[field] !== undefined) landed = false;
|
|
951
|
+
} else {
|
|
952
|
+
if (user[field] !== fieldValue) landed = false;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
setSnap(fresh);
|
|
957
|
+
setSaving(false);
|
|
958
|
+
if (landed) {
|
|
959
|
+
setDraft(null);
|
|
960
|
+
} else {
|
|
961
|
+
setFailed(true);
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
|
|
573
965
|
const Chevron = (Primitives && Primitives.IconChevronDownOutline14) || IconChevronDown;
|
|
574
966
|
|
|
575
967
|
return React.createElement(
|
|
@@ -594,19 +986,134 @@ window.__ModuleLoader__.load({
|
|
|
594
986
|
? React.createElement(
|
|
595
987
|
'div',
|
|
596
988
|
{ className: 'dsh-goal-card-body' },
|
|
597
|
-
|
|
598
|
-
'
|
|
599
|
-
|
|
600
|
-
|
|
989
|
+
status === 'loading'
|
|
990
|
+
? React.createElement('p', { className: 'dsh-goal-field-hint', style: { padding: '12px 0' } }, t('loading') || 'Загрузка настроек…')
|
|
991
|
+
: status !== 'ready'
|
|
992
|
+
? React.createElement('p', { className: 'dsh-goal-foot-note', style: { padding: '12px 0' } }, t('unavailable') || 'Настройки недоступны')
|
|
993
|
+
: React.createElement(
|
|
994
|
+
React.Fragment,
|
|
995
|
+
null,
|
|
996
|
+
React.createElement(
|
|
997
|
+
'div',
|
|
998
|
+
{ className: 'dsh-goal-card-field' },
|
|
999
|
+
React.createElement(
|
|
1000
|
+
'div',
|
|
1001
|
+
{ className: 'dsh-goal-field-label-row' },
|
|
1002
|
+
React.createElement('label', { htmlFor: 'dsh-goal-max-iter' }, t('maxIterLabel') || 'Максимум итераций (Safety Limit):'),
|
|
1003
|
+
React.createElement(
|
|
1004
|
+
'div',
|
|
1005
|
+
{ className: 'dsh-goal-field-meta' },
|
|
1006
|
+
maxIterStatus.isOverridden
|
|
1007
|
+
? React.createElement('span', { className: 'dsh-goal-override-tag' }, t('overridden') || 'изменено')
|
|
1008
|
+
: null,
|
|
1009
|
+
maxIterStatus.isOverridden
|
|
1010
|
+
? React.createElement(
|
|
1011
|
+
'button',
|
|
1012
|
+
{
|
|
1013
|
+
type: 'button',
|
|
1014
|
+
className: 'dsh-goal-btn-inline-reset',
|
|
1015
|
+
title: t('resetField') || 'Сбросить к значению по умолчанию',
|
|
1016
|
+
onClick: () => resetFieldToDefault('maxIterations'),
|
|
1017
|
+
},
|
|
1018
|
+
React.createElement(IconRotateCcw, { size: 12 }),
|
|
1019
|
+
t('resetField') || 'По умолчанию',
|
|
1020
|
+
)
|
|
1021
|
+
: null,
|
|
1022
|
+
),
|
|
1023
|
+
),
|
|
601
1024
|
React.createElement('input', {
|
|
1025
|
+
id: 'dsh-goal-max-iter',
|
|
602
1026
|
type: 'number',
|
|
1027
|
+
min: 1,
|
|
1028
|
+
placeholder: String(maxIterStatus.baseValue),
|
|
603
1029
|
className: 'dsh-goal-card-input',
|
|
604
|
-
value:
|
|
605
|
-
|
|
1030
|
+
value: maxIterStatus.value !== undefined ? maxIterStatus.value : '',
|
|
1031
|
+
disabled,
|
|
1032
|
+
'aria-invalid': invalid,
|
|
1033
|
+
onChange: (e) => edit('maxIterations', e.target.value),
|
|
606
1034
|
}),
|
|
607
1035
|
),
|
|
1036
|
+
React.createElement(
|
|
1037
|
+
'div',
|
|
1038
|
+
{ className: 'dsh-goal-check-row' },
|
|
1039
|
+
React.createElement(
|
|
1040
|
+
'label',
|
|
1041
|
+
{ className: 'dsh-goal-check' },
|
|
1042
|
+
React.createElement('input', {
|
|
1043
|
+
type: 'checkbox',
|
|
1044
|
+
checked: autoDriveStatus.value === true,
|
|
1045
|
+
disabled,
|
|
1046
|
+
onChange: (e) => edit('autoDrive', e.target.checked),
|
|
1047
|
+
}),
|
|
1048
|
+
t('autoDriveLabel') || 'Авто-драйв: продолжать цикл автоматически',
|
|
1049
|
+
),
|
|
1050
|
+
autoDriveStatus.isOverridden
|
|
1051
|
+
? React.createElement(
|
|
1052
|
+
'button',
|
|
1053
|
+
{
|
|
1054
|
+
type: 'button',
|
|
1055
|
+
className: 'dsh-goal-btn-inline-reset',
|
|
1056
|
+
title: t('resetField') || 'Сбросить к значению по умолчанию',
|
|
1057
|
+
onClick: () => resetFieldToDefault('autoDrive'),
|
|
1058
|
+
},
|
|
1059
|
+
React.createElement(IconRotateCcw, { size: 12 }),
|
|
1060
|
+
)
|
|
1061
|
+
: null,
|
|
1062
|
+
),
|
|
1063
|
+
React.createElement(
|
|
1064
|
+
'div',
|
|
1065
|
+
{ className: 'dsh-goal-check-row' },
|
|
1066
|
+
React.createElement(
|
|
1067
|
+
'label',
|
|
1068
|
+
{ className: 'dsh-goal-check' },
|
|
1069
|
+
React.createElement('input', {
|
|
1070
|
+
type: 'checkbox',
|
|
1071
|
+
checked: enableSoundStatus.value === true,
|
|
1072
|
+
disabled,
|
|
1073
|
+
onChange: (e) => edit('enableSound', e.target.checked),
|
|
1074
|
+
}),
|
|
1075
|
+
t('soundLabel') || 'Звук по завершении цели',
|
|
1076
|
+
),
|
|
1077
|
+
enableSoundStatus.isOverridden
|
|
1078
|
+
? React.createElement(
|
|
1079
|
+
'button',
|
|
1080
|
+
{
|
|
1081
|
+
type: 'button',
|
|
1082
|
+
className: 'dsh-goal-btn-inline-reset',
|
|
1083
|
+
title: t('resetField') || 'Сбросить к значению по умолчанию',
|
|
1084
|
+
onClick: () => resetFieldToDefault('enableSound'),
|
|
1085
|
+
},
|
|
1086
|
+
React.createElement(IconRotateCcw, { size: 12 }),
|
|
1087
|
+
)
|
|
1088
|
+
: null,
|
|
1089
|
+
),
|
|
1090
|
+
React.createElement(
|
|
1091
|
+
'div',
|
|
1092
|
+
{ className: 'dsh-goal-foot' },
|
|
1093
|
+
failed
|
|
1094
|
+
? React.createElement('span', { className: 'dsh-goal-foot-note' }, t('saveFailed') || 'Не сохранено — исправьте и повторите')
|
|
1095
|
+
: null,
|
|
1096
|
+
React.createElement(
|
|
1097
|
+
'button',
|
|
1098
|
+
{
|
|
1099
|
+
className: 'dsh-goal-discard',
|
|
1100
|
+
disabled: !dirty || disabled,
|
|
1101
|
+
onClick: () => {
|
|
1102
|
+
setFailed(false);
|
|
1103
|
+
setDraft(null);
|
|
1104
|
+
},
|
|
1105
|
+
},
|
|
1106
|
+
t('discard') || 'Отменить правки',
|
|
1107
|
+
),
|
|
1108
|
+
React.createElement(
|
|
1109
|
+
'button',
|
|
1110
|
+
{ className: 'dsh-goal-save', disabled: !dirty || disabled || invalid, onClick: save },
|
|
1111
|
+
saving ? t('saving') || 'Сохранение…' : t('save') || 'Сохранить',
|
|
1112
|
+
),
|
|
1113
|
+
),
|
|
608
1114
|
)
|
|
609
|
-
|
|
1115
|
+
)
|
|
1116
|
+
: null,
|
|
610
1117
|
);
|
|
611
1118
|
}
|
|
612
1119
|
|
|
@@ -614,49 +1121,94 @@ window.__ModuleLoader__.load({
|
|
|
614
1121
|
const LOCALES = {
|
|
615
1122
|
ru: {
|
|
616
1123
|
goalLabel: 'Текущая цель',
|
|
617
|
-
|
|
1124
|
+
goalCompleted: 'Цель выполнена',
|
|
1125
|
+
clearGoal: 'Очистить цель',
|
|
1126
|
+
closeBanner: 'Закрыть плашку цели',
|
|
618
1127
|
pause: 'Приостановить',
|
|
619
1128
|
resume: 'Возобновить',
|
|
620
1129
|
details: 'Развернуть детали цели',
|
|
621
|
-
modalTitle: '
|
|
622
|
-
|
|
1130
|
+
modalTitle: 'План и статус цели',
|
|
1131
|
+
modalTitleCompleted: 'Цель выполнена: план и результаты',
|
|
623
1132
|
time: 'Время',
|
|
624
|
-
milestones: '
|
|
625
|
-
noMilestones: 'Агент
|
|
1133
|
+
milestones: 'План работ:',
|
|
1134
|
+
noMilestones: 'Агент формирует план работ...',
|
|
626
1135
|
close: 'Закрыть',
|
|
627
1136
|
pluginTitle: 'Цели и автономный режим (Goal Mode)',
|
|
628
|
-
pluginDesc: '
|
|
1137
|
+
pluginDesc: 'Панель цели над полем ввода, декомпозиция вех и авто-драйв',
|
|
629
1138
|
maxIterLabel: 'Максимум итераций (Safety Limit):',
|
|
1139
|
+
autoDriveLabel: 'Авто-драйв: продолжать цикл автоматически',
|
|
1140
|
+
soundLabel: 'Звук по завершении цели',
|
|
1141
|
+
save: 'Сохранить',
|
|
1142
|
+
saving: 'Сохранение…',
|
|
1143
|
+
discard: 'Отменить правки',
|
|
1144
|
+
resetField: 'По умолчанию',
|
|
1145
|
+
overridden: 'изменено',
|
|
1146
|
+
saveFailed: 'Не сохранено — исправьте и повторите',
|
|
1147
|
+
loading: 'Загрузка настроек…',
|
|
1148
|
+
unavailable: 'Настройки недоступны',
|
|
630
1149
|
},
|
|
631
1150
|
en: {
|
|
632
|
-
goalLabel: 'Current
|
|
633
|
-
|
|
1151
|
+
goalLabel: 'Current goal',
|
|
1152
|
+
goalCompleted: 'Goal completed',
|
|
1153
|
+
clearGoal: 'Clear goal',
|
|
1154
|
+
closeBanner: 'Close goal banner',
|
|
634
1155
|
pause: 'Pause',
|
|
635
1156
|
resume: 'Resume',
|
|
636
1157
|
details: 'Expand Goal Details',
|
|
637
|
-
modalTitle: '
|
|
638
|
-
|
|
1158
|
+
modalTitle: 'Goal Plan & Status',
|
|
1159
|
+
modalTitleCompleted: 'Goal Completed: Plan & Results',
|
|
639
1160
|
time: 'Time',
|
|
640
|
-
milestones: '
|
|
641
|
-
noMilestones: 'Agent
|
|
1161
|
+
milestones: 'Plan of work:',
|
|
1162
|
+
noMilestones: 'Agent is preparing the plan of work...',
|
|
642
1163
|
close: 'Close',
|
|
643
1164
|
pluginTitle: 'Goal Mode & Autonomous Loop',
|
|
644
|
-
pluginDesc: '
|
|
1165
|
+
pluginDesc: 'Goal banner above composer dock, milestone decomposition, and auto-drive',
|
|
645
1166
|
maxIterLabel: 'Max iterations (Safety Limit):',
|
|
1167
|
+
autoDriveLabel: 'Auto-drive: keep the loop running automatically',
|
|
1168
|
+
soundLabel: 'Sound when a goal completes',
|
|
1169
|
+
save: 'Save',
|
|
1170
|
+
saving: 'Saving…',
|
|
1171
|
+
discard: 'Discard changes',
|
|
1172
|
+
resetField: 'Default',
|
|
1173
|
+
overridden: 'overridden',
|
|
1174
|
+
saveFailed: 'Not saved — fix the values and try again',
|
|
1175
|
+
loading: 'Loading settings…',
|
|
1176
|
+
unavailable: 'Settings unavailable',
|
|
646
1177
|
},
|
|
647
1178
|
};
|
|
648
1179
|
|
|
649
|
-
module.exports.inject = ['slots', 'locale'];
|
|
1180
|
+
module.exports.inject = ['slots', 'locale', 'settingsScope'];
|
|
650
1181
|
module.exports.apply = function apply(ctx) {
|
|
651
1182
|
// Регистрация локализации
|
|
652
1183
|
try {
|
|
653
1184
|
ctx.locale?.register?.(NS, LOCALES);
|
|
654
1185
|
} catch (_) {}
|
|
655
1186
|
|
|
656
|
-
//
|
|
657
|
-
|
|
1187
|
+
// Пространство настроек плагина
|
|
1188
|
+
const settingsScope =
|
|
1189
|
+
ctx.settingsScope && typeof ctx.settingsScope.bind === 'function'
|
|
1190
|
+
? ctx.settingsScope.bind({ namespace: NS })
|
|
1191
|
+
: null;
|
|
1192
|
+
|
|
1193
|
+
const registerSlotSafe = (name, entry, comp) => {
|
|
1194
|
+
try {
|
|
1195
|
+
if (typeof ctx.slots?.inject === 'function') {
|
|
1196
|
+
ctx.slots.inject(name, () => {
|
|
1197
|
+
try {
|
|
1198
|
+
return ctx.slots.register(entry, comp);
|
|
1199
|
+
} catch (_) {}
|
|
1200
|
+
});
|
|
1201
|
+
} else if (typeof ctx.slots?.register === 'function') {
|
|
1202
|
+
ctx.slots.register(entry, comp);
|
|
1203
|
+
}
|
|
1204
|
+
} catch (_) {}
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
// 1. Регистрация виджета цели над полем ввода (composer dock)
|
|
1208
|
+
registerSlotSafe(
|
|
1209
|
+
'conversation.input.dock',
|
|
658
1210
|
{
|
|
659
|
-
name: '
|
|
1211
|
+
name: 'conversation.input.dock',
|
|
660
1212
|
id: '@goodandready/dsh-goal',
|
|
661
1213
|
order: 10,
|
|
662
1214
|
locale: NS,
|
|
@@ -665,25 +1217,14 @@ window.__ModuleLoader__.load({
|
|
|
665
1217
|
GoalTopBanner,
|
|
666
1218
|
);
|
|
667
1219
|
|
|
668
|
-
// Фоллбек: также регистрируем в chat.banner
|
|
669
|
-
ctx.slots?.register?.(
|
|
670
|
-
{
|
|
671
|
-
name: 'chat.banner',
|
|
672
|
-
id: '@goodandready/dsh-goal:banner',
|
|
673
|
-
order: 5,
|
|
674
|
-
locale: NS,
|
|
675
|
-
inject: () => ({ ctx }),
|
|
676
|
-
},
|
|
677
|
-
GoalTopBanner,
|
|
678
|
-
);
|
|
679
|
-
|
|
680
1220
|
// 2. Регистрация карточки настроек
|
|
681
|
-
|
|
1221
|
+
registerSlotSafe(
|
|
1222
|
+
'settings.plugin.item',
|
|
682
1223
|
{
|
|
683
1224
|
name: 'settings.plugin.item',
|
|
684
1225
|
key: NS,
|
|
685
1226
|
locale: NS,
|
|
686
|
-
inject: () => ({ ctx }),
|
|
1227
|
+
inject: () => ({ ctx, scope: settingsScope }),
|
|
687
1228
|
},
|
|
688
1229
|
GoalSettingsCard,
|
|
689
1230
|
);
|