@restregular/dsh-notepad 0.3.1 → 0.5.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 +50 -22
- package/client.js +1013 -48
- package/index.js +243 -9
- package/lib/image-store.js +50 -1
- package/lib/note-store.js +76 -9
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +2 -2
package/client.js
CHANGED
|
@@ -37,7 +37,7 @@ window.__ModuleLoader__.load({
|
|
|
37
37
|
factory: (require) => {
|
|
38
38
|
const React = require('react');
|
|
39
39
|
const h = React.createElement;
|
|
40
|
-
const { useState, useEffect, useRef } = React;
|
|
40
|
+
const { useState, useEffect, useLayoutEffect, useRef } = React;
|
|
41
41
|
|
|
42
42
|
const NS = 'dsh-notepad';
|
|
43
43
|
const API = '/dsh-notepad/api';
|
|
@@ -50,8 +50,14 @@ window.__ModuleLoader__.load({
|
|
|
50
50
|
const SAVE_DELAY = 500;
|
|
51
51
|
const MAX_SEGMENTS = 200;
|
|
52
52
|
const MAX_SEGMENT_CHARS = 160000;
|
|
53
|
+
const MAX_TITLE_CHARS = 200;
|
|
53
54
|
const MAX_IMAGES = 20;
|
|
54
55
|
const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
56
|
+
const MAX_OPTIMIZE_CHARS = 60000;
|
|
57
|
+
const SIDEBAR_REFRESH_MS = 20000;
|
|
58
|
+
const IMAGE_DELETE_DELAY = 5000;
|
|
59
|
+
const OPTIMIZE_CACHE_LIMIT = 100;
|
|
60
|
+
const EDGE_GAP = 8;
|
|
55
61
|
const IMAGE_MIME = {
|
|
56
62
|
'image/png': 'png',
|
|
57
63
|
'image/jpeg': 'jpg',
|
|
@@ -68,6 +74,9 @@ window.__ModuleLoader__.load({
|
|
|
68
74
|
editor: '分段内容',
|
|
69
75
|
placeholder: '记录开发中遇到的问题,或下一阶段要做的事……',
|
|
70
76
|
segmentPlaceholder: '新分段…',
|
|
77
|
+
titlePlaceholder: '分段标题(可选)',
|
|
78
|
+
segmentTitle: '分段标题',
|
|
79
|
+
indexHint: '点击选择此段 · 拖动可排序',
|
|
71
80
|
loading: '读取中…',
|
|
72
81
|
empty: '还没有内容',
|
|
73
82
|
unsaved: '未保存',
|
|
@@ -102,6 +111,25 @@ window.__ModuleLoader__.load({
|
|
|
102
111
|
selectHint: '勾选分段可批量操作',
|
|
103
112
|
selected: '已选 {n} 段',
|
|
104
113
|
selectSegment: '选择此段',
|
|
114
|
+
merge: '合并分段',
|
|
115
|
+
mergeSelected: '合并选中的 {n} 段',
|
|
116
|
+
mergeHint: '至少选择 2 段才能合并',
|
|
117
|
+
mergedToast: '已合并 {n} 段',
|
|
118
|
+
optimizeSegment: 'AI 优化此段',
|
|
119
|
+
optimizeSelected: 'AI 优化选中的 {n} 段',
|
|
120
|
+
optimizeSelectHint: '勾选分段后可批量优化',
|
|
121
|
+
optimizeAllProgress: 'AI 优化中 {done}/{total}',
|
|
122
|
+
optimizing: '优化中…',
|
|
123
|
+
undoOptimize: '撤销优化',
|
|
124
|
+
optimizedToast: '已优化此段',
|
|
125
|
+
optimizedCount: '已优化 {n} 段',
|
|
126
|
+
optimizeUndone: '已撤销优化',
|
|
127
|
+
optimizeEmpty: '没有可优化的文字',
|
|
128
|
+
optimizeTooLong: '内容太长,无法优化',
|
|
129
|
+
optimizeUnavailable: 'AI 服务不可用',
|
|
130
|
+
optimizeFailed: '优化失败',
|
|
131
|
+
dragSegment: '拖动排序',
|
|
132
|
+
unsent: '{n} 条未发送',
|
|
105
133
|
archive: '归档',
|
|
106
134
|
archiveSegment: '归档此段',
|
|
107
135
|
archiveSelected: '归档选中的 {n} 段',
|
|
@@ -132,9 +160,10 @@ window.__ModuleLoader__.load({
|
|
|
132
160
|
show: '显示记事本(Ctrl+Shift+N)',
|
|
133
161
|
chars: '{n} 字符',
|
|
134
162
|
segments: '{n} 段',
|
|
135
|
-
hint: 'Enter 新建分段 · Shift+Enter 换行 · Ctrl+Enter 插入 · 支持粘贴图片',
|
|
163
|
+
hint: 'Enter 新建分段 · Shift+Enter 换行 · Ctrl+Enter 插入 · Ctrl+Shift+0 复位浮窗 · 支持粘贴图片',
|
|
136
164
|
workspace: '工作区',
|
|
137
165
|
resize: '拖动调整大小',
|
|
166
|
+
uiReset: '已复位浮窗位置和尺寸',
|
|
138
167
|
addImage: '添加图片',
|
|
139
168
|
removeImage: '移除图片',
|
|
140
169
|
openImage: '打开图片',
|
|
@@ -153,6 +182,9 @@ window.__ModuleLoader__.load({
|
|
|
153
182
|
editor: 'Segment content',
|
|
154
183
|
placeholder: 'Jot down problems, or what to build next…',
|
|
155
184
|
segmentPlaceholder: 'New segment…',
|
|
185
|
+
titlePlaceholder: 'Segment title (optional)',
|
|
186
|
+
segmentTitle: 'Segment title',
|
|
187
|
+
indexHint: 'Click to select · drag to reorder',
|
|
156
188
|
loading: 'Loading…',
|
|
157
189
|
empty: 'Nothing here yet',
|
|
158
190
|
unsaved: 'Unsaved',
|
|
@@ -187,6 +219,25 @@ window.__ModuleLoader__.load({
|
|
|
187
219
|
selectHint: 'Check segments to batch',
|
|
188
220
|
selected: '{n} selected',
|
|
189
221
|
selectSegment: 'Select this segment',
|
|
222
|
+
merge: 'Merge segments',
|
|
223
|
+
mergeSelected: 'Merge {n} selected',
|
|
224
|
+
mergeHint: 'Select at least 2 segments to merge',
|
|
225
|
+
mergedToast: 'Merged {n} segments',
|
|
226
|
+
optimizeSegment: 'Improve this segment with AI',
|
|
227
|
+
optimizeSelected: 'Improve {n} selected with AI',
|
|
228
|
+
optimizeSelectHint: 'Select segments to improve',
|
|
229
|
+
optimizeAllProgress: 'Improving {done}/{total}',
|
|
230
|
+
optimizing: 'Improving…',
|
|
231
|
+
undoOptimize: 'Undo improvement',
|
|
232
|
+
optimizedToast: 'Segment improved',
|
|
233
|
+
optimizedCount: 'Improved {n} segments',
|
|
234
|
+
optimizeUndone: 'Improvement undone',
|
|
235
|
+
optimizeEmpty: 'Nothing to improve',
|
|
236
|
+
optimizeTooLong: 'Too long to improve',
|
|
237
|
+
optimizeUnavailable: 'AI service unavailable',
|
|
238
|
+
optimizeFailed: 'Improve failed',
|
|
239
|
+
dragSegment: 'Drag to reorder',
|
|
240
|
+
unsent: '{n} unsent',
|
|
190
241
|
archive: 'Archive',
|
|
191
242
|
archiveSegment: 'Archive this segment',
|
|
192
243
|
archiveSelected: 'Archive {n} selected',
|
|
@@ -217,9 +268,10 @@ window.__ModuleLoader__.load({
|
|
|
217
268
|
show: 'Show notepad (Ctrl+Shift+N)',
|
|
218
269
|
chars: '{n} chars',
|
|
219
270
|
segments: '{n} segments',
|
|
220
|
-
hint: 'Enter new segment · Shift+Enter newline · Ctrl+Enter insert · Paste images',
|
|
271
|
+
hint: 'Enter new segment · Shift+Enter newline · Ctrl+Enter insert · Ctrl+Shift+0 reset window · Paste images',
|
|
221
272
|
workspace: 'Workspace',
|
|
222
273
|
resize: 'Drag to resize',
|
|
274
|
+
uiReset: 'Window position and size reset',
|
|
223
275
|
addImage: 'Add image',
|
|
224
276
|
removeImage: 'Remove image',
|
|
225
277
|
openImage: 'Open image',
|
|
@@ -488,6 +540,60 @@ window.__ModuleLoader__.load({
|
|
|
488
540
|
);
|
|
489
541
|
}
|
|
490
542
|
|
|
543
|
+
/** Four-point sparkle: an AI improvement action. */
|
|
544
|
+
function SparkleIcon({ size = 16 }) {
|
|
545
|
+
return h(
|
|
546
|
+
Svg,
|
|
547
|
+
{ size, viewBox: '0 0 16 16' },
|
|
548
|
+
h('path', {
|
|
549
|
+
d: 'M8 1.8c.5 2.7 1.5 3.7 4.2 4.2-2.7.5-3.7 1.5-4.2 4.2-.5-2.7-1.5-3.7-4.2-4.2C6.5 5.5 7.5 4.5 8 1.8Z',
|
|
550
|
+
stroke: 'currentColor', strokeWidth: 1.2,
|
|
551
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
552
|
+
}),
|
|
553
|
+
h('path', {
|
|
554
|
+
d: 'M12.4 9.6c.3 1.5.8 2 2.3 2.3-1.5.3-2 .8-2.3 2.3-.3-1.5-.8-2-2.3-2.3 1.5-.3 2-.8 2.3-2.3Z',
|
|
555
|
+
stroke: 'currentColor', strokeWidth: 1.1,
|
|
556
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
557
|
+
}),
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** Curved arrow: undo the last AI improvement. */
|
|
562
|
+
function UndoIcon({ size = 16 }) {
|
|
563
|
+
return h(
|
|
564
|
+
Svg,
|
|
565
|
+
{ size, viewBox: '0 0 16 16' },
|
|
566
|
+
h('path', {
|
|
567
|
+
d: 'M5.4 4.2 2.8 6.8l2.6 2.6',
|
|
568
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
569
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
570
|
+
}),
|
|
571
|
+
h('path', {
|
|
572
|
+
d: 'M3 6.8h5.3c2.7 0 4.9 2.2 4.9 4.9',
|
|
573
|
+
stroke: 'currentColor', strokeWidth: 1.35,
|
|
574
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
575
|
+
}),
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** Two arrows converging: merge selected segments into one. */
|
|
580
|
+
function MergeIcon({ size = 16 }) {
|
|
581
|
+
return h(
|
|
582
|
+
Svg,
|
|
583
|
+
{ size, viewBox: '0 0 16 16' },
|
|
584
|
+
h('path', {
|
|
585
|
+
d: 'M3.2 3.4v2.2c0 1.4 1.1 2.5 2.5 2.5h4.6c1.4 0 2.5 1.1 2.5 2.5v2.2',
|
|
586
|
+
stroke: 'currentColor', strokeWidth: 1.3,
|
|
587
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
588
|
+
}),
|
|
589
|
+
h('path', {
|
|
590
|
+
d: 'M3.2 12.6v-2.2c0-1.4 1.1-2.5 2.5-2.5h4.6c1.4 0 2.5-1.1 2.5-2.5V3.4',
|
|
591
|
+
stroke: 'currentColor', strokeWidth: 1.3,
|
|
592
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
593
|
+
}),
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
491
597
|
/** Arrow towards the bottom-right corner: minimize to the corner icon. */
|
|
492
598
|
function MinimizeIcon({ size = 16 }) {
|
|
493
599
|
return h(
|
|
@@ -530,10 +636,10 @@ window.__ModuleLoader__.load({
|
|
|
530
636
|
.dsh-np-btn[data-accent=true]{background:var(--np-accent);color:var(--np-accent-fg)}
|
|
531
637
|
.dsh-np-btn[data-accent=true]:hover:not(:disabled){background:color-mix(in srgb,var(--np-accent) 88%,#000);color:var(--np-accent-fg)}
|
|
532
638
|
.dsh-np-btn:disabled{opacity:.38;cursor:default;background:transparent;color:var(--dsw-alias-label-tertiary,#6b7280)}
|
|
533
|
-
.dsh-np-btn:focus-visible,.dsh-np-segbtn:focus-visible,.dsh-np-link:focus-visible,.dsh-np-empty-add:focus-visible,.dsh-np-toast-action:focus-visible,.dsh-np-selectall input:focus-visible,.dsh-np-check input:focus-visible,.dsh-np-restore:focus-visible,.dsh-np-img-open:focus-visible,.dsh-np-img-remove:focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary,#4176e6));outline-offset:-2px}
|
|
639
|
+
.dsh-np-btn:focus-visible,.dsh-np-segbtn:focus-visible,.dsh-np-link:focus-visible,.dsh-np-empty-add:focus-visible,.dsh-np-toast-action:focus-visible,.dsh-np-selectall input:focus-visible,.dsh-np-check input:focus-visible,.dsh-np-title-add:focus-visible,.dsh-np-restore:focus-visible,.dsh-np-img-open:focus-visible,.dsh-np-img-remove:focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary,#4176e6));outline-offset:-2px}
|
|
534
640
|
.dsh-np-body{display:flex;flex:1 1 auto;flex-direction:column;min-height:0;padding:8px 10px 0;opacity:1;transition:opacity .16s ease,visibility 0s linear 0s}
|
|
535
641
|
.dsh-np-card[data-minimized=true] .dsh-np-body{opacity:0;visibility:hidden;pointer-events:none;transition:opacity .16s ease,visibility 0s linear .16s}
|
|
536
|
-
.dsh-np-tools{display:flex;align-items:center;gap:4px;flex:none;padding:0 2px 7px;border-bottom:1px solid var(--dsw-alias-border-l1,rgb(15 23 42/.08))}
|
|
642
|
+
.dsh-np-tools{display:flex;align-items:center;flex-wrap:wrap;gap:4px;flex:none;padding:0 2px 7px;border-bottom:1px solid var(--dsw-alias-border-l1,rgb(15 23 42/.08))}
|
|
537
643
|
.dsh-np-selectall{display:inline-flex;align-items:center;gap:6px;min-width:0;cursor:pointer;color:var(--dsw-alias-label-tertiary,#6b7280);font-size:11px;white-space:nowrap}
|
|
538
644
|
.dsh-np-selectall input{width:14px;height:14px;margin:0;accent-color:var(--np-accent);cursor:pointer;flex:none}
|
|
539
645
|
.dsh-np-selectall input:disabled{cursor:default;opacity:.45}
|
|
@@ -543,12 +649,24 @@ window.__ModuleLoader__.load({
|
|
|
543
649
|
.dsh-np-seg[data-selected=true]{border-color:var(--np-accent);background:color-mix(in srgb,var(--np-accent) 6%,var(--np-surface-2));box-shadow:0 0 0 1px var(--np-accent) inset}
|
|
544
650
|
.dsh-np-seg[data-archived=true]{border-style:dashed;background:transparent;opacity:.82;animation:none}
|
|
545
651
|
.dsh-np-seg-head{display:flex;align-items:center;gap:3px;flex:none;min-height:30px;padding:3px 5px 0 7px;box-sizing:border-box}
|
|
652
|
+
.dsh-np-seg[data-reorder-drag=true]{opacity:.45}
|
|
653
|
+
.dsh-np-seg[data-drop-before=true]{box-shadow:0 -2px 0 0 var(--np-accent)}
|
|
654
|
+
.dsh-np-seg[data-drop-after=true]{box-shadow:0 2px 0 0 var(--np-accent)}
|
|
546
655
|
.dsh-np-check{display:inline-flex;align-items:center;justify-content:center;flex:none;width:20px;height:20px;cursor:pointer}
|
|
547
656
|
.dsh-np-check input{width:14px;height:14px;margin:0;accent-color:var(--np-accent);cursor:pointer}
|
|
548
|
-
.dsh-np-index{flex:none;color:var(--dsw-alias-label-caption,#9aa3b2);font-size:10px;font-weight:
|
|
657
|
+
.dsh-np-index{flex:none;display:inline-flex;align-items:center;justify-content:center;min-width:22px;height:20px;padding:0 2px;border:0;border-radius:5px;background:transparent;color:var(--dsw-alias-label-caption,#9aa3b2);font:inherit;font-size:10px;font-weight:700;letter-spacing:.04em;font-variant-numeric:tabular-nums;cursor:pointer;user-select:none}
|
|
658
|
+
.dsh-np-index:hover{background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06));color:var(--dsw-alias-label-primary,#1f2430)}
|
|
659
|
+
.dsh-np-index[aria-pressed=true]{background:color-mix(in srgb,var(--np-accent) 14%,transparent);color:var(--np-accent)}
|
|
660
|
+
.dsh-np-title-input{flex:1 1 auto;min-width:0;width:0;height:20px;padding:0 4px;border:0;border-radius:5px;background:transparent;color:var(--dsw-alias-label-secondary,#4b5563);font:inherit;font-size:11px;font-weight:600;line-height:20px;outline:0}
|
|
661
|
+
.dsh-np-title-input::placeholder{color:var(--dsw-alias-label-caption,#9aa3b2);font-weight:400}
|
|
662
|
+
.dsh-np-title-input:hover{background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06))}
|
|
663
|
+
.dsh-np-title-input:focus{background:var(--np-surface);box-shadow:0 0 0 1px var(--np-accent) inset;color:var(--dsw-alias-label-primary,#1f2430)}
|
|
664
|
+
.dsh-np-title-add{flex:1 1 auto;min-width:0;width:0;height:20px;padding:0 4px;border:0;border-radius:5px;background:transparent;color:var(--dsw-alias-label-caption,#9aa3b2);font:inherit;font-size:11px;font-weight:400;line-height:20px;text-align:left;cursor:text;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;opacity:.75}
|
|
665
|
+
.dsh-np-seg-title{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary,#4b5563);font-size:11px;font-weight:600}
|
|
549
666
|
.dsh-np-archived-chip{flex:none;padding:0 6px;border-radius:5px;background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06));color:var(--dsw-alias-label-tertiary,#6b7280);font-size:10px;line-height:16px}
|
|
550
667
|
.dsh-np-segbtn{flex:none;display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary,#6b7280);cursor:pointer;transition:background .12s,color .12s,opacity .12s;opacity:.8}
|
|
551
668
|
.dsh-np-segbtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06));color:var(--dsw-alias-label-primary,#1f2430);opacity:1}
|
|
669
|
+
.dsh-np-segbtn[data-ai=true]{color:var(--np-accent);opacity:1}
|
|
552
670
|
.dsh-np-segbtn[data-danger=true]:hover:not(:disabled){color:var(--dsw-alias-state-error-primary,#e25555);background:var(--dsw-alias-interactive-bg-hover-danger,rgb(236 19 19/.05))}
|
|
553
671
|
.dsh-np-segbtn:disabled{opacity:.3;cursor:default}
|
|
554
672
|
.dsh-np-seg-text{display:block;width:100%;box-sizing:border-box;padding:2px 10px 8px;border:0;outline:0;resize:none;background:transparent;color:inherit;font-family:${FONT_STACK};font-size:13px;line-height:1.6;min-height:26px;max-height:240px;overflow-y:auto;white-space:pre-wrap;overflow-wrap:anywhere;tab-size:2}
|
|
@@ -566,6 +684,7 @@ window.__ModuleLoader__.load({
|
|
|
566
684
|
.dsh-np-img-remove{display:inline-flex;align-items:center;justify-content:center;flex:none;width:17px;height:17px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--dsw-alias-label-caption,#9aa3b2);cursor:pointer}
|
|
567
685
|
.dsh-np-img-remove:hover{background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06));color:var(--dsw-alias-state-error-primary,#e25555)}
|
|
568
686
|
.dsh-np-img-spinner{flex:none;width:14px;height:14px;border:2px solid color-mix(in srgb,var(--np-accent) 30%,transparent);border-top-color:var(--np-accent);border-radius:50%;animation:dsh-np-spin .8s linear infinite}
|
|
687
|
+
.dsh-np-mini-spinner{flex:none;width:12px;height:12px;border:1.8px solid color-mix(in srgb,var(--np-accent) 30%,transparent);border-top-color:var(--np-accent);border-radius:50%;animation:dsh-np-spin .8s linear infinite}
|
|
569
688
|
.dsh-np-seg-archived-text{padding:2px 10px 9px;color:var(--dsw-alias-label-secondary,#4b5563);font-size:13px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
570
689
|
.dsh-np-seg[data-archived=true] .dsh-np-medias{padding:0 10px 8px}
|
|
571
690
|
.dsh-np-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;flex:none;padding:22px 12px;text-align:center;color:var(--dsw-alias-label-tertiary,#6b7280);font-size:12px}
|
|
@@ -597,6 +716,9 @@ window.__ModuleLoader__.load({
|
|
|
597
716
|
.dsh-np-restore{position:fixed;right:16px;bottom:16px;z-index:30;display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;padding:0;border:1px solid var(--dsw-alias-border-l2,rgb(15 23 42/.12));border-radius:10px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-secondary,#4b5563);box-shadow:var(--dsw-elevation-lv2,0 8px 24px rgb(15 23 42/.16));cursor:pointer;opacity:0;visibility:hidden;pointer-events:none;transform:translateY(6px) scale(.9);transition:opacity .18s ease,transform .18s ease,visibility 0s linear .18s}
|
|
598
717
|
.dsh-np-restore[data-visible=true]{opacity:.9;visibility:visible;pointer-events:auto;transform:none;transition:opacity .18s ease,transform .18s ease,visibility 0s linear 0s}
|
|
599
718
|
.dsh-np-restore[data-visible=true]:hover{opacity:1;transform:translateY(-1px)}
|
|
719
|
+
.dsh-np-restore-badge{position:absolute;top:-5px;right:-5px;min-width:16px;height:16px;padding:0 4px;box-sizing:border-box;border-radius:9px;background:var(--dsw-alias-state-business-primary,#4176e6);color:#fff;font-size:9.5px;font-weight:700;line-height:16px;text-align:center;font-variant-numeric:tabular-nums;box-shadow:0 0 0 1.5px var(--dsw-specific-sidebar-fill,var(--dsw-alias-bg-layer-1,#fff))}
|
|
720
|
+
[data-row-key^="workspace:"][data-np-unsent]{position:relative}
|
|
721
|
+
[data-row-key^="workspace:"][data-np-unsent]::after{content:attr(data-np-unsent);position:absolute;top:-4px;left:1px;z-index:2;min-width:15px;height:15px;padding:0 4px;box-sizing:border-box;border-radius:8px;background:var(--dsw-alias-state-business-primary,#4176e6);color:#fff;font-size:9.5px;font-weight:700;line-height:15px;text-align:center;font-variant-numeric:tabular-nums;pointer-events:none;box-shadow:0 0 0 1.5px var(--dsw-specific-sidebar-fill,var(--dsw-alias-bg-layer-1,#fff))}
|
|
600
722
|
@keyframes dsh-np-pulse{0%,100%{opacity:1}50%{opacity:.35}}
|
|
601
723
|
@keyframes dsh-np-rise{from{opacity:0;transform:translate(-50%,4px)}to{opacity:1;transform:translate(-50%,0)}}
|
|
602
724
|
@keyframes dsh-np-item-in{from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:none}}
|
|
@@ -605,7 +727,7 @@ body[data-ds-dark-theme] .dsh-np-card{--np-accent-fg:#0f1115;box-shadow:0 18px 4
|
|
|
605
727
|
body[data-ds-dark-theme] .dsh-np-seg[data-selected=true]{background:color-mix(in srgb,var(--np-accent) 14%,var(--dsw-alias-bg-layer-2,#2c2c2e))}
|
|
606
728
|
body[data-ds-dark-theme] .dsh-np-btn[data-accent=true]:hover:not(:disabled){background:color-mix(in srgb,var(--np-accent) 84%,#fff)}
|
|
607
729
|
body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
608
|
-
@media (prefers-reduced-motion:reduce){.dsh-np-card,.dsh-np-body,.dsh-np-archive,.dsh-np-restore,.dsh-np-seg,.dsh-np-toast{transition:none!important;animation:none!important}.dsh-np-dot[data-state=saving]{animation:none}.dsh-np-img-spinner{animation:none}}`;
|
|
730
|
+
@media (prefers-reduced-motion:reduce){.dsh-np-card,.dsh-np-body,.dsh-np-archive,.dsh-np-restore,.dsh-np-seg,.dsh-np-toast{transition:none!important;animation:none!important}.dsh-np-dot[data-state=saving]{animation:none}.dsh-np-img-spinner,.dsh-np-mini-spinner{animation:none}}`;
|
|
609
731
|
|
|
610
732
|
// ------------------------------------------------------------------
|
|
611
733
|
// Helpers
|
|
@@ -625,14 +747,38 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
625
747
|
return `seg-${Date.now().toString(36)}-${segmentSequence.toString(36)}`;
|
|
626
748
|
}
|
|
627
749
|
|
|
628
|
-
function
|
|
629
|
-
|
|
750
|
+
function nowIso() {
|
|
751
|
+
try {
|
|
752
|
+
return new Date().toISOString();
|
|
753
|
+
} catch {
|
|
754
|
+
return '';
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function newSegment(text, options = {}) {
|
|
759
|
+
return {
|
|
760
|
+
id: newSegmentId(),
|
|
761
|
+
text,
|
|
762
|
+
title: typeof options.title === 'string' ? options.title.slice(0, MAX_TITLE_CHARS) : '',
|
|
763
|
+
createdAt: typeof options.createdAt === 'string' && options.createdAt !== ''
|
|
764
|
+
? options.createdAt
|
|
765
|
+
: nowIso(),
|
|
766
|
+
images: [],
|
|
767
|
+
archived: false,
|
|
768
|
+
archivedAt: null,
|
|
769
|
+
};
|
|
630
770
|
}
|
|
631
771
|
|
|
632
772
|
function serializeSegments(segments) {
|
|
633
773
|
return segments.map((segment) => ({
|
|
634
774
|
id: segment.id,
|
|
635
775
|
text: segment.text,
|
|
776
|
+
...(typeof segment.title === 'string' && segment.title !== ''
|
|
777
|
+
? { title: segment.title }
|
|
778
|
+
: {}),
|
|
779
|
+
...(typeof segment.createdAt === 'string' && segment.createdAt !== ''
|
|
780
|
+
? { createdAt: segment.createdAt }
|
|
781
|
+
: {}),
|
|
636
782
|
...(Array.isArray(segment.images) && segment.images.length > 0
|
|
637
783
|
? { images: segment.images.map((image) => ({ path: image.path, name: image.name })) }
|
|
638
784
|
: {}),
|
|
@@ -654,6 +800,8 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
654
800
|
out.push({
|
|
655
801
|
id,
|
|
656
802
|
text: item.text,
|
|
803
|
+
title: typeof item.title === 'string' ? item.title.slice(0, MAX_TITLE_CHARS) : '',
|
|
804
|
+
createdAt: typeof item.createdAt === 'string' && item.createdAt !== '' ? item.createdAt : null,
|
|
657
805
|
images: normalizeImages(item.images),
|
|
658
806
|
archived,
|
|
659
807
|
archivedAt: archived && typeof item.archivedAt === 'string' ? item.archivedAt : null,
|
|
@@ -737,6 +885,46 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
737
885
|
.join('\n\n');
|
|
738
886
|
}
|
|
739
887
|
|
|
888
|
+
/** Whether a segment still counts as unsent work (active + has content). */
|
|
889
|
+
function isUnsent(segment) {
|
|
890
|
+
if (segment === null || segment === undefined || segment.archived === true) return false;
|
|
891
|
+
if (typeof segment.text === 'string' && segment.text.trim() !== '') return true;
|
|
892
|
+
return Array.isArray(segment.images) && segment.images.length > 0;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function countUnsent(segments) {
|
|
896
|
+
let count = 0;
|
|
897
|
+
for (const segment of segments) {
|
|
898
|
+
if (isUnsent(segment)) count += 1;
|
|
899
|
+
}
|
|
900
|
+
return count;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/** Whether a whole server payload predates per-segment creation times. */
|
|
904
|
+
function isLegacyPayload(data) {
|
|
905
|
+
const segments = Array.isArray(data?.segments) ? data.segments : [];
|
|
906
|
+
return segments.length > 0 && segments.every((segment) =>
|
|
907
|
+
segment !== null && typeof segment === 'object'
|
|
908
|
+
&& (typeof segment.createdAt !== 'string' || segment.createdAt === ''));
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Adopt server content without moving cards the user is looking at.
|
|
913
|
+
* Known ids keep the local order; cards created elsewhere land on top.
|
|
914
|
+
*/
|
|
915
|
+
function mergeServerOrder(current, incoming) {
|
|
916
|
+
const byId = new Map();
|
|
917
|
+
for (const segment of incoming) byId.set(segment.id, segment);
|
|
918
|
+
const kept = [];
|
|
919
|
+
for (const segment of current) {
|
|
920
|
+
const next = byId.get(segment.id);
|
|
921
|
+
if (next !== undefined) kept.push(next);
|
|
922
|
+
}
|
|
923
|
+
const known = new Set(kept.map((segment) => segment.id));
|
|
924
|
+
const added = incoming.filter((segment) => !known.has(segment.id));
|
|
925
|
+
return [...added, ...kept];
|
|
926
|
+
}
|
|
927
|
+
|
|
740
928
|
function isAbsolutePath(value) {
|
|
741
929
|
const text = String(value ?? '');
|
|
742
930
|
return text.startsWith('/') || /^[A-Za-z]:\//u.test(text) || text.startsWith('//');
|
|
@@ -873,6 +1061,43 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
873
1061
|
return parts.length === 0 ? String(root ?? '') : parts[parts.length - 1];
|
|
874
1062
|
}
|
|
875
1063
|
|
|
1064
|
+
/**
|
|
1065
|
+
* Height of the chrome the host draws over the page (the Windows title bar
|
|
1066
|
+
* with maximize / minimize / close, or the macOS traffic lights). The
|
|
1067
|
+
* floating card must never move above that strip. The host exposes it as a
|
|
1068
|
+
* CSS variable on <html>; standalone browsers have none, so this is 0.
|
|
1069
|
+
* @returns {number}
|
|
1070
|
+
*/
|
|
1071
|
+
function hostChromeTop() {
|
|
1072
|
+
try {
|
|
1073
|
+
const root = typeof document !== 'undefined' ? document.documentElement : null;
|
|
1074
|
+
if (root === null || root === undefined) return 0;
|
|
1075
|
+
const hasAttribute = (name) => typeof root.hasAttribute === 'function' && root.hasAttribute(name);
|
|
1076
|
+
const readVar = (name) => {
|
|
1077
|
+
try {
|
|
1078
|
+
if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') return NaN;
|
|
1079
|
+
const style = window.getComputedStyle(root);
|
|
1080
|
+
return Number.parseFloat(style?.getPropertyValue(name) ?? '');
|
|
1081
|
+
} catch {
|
|
1082
|
+
return NaN;
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
// `--dsh-frame-chrome-top` is 0 while the host is in fullscreen.
|
|
1086
|
+
const chrome = readVar('--dsh-frame-chrome-top');
|
|
1087
|
+
if (Number.isFinite(chrome)) return Math.max(0, chrome);
|
|
1088
|
+
const clearance = readVar('--dsh-frame-top-clearance');
|
|
1089
|
+
if (Number.isFinite(clearance) && clearance > 0) return clearance;
|
|
1090
|
+
if (hasAttribute('data-windows-titlebar')) {
|
|
1091
|
+
const titlebar = readVar('--dsh-windows-titlebar-height');
|
|
1092
|
+
return Number.isFinite(titlebar) && titlebar > 0 ? titlebar : 32;
|
|
1093
|
+
}
|
|
1094
|
+
const platform = typeof root.getAttribute === 'function' ? root.getAttribute('data-platform') : '';
|
|
1095
|
+
return platform === 'darwin' ? 48 : 0;
|
|
1096
|
+
} catch {
|
|
1097
|
+
return 0;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
876
1101
|
function readStoredUi() {
|
|
877
1102
|
const base = {
|
|
878
1103
|
x: null,
|
|
@@ -905,17 +1130,18 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
905
1130
|
function clampUi(ui) {
|
|
906
1131
|
const vw = (typeof window !== 'undefined' && window.innerWidth) || 1280;
|
|
907
1132
|
const vh = (typeof window !== 'undefined' && window.innerHeight) || 800;
|
|
908
|
-
const
|
|
909
|
-
const
|
|
1133
|
+
const minTop = hostChromeTop() + EDGE_GAP;
|
|
1134
|
+
const widthLimit = Math.max(240, vw - EDGE_GAP * 2);
|
|
1135
|
+
const heightLimit = Math.max(180, vh - minTop - EDGE_GAP);
|
|
910
1136
|
const width = Math.min(Math.max(MIN_W, Number(ui.width) || DEFAULT_W), widthLimit);
|
|
911
1137
|
const height = Math.min(Math.max(MIN_H, Number(ui.height) || DEFAULT_H), heightLimit);
|
|
912
1138
|
const displayHeight = ui.minimized ? HEAD_H : height;
|
|
913
1139
|
const x = ui.x === null || ui.x === undefined
|
|
914
|
-
? vw - width - 24
|
|
915
|
-
: Math.min(Math.max(
|
|
1140
|
+
? Math.max(EDGE_GAP, vw - width - 24)
|
|
1141
|
+
: Math.min(Math.max(EDGE_GAP, ui.x), Math.max(EDGE_GAP, vw - width - EDGE_GAP));
|
|
916
1142
|
const y = ui.y === null || ui.y === undefined
|
|
917
|
-
? vh - displayHeight - 24
|
|
918
|
-
: Math.min(Math.max(
|
|
1143
|
+
? Math.max(minTop, vh - displayHeight - 24)
|
|
1144
|
+
: Math.min(Math.max(minTop, ui.y), Math.max(minTop, vh - displayHeight - EDGE_GAP));
|
|
919
1145
|
return { ...ui, x, y, width, height };
|
|
920
1146
|
}
|
|
921
1147
|
|
|
@@ -950,6 +1176,32 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
950
1176
|
return readResponse(response);
|
|
951
1177
|
}
|
|
952
1178
|
|
|
1179
|
+
async function optimizeNoteText(root, sessionId, text, title) {
|
|
1180
|
+
const response = await fetch(`${API}/optimize`, {
|
|
1181
|
+
method: 'POST',
|
|
1182
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1183
|
+
body: JSON.stringify({ root, sessionId, text, title }),
|
|
1184
|
+
});
|
|
1185
|
+
return readResponse(response);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
async function removeStoredImage(root, sessionId, pathValue) {
|
|
1189
|
+
const response = await fetch(`${API}/image`, {
|
|
1190
|
+
method: 'DELETE',
|
|
1191
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1192
|
+
body: JSON.stringify({ root, sessionId, path: pathValue }),
|
|
1193
|
+
});
|
|
1194
|
+
return readResponse(response);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
async function fetchUnsentCounts(sessionId) {
|
|
1198
|
+
const response = await fetch(`${API}/unsent?sessionId=${encodeURIComponent(sessionId)}`, {
|
|
1199
|
+
method: 'GET',
|
|
1200
|
+
cache: 'no-store',
|
|
1201
|
+
});
|
|
1202
|
+
return readResponse(response);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
953
1205
|
async function copyText(value) {
|
|
954
1206
|
try {
|
|
955
1207
|
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
|
@@ -1016,11 +1268,20 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1016
1268
|
const [error, setError] = useState('');
|
|
1017
1269
|
const [savedAt, setSavedAt] = useState(null);
|
|
1018
1270
|
const [toast, setToast] = useState(null);
|
|
1019
|
-
const [ui, setUi] = useState(readStoredUi);
|
|
1271
|
+
const [ui, setUi] = useState(() => clampUi(readStoredUi()));
|
|
1020
1272
|
const [sending, setSending] = useState(false);
|
|
1021
1273
|
const [resizing, setResizing] = useState(false);
|
|
1022
1274
|
const [uploading, setUploading] = useState({});
|
|
1023
1275
|
const [dragSegment, setDragSegment] = useState('');
|
|
1276
|
+
const [reorderDrag, setReorderDrag] = useState('');
|
|
1277
|
+
const [reorderOver, setReorderOver] = useState(null);
|
|
1278
|
+
const [optimizing, setOptimizing] = useState(() => new Set());
|
|
1279
|
+
const [optimizeBatch, setOptimizeBatch] = useState(null);
|
|
1280
|
+
const [segmentUndo, setSegmentUndo] = useState(() => new Map());
|
|
1281
|
+
const [batchUndo, setBatchUndo] = useState(null);
|
|
1282
|
+
const [titleEditing, setTitleEditing] = useState('');
|
|
1283
|
+
const [wsCounts, setWsCounts] = useState({});
|
|
1284
|
+
const [currentWsId, setCurrentWsId] = useState('');
|
|
1024
1285
|
|
|
1025
1286
|
const bindingRef = useRef(null);
|
|
1026
1287
|
const rootSessionRef = useRef('');
|
|
@@ -1044,7 +1305,16 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1044
1305
|
const fileInputRefs = useRef(new Map());
|
|
1045
1306
|
const listRef = useRef(null);
|
|
1046
1307
|
const pendingFocusRef = useRef(null);
|
|
1308
|
+
const pendingScrollRef = useRef(null);
|
|
1309
|
+
const refreshSeqRef = useRef(0);
|
|
1047
1310
|
const undoRef = useRef(null);
|
|
1311
|
+
const optimizingRef = useRef(new Set());
|
|
1312
|
+
const optimizeBatchRef = useRef(false);
|
|
1313
|
+
const optimizeCacheRef = useRef(new Map());
|
|
1314
|
+
const batchUndoRef = useRef(null);
|
|
1315
|
+
const reorderDragRef = useRef('');
|
|
1316
|
+
const reorderOverRef = useRef(null);
|
|
1317
|
+
const imageDeleteTimersRef = useRef(new Map());
|
|
1048
1318
|
|
|
1049
1319
|
const setSelection = (next) => {
|
|
1050
1320
|
selectedRef.current = next;
|
|
@@ -1063,6 +1333,17 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1063
1333
|
if (changed) setSelection(pruned);
|
|
1064
1334
|
};
|
|
1065
1335
|
|
|
1336
|
+
/** Keep the ref and the state in sync for the batch-optimize undo slot. */
|
|
1337
|
+
const setBatchUndoState = (value) => {
|
|
1338
|
+
batchUndoRef.current = value;
|
|
1339
|
+
setBatchUndo(value);
|
|
1340
|
+
};
|
|
1341
|
+
|
|
1342
|
+
/** Any local edit invalidates the "undo the last batch" offer. */
|
|
1343
|
+
const invalidateBatchUndo = () => {
|
|
1344
|
+
if (batchUndoRef.current !== null) setBatchUndoState(null);
|
|
1345
|
+
};
|
|
1346
|
+
|
|
1066
1347
|
// Session + workspace -------------------------------------------------
|
|
1067
1348
|
useEffect(() => {
|
|
1068
1349
|
let disposed = false;
|
|
@@ -1182,6 +1463,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1182
1463
|
saveNowRef.current = saveNow;
|
|
1183
1464
|
|
|
1184
1465
|
const commitSegments = (next, options = {}) => {
|
|
1466
|
+
invalidateBatchUndo();
|
|
1185
1467
|
segmentsRef.current = next;
|
|
1186
1468
|
setSegments(next);
|
|
1187
1469
|
pruneSelectionFor(next);
|
|
@@ -1200,6 +1482,10 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1200
1482
|
segmentsRef.current = [];
|
|
1201
1483
|
setSegments([]);
|
|
1202
1484
|
setSelection(new Set());
|
|
1485
|
+
setSegmentUndo(new Map());
|
|
1486
|
+
setBatchUndoState(null);
|
|
1487
|
+
setTitleEditing('');
|
|
1488
|
+
pendingScrollRef.current = null;
|
|
1203
1489
|
setStatus('idle');
|
|
1204
1490
|
setError('');
|
|
1205
1491
|
return undefined;
|
|
@@ -1216,6 +1502,10 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1216
1502
|
segmentsRef.current = [];
|
|
1217
1503
|
setSegments([]);
|
|
1218
1504
|
setSelection(new Set());
|
|
1505
|
+
setSegmentUndo(new Map());
|
|
1506
|
+
setBatchUndoState(null);
|
|
1507
|
+
setTitleEditing('');
|
|
1508
|
+
pendingScrollRef.current = null;
|
|
1219
1509
|
setStatus('loading');
|
|
1220
1510
|
setError('');
|
|
1221
1511
|
const load = () => {
|
|
@@ -1227,10 +1517,15 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1227
1517
|
.then((data) => {
|
|
1228
1518
|
if (cancelled || rootRef.current !== root || mySeq !== loadSeqRef.current) return;
|
|
1229
1519
|
let value = normalizeServerSegments(data);
|
|
1520
|
+
// Notes written before 0.4 appended new cards at the bottom;
|
|
1521
|
+
// reverse that legacy order once so the newest card leads.
|
|
1522
|
+
if (isLegacyPayload(data)) value = value.slice().reverse();
|
|
1230
1523
|
if (value.length === 0) value = [newSegment('')];
|
|
1231
1524
|
segmentsRef.current = value;
|
|
1232
1525
|
setSegments(value);
|
|
1233
1526
|
setSelection(new Set());
|
|
1527
|
+
setSegmentUndo(new Map());
|
|
1528
|
+
setBatchUndoState(null);
|
|
1234
1529
|
versionRef.current = typeof data.mtimeMs === 'number' ? data.mtimeMs : null;
|
|
1235
1530
|
setSavedAt(typeof data.mtimeMs === 'number' ? data.mtimeMs : null);
|
|
1236
1531
|
dirtyRef.current = false;
|
|
@@ -1251,6 +1546,91 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1251
1546
|
};
|
|
1252
1547
|
}, [binding === null ? '' : binding.id, binding === null ? '' : binding.cwd]);
|
|
1253
1548
|
|
|
1549
|
+
// Sidebar workspace badges --------------------------------------------
|
|
1550
|
+
useEffect(() => {
|
|
1551
|
+
if (binding === null) {
|
|
1552
|
+
setCurrentWsId('');
|
|
1553
|
+
setWsCounts({});
|
|
1554
|
+
return undefined;
|
|
1555
|
+
}
|
|
1556
|
+
setCurrentWsId('');
|
|
1557
|
+
let disposed = false;
|
|
1558
|
+
const refresh = () => {
|
|
1559
|
+
const sessionId = binding.id;
|
|
1560
|
+
if (typeof sessionId !== 'string' || sessionId === '') return;
|
|
1561
|
+
fetchUnsentCounts(sessionId)
|
|
1562
|
+
.then((data) => {
|
|
1563
|
+
if (disposed || bindingRef.current?.id !== sessionId) return;
|
|
1564
|
+
const next = {};
|
|
1565
|
+
let matched = '';
|
|
1566
|
+
const target = normalizedCwd(binding.cwd);
|
|
1567
|
+
for (const workspace of Array.isArray(data?.workspaces) ? data.workspaces : []) {
|
|
1568
|
+
const id = typeof workspace?.id === 'string' ? workspace.id : '';
|
|
1569
|
+
if (id === '') continue;
|
|
1570
|
+
next[id] = Number.isFinite(workspace.count) ? Math.max(0, Math.floor(workspace.count)) : 0;
|
|
1571
|
+
if (matched === '' && normalizedCwd(workspace.path) === target) matched = id;
|
|
1572
|
+
}
|
|
1573
|
+
setWsCounts(next);
|
|
1574
|
+
if (matched !== '') setCurrentWsId(matched);
|
|
1575
|
+
})
|
|
1576
|
+
.catch(() => {});
|
|
1577
|
+
};
|
|
1578
|
+
refresh();
|
|
1579
|
+
const timer = setInterval(refresh, SIDEBAR_REFRESH_MS);
|
|
1580
|
+
const onFocus = () => refresh();
|
|
1581
|
+
const onVisibility = () => {
|
|
1582
|
+
if (typeof document !== 'undefined' && document.visibilityState === 'visible') refresh();
|
|
1583
|
+
};
|
|
1584
|
+
window.addEventListener('focus', onFocus);
|
|
1585
|
+
if (typeof document !== 'undefined') document.addEventListener('visibilitychange', onVisibility);
|
|
1586
|
+
return () => {
|
|
1587
|
+
disposed = true;
|
|
1588
|
+
clearInterval(timer);
|
|
1589
|
+
window.removeEventListener('focus', onFocus);
|
|
1590
|
+
if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', onVisibility);
|
|
1591
|
+
};
|
|
1592
|
+
}, [binding === null ? '' : binding.id, binding === null ? '' : binding.cwd]);
|
|
1593
|
+
|
|
1594
|
+
const localUnsentCount = countUnsent(segments);
|
|
1595
|
+
|
|
1596
|
+
useEffect(() => {
|
|
1597
|
+
if (currentWsId === '') return;
|
|
1598
|
+
setWsCounts((previous) =>
|
|
1599
|
+
(previous[currentWsId] === localUnsentCount
|
|
1600
|
+
? previous
|
|
1601
|
+
: { ...previous, [currentWsId]: localUnsentCount }));
|
|
1602
|
+
}, [currentWsId, localUnsentCount]);
|
|
1603
|
+
|
|
1604
|
+
useEffect(() => {
|
|
1605
|
+
if (typeof document === 'undefined' || typeof document.querySelectorAll !== 'function') return undefined;
|
|
1606
|
+
const apply = () => {
|
|
1607
|
+
let rows;
|
|
1608
|
+
try {
|
|
1609
|
+
rows = document.querySelectorAll('[data-row-key^="workspace:"]');
|
|
1610
|
+
} catch {
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
for (const row of rows) {
|
|
1614
|
+
const key = typeof row.getAttribute === 'function' ? String(row.getAttribute('data-row-key') ?? '') : '';
|
|
1615
|
+
const id = key.startsWith('workspace:') ? key.slice('workspace:'.length) : '';
|
|
1616
|
+
const count = id === '' ? 0 : (wsCounts[id] ?? 0);
|
|
1617
|
+
if (count > 0) row.setAttribute('data-np-unsent', count > 99 ? '99+' : String(count));
|
|
1618
|
+
else row.removeAttribute('data-np-unsent');
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
apply();
|
|
1622
|
+
const Observer = typeof globalThis !== 'undefined' ? globalThis.MutationObserver : undefined;
|
|
1623
|
+
if (typeof Observer !== 'function' || typeof document.body === 'undefined' || document.body === null) {
|
|
1624
|
+
return () => apply();
|
|
1625
|
+
}
|
|
1626
|
+
const observer = new Observer(apply);
|
|
1627
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
1628
|
+
return () => {
|
|
1629
|
+
observer.disconnect();
|
|
1630
|
+
apply();
|
|
1631
|
+
};
|
|
1632
|
+
}, [wsCounts]);
|
|
1633
|
+
|
|
1254
1634
|
// Focus a just-created or restored card ------------------------------
|
|
1255
1635
|
useEffect(() => {
|
|
1256
1636
|
const pending = pendingFocusRef.current;
|
|
@@ -1282,20 +1662,49 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1282
1662
|
autoGrow(element);
|
|
1283
1663
|
}, [segments]);
|
|
1284
1664
|
|
|
1665
|
+
// Restore a captured scroll offset right after a refresh commits ------
|
|
1666
|
+
useLayoutEffect(() => {
|
|
1667
|
+
const pending = pendingScrollRef.current;
|
|
1668
|
+
if (pending === null || pending === undefined) return;
|
|
1669
|
+
pendingScrollRef.current = null;
|
|
1670
|
+
const list = listRef.current;
|
|
1671
|
+
if (list === null || list === undefined) return;
|
|
1672
|
+
try {
|
|
1673
|
+
list.scrollTop = pending;
|
|
1674
|
+
} catch {
|
|
1675
|
+
/* A detached list cannot be scrolled. */
|
|
1676
|
+
}
|
|
1677
|
+
}, [segments]);
|
|
1678
|
+
|
|
1285
1679
|
// Keyboard ------------------------------------------------------------
|
|
1286
1680
|
useEffect(() => {
|
|
1287
1681
|
const onKeyDown = (event) => {
|
|
1288
1682
|
const chord = event.ctrlKey || event.metaKey;
|
|
1289
|
-
if (!chord || !event.shiftKey) return;
|
|
1290
|
-
if (event.key
|
|
1291
|
-
|
|
1292
|
-
|
|
1683
|
+
if (!chord || !event.shiftKey || event.altKey) return;
|
|
1684
|
+
if (event.key === 'N' || event.key === 'n') {
|
|
1685
|
+
event.preventDefault();
|
|
1686
|
+
setUi((previous) => clampUi({ ...previous, hidden: !previous.hidden }));
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
if (event.key === '0' || event.code === 'Digit0' || event.code === 'Numpad0') {
|
|
1690
|
+
event.preventDefault();
|
|
1691
|
+
setUi((previous) => clampUi({
|
|
1692
|
+
...previous,
|
|
1693
|
+
x: null,
|
|
1694
|
+
y: null,
|
|
1695
|
+
width: DEFAULT_W,
|
|
1696
|
+
height: DEFAULT_H,
|
|
1697
|
+
}));
|
|
1698
|
+
showToast(t('uiReset'));
|
|
1699
|
+
}
|
|
1293
1700
|
};
|
|
1294
1701
|
window.addEventListener('keydown', onKeyDown, true);
|
|
1295
1702
|
return () => {
|
|
1296
1703
|
window.removeEventListener('keydown', onKeyDown, true);
|
|
1297
1704
|
clearTimer();
|
|
1298
1705
|
if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
|
|
1706
|
+
for (const timer of imageDeleteTimersRef.current.values()) clearTimeout(timer);
|
|
1707
|
+
imageDeleteTimersRef.current.clear();
|
|
1299
1708
|
};
|
|
1300
1709
|
}, []);
|
|
1301
1710
|
|
|
@@ -1330,20 +1739,41 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1330
1739
|
const active = typeof document !== 'undefined' ? document.activeElement : null;
|
|
1331
1740
|
if (active !== null && active !== undefined && typeof active.closest === 'function'
|
|
1332
1741
|
&& active.closest('[data-dsh-notepad="true"]') !== null) return;
|
|
1742
|
+
// Snapshot the scroll offset right before the refresh; the list may
|
|
1743
|
+
// re-render (and jump) while the updated note is applied.
|
|
1744
|
+
const list = listRef.current;
|
|
1745
|
+
const scrollTop = list !== null && list !== undefined && Number.isFinite(list.scrollTop)
|
|
1746
|
+
? list.scrollTop
|
|
1747
|
+
: null;
|
|
1748
|
+
const mySeq = ++refreshSeqRef.current;
|
|
1749
|
+
pendingScrollRef.current = scrollTop;
|
|
1333
1750
|
fetchNote(root, sessionId)
|
|
1334
1751
|
.then((data) => {
|
|
1335
|
-
if (
|
|
1336
|
-
if (
|
|
1337
|
-
|
|
1338
|
-
|
|
1752
|
+
if (mySeq !== refreshSeqRef.current) return;
|
|
1753
|
+
if (rootRef.current !== root || dirtyRef.current) {
|
|
1754
|
+
pendingScrollRef.current = null;
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
if (typeof data.mtimeMs === 'number' && data.mtimeMs === versionRef.current) {
|
|
1758
|
+
pendingScrollRef.current = null;
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
let incoming = normalizeServerSegments(data);
|
|
1762
|
+
if (isLegacyPayload(data)) incoming = incoming.slice().reverse();
|
|
1763
|
+
if (incoming.length === 0) incoming = [newSegment('')];
|
|
1764
|
+
const value = mergeServerOrder(segmentsRef.current, incoming);
|
|
1339
1765
|
segmentsRef.current = value;
|
|
1340
1766
|
setSegments(value);
|
|
1341
|
-
|
|
1767
|
+
pruneSelectionFor(value);
|
|
1768
|
+
setSegmentUndo(new Map());
|
|
1769
|
+
setBatchUndoState(null);
|
|
1342
1770
|
versionRef.current = typeof data.mtimeMs === 'number' ? data.mtimeMs : null;
|
|
1343
1771
|
setSavedAt(typeof data.mtimeMs === 'number' ? data.mtimeMs : null);
|
|
1344
1772
|
setStatus('saved');
|
|
1345
1773
|
})
|
|
1346
|
-
.catch(() => {
|
|
1774
|
+
.catch(() => {
|
|
1775
|
+
if (mySeq === refreshSeqRef.current) pendingScrollRef.current = null;
|
|
1776
|
+
});
|
|
1347
1777
|
};
|
|
1348
1778
|
const onVisibility = () => {
|
|
1349
1779
|
if (typeof document !== 'undefined' && document.visibilityState === 'visible') refresh();
|
|
@@ -1362,9 +1792,31 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1362
1792
|
|
|
1363
1793
|
// Segment mutations ---------------------------------------------------
|
|
1364
1794
|
const updateSegmentText = (id, value) => {
|
|
1795
|
+
invalidateBatchUndo();
|
|
1365
1796
|
const next = segmentsRef.current.map((segment) => (segment.id === id ? { ...segment, text: value } : segment));
|
|
1366
1797
|
segmentsRef.current = next;
|
|
1367
1798
|
setSegments(next);
|
|
1799
|
+
setSegmentUndo((previous) => {
|
|
1800
|
+
const record = previous.get(id);
|
|
1801
|
+
if (record === undefined || record.after === value) return previous;
|
|
1802
|
+
const pruned = new Map(previous);
|
|
1803
|
+
pruned.delete(id);
|
|
1804
|
+
return pruned;
|
|
1805
|
+
});
|
|
1806
|
+
dirtyRef.current = true;
|
|
1807
|
+
setStatus('dirty');
|
|
1808
|
+
setError('');
|
|
1809
|
+
scheduleSave();
|
|
1810
|
+
};
|
|
1811
|
+
|
|
1812
|
+
const updateSegmentTitle = (id, value) => {
|
|
1813
|
+
const title = String(value ?? '').slice(0, MAX_TITLE_CHARS);
|
|
1814
|
+
const target = segmentsRef.current.find((segment) => segment.id === id);
|
|
1815
|
+
if (target === undefined || target.title === title) return;
|
|
1816
|
+
invalidateBatchUndo();
|
|
1817
|
+
const next = segmentsRef.current.map((segment) => (segment.id === id ? { ...segment, title } : segment));
|
|
1818
|
+
segmentsRef.current = next;
|
|
1819
|
+
setSegments(next);
|
|
1368
1820
|
dirtyRef.current = true;
|
|
1369
1821
|
setStatus('dirty');
|
|
1370
1822
|
setError('');
|
|
@@ -1445,13 +1897,46 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1445
1897
|
}
|
|
1446
1898
|
};
|
|
1447
1899
|
|
|
1900
|
+
const clearPendingImageDeletes = () => {
|
|
1901
|
+
for (const timer of imageDeleteTimersRef.current.values()) {
|
|
1902
|
+
clearTimeout(timer);
|
|
1903
|
+
}
|
|
1904
|
+
imageDeleteTimersRef.current.clear();
|
|
1905
|
+
};
|
|
1906
|
+
|
|
1907
|
+
const deleteImageFile = (pathValue) => {
|
|
1908
|
+
const root = rootRef.current;
|
|
1909
|
+
const sessionId = rootSessionRef.current;
|
|
1910
|
+
if (root === '' || sessionId === '' || typeof pathValue !== 'string' || pathValue === '') return;
|
|
1911
|
+
removeStoredImage(root, sessionId, pathValue).catch(() => {});
|
|
1912
|
+
};
|
|
1913
|
+
|
|
1914
|
+
const scheduleImageCleanup = (images) => {
|
|
1915
|
+
const list = Array.isArray(images) ? images : [];
|
|
1916
|
+
for (const image of list) {
|
|
1917
|
+
const pathValue = typeof image?.path === 'string' ? image.path : '';
|
|
1918
|
+
if (pathValue === '' || imageDeleteTimersRef.current.has(pathValue)) continue;
|
|
1919
|
+
const timer = setTimeout(() => {
|
|
1920
|
+
imageDeleteTimersRef.current.delete(pathValue);
|
|
1921
|
+
if (!segmentsRef.current.some((segment) =>
|
|
1922
|
+
(Array.isArray(segment.images) ? segment.images : []).some((item) => item.path === pathValue))) {
|
|
1923
|
+
deleteImageFile(pathValue);
|
|
1924
|
+
}
|
|
1925
|
+
}, IMAGE_DELETE_DELAY);
|
|
1926
|
+
imageDeleteTimersRef.current.set(pathValue, timer);
|
|
1927
|
+
}
|
|
1928
|
+
};
|
|
1929
|
+
|
|
1448
1930
|
const removeImage = (id, pathValue) => {
|
|
1449
1931
|
const next = segmentsRef.current.map((segment) => {
|
|
1450
1932
|
if (segment.id !== id) return segment;
|
|
1451
1933
|
const images = Array.isArray(segment.images) ? segment.images.filter((image) => image.path !== pathValue) : [];
|
|
1452
1934
|
return { ...segment, images };
|
|
1453
1935
|
});
|
|
1936
|
+
const stillReferenced = next.some((segment) =>
|
|
1937
|
+
(Array.isArray(segment.images) ? segment.images : []).some((image) => image.path === pathValue));
|
|
1454
1938
|
commitSegments(next);
|
|
1939
|
+
if (!stillReferenced) deleteImageFile(pathValue);
|
|
1455
1940
|
};
|
|
1456
1941
|
|
|
1457
1942
|
const triggerFilePicker = (id) => {
|
|
@@ -1524,7 +2009,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1524
2009
|
return;
|
|
1525
2010
|
}
|
|
1526
2011
|
const segment = newSegment('');
|
|
1527
|
-
commitSegments([...segmentsRef.current
|
|
2012
|
+
commitSegments([segment, ...segmentsRef.current], { focus: { id: segment.id, caret: 0 } });
|
|
1528
2013
|
};
|
|
1529
2014
|
|
|
1530
2015
|
const splitSegment = (id, element) => {
|
|
@@ -1534,15 +2019,21 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1534
2019
|
const head = current.text.slice(0, start);
|
|
1535
2020
|
const tail = current.text.slice(start);
|
|
1536
2021
|
const nextSegment = newSegment(tail);
|
|
1537
|
-
const
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
2022
|
+
const updated = segmentsRef.current.map((segment) =>
|
|
2023
|
+
(segment.id === id ? { ...segment, text: head } : segment));
|
|
2024
|
+
// Enter at the end creates a fresh card, which belongs at the top.
|
|
2025
|
+
// Splitting in the middle keeps the tail next to its source card so
|
|
2026
|
+
// the caret does not jump away from what the user is editing.
|
|
2027
|
+
const next = tail.trim() === ''
|
|
2028
|
+
? [nextSegment, ...updated]
|
|
2029
|
+
: (() => {
|
|
2030
|
+
const out = [];
|
|
2031
|
+
for (const segment of updated) {
|
|
2032
|
+
out.push(segment);
|
|
2033
|
+
if (segment.id === id) out.push(nextSegment);
|
|
2034
|
+
}
|
|
2035
|
+
return out;
|
|
2036
|
+
})();
|
|
1546
2037
|
commitSegments(next, { focus: { id: nextSegment.id, caret: 0 } });
|
|
1547
2038
|
};
|
|
1548
2039
|
|
|
@@ -1594,6 +2085,9 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1594
2085
|
const record = undoRef.current;
|
|
1595
2086
|
if (record === null || record === undefined) return;
|
|
1596
2087
|
undoRef.current = null;
|
|
2088
|
+
clearPendingImageDeletes();
|
|
2089
|
+
setSegmentUndo(new Map());
|
|
2090
|
+
setBatchUndoState(null);
|
|
1597
2091
|
commitSegments(record.snapshot);
|
|
1598
2092
|
setToast(null);
|
|
1599
2093
|
};
|
|
@@ -1604,6 +2098,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1604
2098
|
undoRef.current = { snapshot: segmentsRef.current };
|
|
1605
2099
|
const next = segmentsRef.current.filter((segment) => segment.id !== id);
|
|
1606
2100
|
commitSegments(next);
|
|
2101
|
+
scheduleImageCleanup(target.images);
|
|
1607
2102
|
showToast(t('deletedToast'), { label: t('undo'), onClick: undoLast });
|
|
1608
2103
|
};
|
|
1609
2104
|
|
|
@@ -1613,9 +2108,288 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1613
2108
|
undoRef.current = { snapshot: segmentsRef.current };
|
|
1614
2109
|
const next = segmentsRef.current.filter((segment) => segment.archived !== true);
|
|
1615
2110
|
commitSegments(next);
|
|
2111
|
+
scheduleImageCleanup(archived.flatMap((segment) => (Array.isArray(segment.images) ? segment.images : [])));
|
|
1616
2112
|
showToast(t('clearedArchive', { n: archived.length }), { label: t('undo'), onClick: undoLast });
|
|
1617
2113
|
};
|
|
1618
2114
|
|
|
2115
|
+
// Merge / reorder -----------------------------------------------------
|
|
2116
|
+
const mergeSelected = () => {
|
|
2117
|
+
const ids = new Set(
|
|
2118
|
+
segmentsRef.current
|
|
2119
|
+
.filter((segment) => segment.archived !== true && selectedRef.current.has(segment.id))
|
|
2120
|
+
.map((segment) => segment.id),
|
|
2121
|
+
);
|
|
2122
|
+
if (ids.size < 2) {
|
|
2123
|
+
showToast(t('mergeHint'));
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
const chosen = segmentsRef.current.filter((segment) => ids.has(segment.id));
|
|
2127
|
+
const firstIndex = segmentsRef.current.findIndex((segment) => ids.has(segment.id));
|
|
2128
|
+
let insertAt = 0;
|
|
2129
|
+
for (let index = 0; index < firstIndex; index += 1) {
|
|
2130
|
+
if (!ids.has(segmentsRef.current[index].id)) insertAt += 1;
|
|
2131
|
+
}
|
|
2132
|
+
const seenPaths = new Set();
|
|
2133
|
+
const images = [];
|
|
2134
|
+
for (const segment of chosen) {
|
|
2135
|
+
for (const image of Array.isArray(segment.images) ? segment.images : []) {
|
|
2136
|
+
if (seenPaths.has(image.path) || images.length >= MAX_IMAGES) continue;
|
|
2137
|
+
seenPaths.add(image.path);
|
|
2138
|
+
images.push(image);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
const merged = {
|
|
2142
|
+
id: newSegmentId(),
|
|
2143
|
+
text: chosen.map((segment) => segment.text).filter((text) => text.trim() !== '').join('\n\n'),
|
|
2144
|
+
title: chosen
|
|
2145
|
+
.map((segment) => (typeof segment.title === 'string' ? segment.title.trim() : ''))
|
|
2146
|
+
.filter((title) => title !== '')
|
|
2147
|
+
.join(' · ')
|
|
2148
|
+
.slice(0, MAX_TITLE_CHARS),
|
|
2149
|
+
createdAt: nowIso(),
|
|
2150
|
+
images,
|
|
2151
|
+
archived: false,
|
|
2152
|
+
archivedAt: null,
|
|
2153
|
+
};
|
|
2154
|
+
undoRef.current = { snapshot: segmentsRef.current };
|
|
2155
|
+
const next = segmentsRef.current.filter((segment) => !ids.has(segment.id));
|
|
2156
|
+
next.splice(Math.min(insertAt, next.length), 0, merged);
|
|
2157
|
+
setSelection(new Set());
|
|
2158
|
+
commitSegments(next, { focus: { id: merged.id, caret: merged.text.length } });
|
|
2159
|
+
showToast(t('mergedToast', { n: chosen.length }), { label: t('undo'), onClick: undoLast });
|
|
2160
|
+
};
|
|
2161
|
+
|
|
2162
|
+
const reorderSegments = (sourceId, targetId, after) => {
|
|
2163
|
+
if (sourceId === '' || sourceId === targetId) return;
|
|
2164
|
+
const active = segmentsRef.current.filter((segment) => segment.archived !== true);
|
|
2165
|
+
const from = active.findIndex((segment) => segment.id === sourceId);
|
|
2166
|
+
const to = active.findIndex((segment) => segment.id === targetId);
|
|
2167
|
+
if (from < 0 || to < 0) return;
|
|
2168
|
+
const moved = active.splice(from, 1)[0];
|
|
2169
|
+
let insertAt = to + (after ? 1 : 0);
|
|
2170
|
+
if (from < insertAt) insertAt -= 1;
|
|
2171
|
+
insertAt = Math.max(0, Math.min(insertAt, active.length));
|
|
2172
|
+
active.splice(insertAt, 0, moved);
|
|
2173
|
+
let cursor = 0;
|
|
2174
|
+
const next = segmentsRef.current.map((segment) =>
|
|
2175
|
+
(segment.archived === true ? segment : active[cursor++]));
|
|
2176
|
+
if (next.every((segment, index) => segment.id === segmentsRef.current[index].id)) return;
|
|
2177
|
+
commitSegments(next);
|
|
2178
|
+
};
|
|
2179
|
+
|
|
2180
|
+
const setReorderTarget = (target) => {
|
|
2181
|
+
reorderOverRef.current = target;
|
|
2182
|
+
setReorderOver(target);
|
|
2183
|
+
};
|
|
2184
|
+
|
|
2185
|
+
const onSegmentReorderStart = (event, id) => {
|
|
2186
|
+
reorderDragRef.current = id;
|
|
2187
|
+
setReorderDrag(id);
|
|
2188
|
+
try {
|
|
2189
|
+
event.dataTransfer?.setData('application/x-dsh-notepad-segment', id);
|
|
2190
|
+
if (event.dataTransfer !== undefined && event.dataTransfer !== null) event.dataTransfer.effectAllowed = 'move';
|
|
2191
|
+
} catch {
|
|
2192
|
+
/* Some browsers expose a read-only transfer. */
|
|
2193
|
+
}
|
|
2194
|
+
};
|
|
2195
|
+
|
|
2196
|
+
const onSegmentReorderOver = (event, id) => {
|
|
2197
|
+
const source = reorderDragRef.current;
|
|
2198
|
+
if (source === '' || source === id) return;
|
|
2199
|
+
const types = event?.dataTransfer?.types;
|
|
2200
|
+
if (types !== undefined && types !== null && !Array.from(types).includes('application/x-dsh-notepad-segment')) return;
|
|
2201
|
+
event.preventDefault?.();
|
|
2202
|
+
event.stopPropagation?.();
|
|
2203
|
+
const rect = typeof event.currentTarget?.getBoundingClientRect === 'function'
|
|
2204
|
+
? event.currentTarget.getBoundingClientRect()
|
|
2205
|
+
: null;
|
|
2206
|
+
const after = rect !== null && Number(event.clientY) > rect.top + rect.height / 2;
|
|
2207
|
+
if (reorderOverRef.current?.id !== id || reorderOverRef.current?.after !== after) {
|
|
2208
|
+
setReorderTarget({ id, after });
|
|
2209
|
+
}
|
|
2210
|
+
};
|
|
2211
|
+
|
|
2212
|
+
const onSegmentReorderDrop = (event, id) => {
|
|
2213
|
+
const source = reorderDragRef.current
|
|
2214
|
+
|| (() => {
|
|
2215
|
+
try {
|
|
2216
|
+
return event?.dataTransfer?.getData('application/x-dsh-notepad-segment') ?? '';
|
|
2217
|
+
} catch {
|
|
2218
|
+
return '';
|
|
2219
|
+
}
|
|
2220
|
+
})();
|
|
2221
|
+
const target = reorderOverRef.current?.id === id ? reorderOverRef.current : null;
|
|
2222
|
+
reorderDragRef.current = '';
|
|
2223
|
+
setReorderDrag('');
|
|
2224
|
+
setReorderTarget(null);
|
|
2225
|
+
if (source === '') return;
|
|
2226
|
+
event.preventDefault?.();
|
|
2227
|
+
event.stopPropagation?.();
|
|
2228
|
+
reorderSegments(source, id, target?.after === true);
|
|
2229
|
+
};
|
|
2230
|
+
|
|
2231
|
+
// AI optimization -----------------------------------------------------
|
|
2232
|
+
const markOptimizing = (id, active) => {
|
|
2233
|
+
const next = new Set(optimizingRef.current);
|
|
2234
|
+
if (active) next.add(id);
|
|
2235
|
+
else next.delete(id);
|
|
2236
|
+
optimizingRef.current = next;
|
|
2237
|
+
setOptimizing(next);
|
|
2238
|
+
};
|
|
2239
|
+
|
|
2240
|
+
/** Cache key: the prompt depends on the workspace, title and card text. */
|
|
2241
|
+
const optimizationCacheKey = (root, segment) =>
|
|
2242
|
+
`${root}\u0000${segment.title ?? ''}\u0000${segment.text}`;
|
|
2243
|
+
|
|
2244
|
+
const rememberOptimization = (key, value) => {
|
|
2245
|
+
const cache = optimizeCacheRef.current;
|
|
2246
|
+
cache.delete(key);
|
|
2247
|
+
cache.set(key, value);
|
|
2248
|
+
while (cache.size > OPTIMIZE_CACHE_LIMIT) {
|
|
2249
|
+
const oldest = cache.keys().next();
|
|
2250
|
+
if (oldest.done === true) break;
|
|
2251
|
+
cache.delete(oldest.value);
|
|
2252
|
+
}
|
|
2253
|
+
};
|
|
2254
|
+
|
|
2255
|
+
const requestOptimization = async (segment) => {
|
|
2256
|
+
const root = rootRef.current;
|
|
2257
|
+
const sessionId = rootSessionRef.current;
|
|
2258
|
+
if (root === '' || sessionId === '') throw new Error(t('optimizeUnavailable'));
|
|
2259
|
+
if (segment.text.length > MAX_OPTIMIZE_CHARS) throw new Error(t('optimizeTooLong'));
|
|
2260
|
+
const cacheKey = optimizationCacheKey(root, segment);
|
|
2261
|
+
const cached = optimizeCacheRef.current.get(cacheKey);
|
|
2262
|
+
if (typeof cached === 'string' && cached !== '') return cached;
|
|
2263
|
+
const data = await optimizeNoteText(root, sessionId, segment.text, segment.title ?? '');
|
|
2264
|
+
const value = typeof data.text === 'string' ? data.text.trim() : '';
|
|
2265
|
+
if (value === '') throw new Error(t('optimizeFailed'));
|
|
2266
|
+
rememberOptimization(cacheKey, value);
|
|
2267
|
+
return value;
|
|
2268
|
+
};
|
|
2269
|
+
|
|
2270
|
+
const optimizeSegment = async (id) => {
|
|
2271
|
+
if (optimizingRef.current.has(id)) return;
|
|
2272
|
+
const target = segmentsRef.current.find((segment) => segment.id === id && segment.archived !== true);
|
|
2273
|
+
if (target === undefined || target.text.trim() === '') {
|
|
2274
|
+
showToast(t('optimizeEmpty'));
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
const original = target.text;
|
|
2278
|
+
markOptimizing(id, true);
|
|
2279
|
+
try {
|
|
2280
|
+
const value = await requestOptimization(target);
|
|
2281
|
+
const current = segmentsRef.current.find((segment) => segment.id === id);
|
|
2282
|
+
if (current === undefined || current.text !== original || value === original) return;
|
|
2283
|
+
const next = segmentsRef.current.map((segment) =>
|
|
2284
|
+
(segment.id === id ? { ...segment, text: value } : segment));
|
|
2285
|
+
commitSegments(next);
|
|
2286
|
+
setSegmentUndo((previous) => {
|
|
2287
|
+
const updated = new Map(previous);
|
|
2288
|
+
updated.set(id, { before: original, after: value });
|
|
2289
|
+
return updated;
|
|
2290
|
+
});
|
|
2291
|
+
showToast(t('optimizedToast'));
|
|
2292
|
+
} catch (reason) {
|
|
2293
|
+
const message = String(reason?.message ?? reason);
|
|
2294
|
+
showToast(message === t('optimizeTooLong') || message === t('optimizeUnavailable')
|
|
2295
|
+
? message
|
|
2296
|
+
: `${t('optimizeFailed')}: ${message}`);
|
|
2297
|
+
} finally {
|
|
2298
|
+
markOptimizing(id, false);
|
|
2299
|
+
}
|
|
2300
|
+
};
|
|
2301
|
+
|
|
2302
|
+
const undoOptimizeSegment = (id) => {
|
|
2303
|
+
const record = segmentUndo.get(id);
|
|
2304
|
+
const current = segmentsRef.current.find((segment) => segment.id === id);
|
|
2305
|
+
if (record !== undefined && current !== undefined && current.text === record.after) {
|
|
2306
|
+
const next = segmentsRef.current.map((segment) =>
|
|
2307
|
+
(segment.id === id ? { ...segment, text: record.before } : segment));
|
|
2308
|
+
commitSegments(next);
|
|
2309
|
+
setSegmentUndo((previous) => {
|
|
2310
|
+
if (!previous.has(id)) return previous;
|
|
2311
|
+
const updated = new Map(previous);
|
|
2312
|
+
updated.delete(id);
|
|
2313
|
+
return updated;
|
|
2314
|
+
});
|
|
2315
|
+
showToast(t('optimizeUndone'));
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
setSegmentUndo((previous) => {
|
|
2319
|
+
if (!previous.has(id)) return previous;
|
|
2320
|
+
const updated = new Map(previous);
|
|
2321
|
+
updated.delete(id);
|
|
2322
|
+
return updated;
|
|
2323
|
+
});
|
|
2324
|
+
};
|
|
2325
|
+
|
|
2326
|
+
/** Improve only the checked segments; identical text is answered from cache. */
|
|
2327
|
+
const optimizeSelectedSegments = async () => {
|
|
2328
|
+
if (optimizeBatchRef.current) return;
|
|
2329
|
+
const ids = new Set(selectedRef.current);
|
|
2330
|
+
const targets = segmentsRef.current.filter((segment) =>
|
|
2331
|
+
segment.archived !== true && ids.has(segment.id) && segment.text.trim() !== '');
|
|
2332
|
+
if (targets.length === 0) {
|
|
2333
|
+
showToast(t('optimizeEmpty'));
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
optimizeBatchRef.current = true;
|
|
2337
|
+
const snapshot = segmentsRef.current;
|
|
2338
|
+
let done = 0;
|
|
2339
|
+
let changed = 0;
|
|
2340
|
+
let failed = 0;
|
|
2341
|
+
setOptimizeBatch({ done, total: targets.length });
|
|
2342
|
+
try {
|
|
2343
|
+
for (const target of targets) {
|
|
2344
|
+
const current = segmentsRef.current.find((segment) =>
|
|
2345
|
+
segment.id === target.id && segment.archived !== true);
|
|
2346
|
+
if (current !== undefined && current.text === target.text) {
|
|
2347
|
+
markOptimizing(target.id, true);
|
|
2348
|
+
try {
|
|
2349
|
+
const value = await requestOptimization(current);
|
|
2350
|
+
if (value !== current.text) {
|
|
2351
|
+
const next = segmentsRef.current.map((segment) =>
|
|
2352
|
+
(segment.id === target.id && segment.text === current.text
|
|
2353
|
+
? { ...segment, text: value }
|
|
2354
|
+
: segment));
|
|
2355
|
+
commitSegments(next);
|
|
2356
|
+
setSegmentUndo((previous) => {
|
|
2357
|
+
const updated = new Map(previous);
|
|
2358
|
+
updated.set(target.id, { before: current.text, after: value });
|
|
2359
|
+
return updated;
|
|
2360
|
+
});
|
|
2361
|
+
changed += 1;
|
|
2362
|
+
}
|
|
2363
|
+
} catch {
|
|
2364
|
+
failed += 1;
|
|
2365
|
+
} finally {
|
|
2366
|
+
markOptimizing(target.id, false);
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
done += 1;
|
|
2370
|
+
setOptimizeBatch({ done, total: targets.length });
|
|
2371
|
+
}
|
|
2372
|
+
if (changed > 0) {
|
|
2373
|
+
setBatchUndoState({ snapshot });
|
|
2374
|
+
showToast(t('optimizedCount', { n: changed }));
|
|
2375
|
+
} else {
|
|
2376
|
+
showToast(failed > 0 ? t('optimizeFailed') : t('optimizedToast'));
|
|
2377
|
+
}
|
|
2378
|
+
} finally {
|
|
2379
|
+
optimizeBatchRef.current = false;
|
|
2380
|
+
setOptimizeBatch(null);
|
|
2381
|
+
}
|
|
2382
|
+
};
|
|
2383
|
+
|
|
2384
|
+
const undoBatchOptimization = () => {
|
|
2385
|
+
const record = batchUndoRef.current;
|
|
2386
|
+
if (record === null || record === undefined) return;
|
|
2387
|
+
setBatchUndoState(null);
|
|
2388
|
+
setSegmentUndo(new Map());
|
|
2389
|
+
commitSegments(record.snapshot);
|
|
2390
|
+
showToast(t('optimizeUndone'));
|
|
2391
|
+
};
|
|
2392
|
+
|
|
1619
2393
|
// Selection -----------------------------------------------------------
|
|
1620
2394
|
const toggleSelected = (id) => {
|
|
1621
2395
|
const next = new Set(selectedRef.current);
|
|
@@ -1811,6 +2585,34 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1811
2585
|
updateSegmentText(id, event.target?.value ?? '');
|
|
1812
2586
|
};
|
|
1813
2587
|
|
|
2588
|
+
/** Focus a card's textarea and place the caret at an absolute offset. */
|
|
2589
|
+
const focusSegmentAt = (id, caret) => {
|
|
2590
|
+
const element = textareaRefs.current.get(id);
|
|
2591
|
+
if (element === null || element === undefined) return false;
|
|
2592
|
+
try {
|
|
2593
|
+
element.focus?.({ preventScroll: true });
|
|
2594
|
+
} catch {
|
|
2595
|
+
try {
|
|
2596
|
+
element.focus?.();
|
|
2597
|
+
} catch {
|
|
2598
|
+
/* ignore */
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
try {
|
|
2602
|
+
const value = typeof element.value === 'string' ? element.value : '';
|
|
2603
|
+
const position = Math.max(0, Math.min(Number(caret) || 0, value.length));
|
|
2604
|
+
if (typeof element.setSelectionRange === 'function') element.setSelectionRange(position, position);
|
|
2605
|
+
} catch {
|
|
2606
|
+
/* A non-layout shim may not support ranges. */
|
|
2607
|
+
}
|
|
2608
|
+
try {
|
|
2609
|
+
element.scrollIntoView?.({ block: 'nearest' });
|
|
2610
|
+
} catch {
|
|
2611
|
+
/* ignore */
|
|
2612
|
+
}
|
|
2613
|
+
return true;
|
|
2614
|
+
};
|
|
2615
|
+
|
|
1814
2616
|
const onSegmentKeyDown = (event, id) => {
|
|
1815
2617
|
if (event.isComposing === true || event.nativeEvent?.isComposing === true) return;
|
|
1816
2618
|
const chord = event.ctrlKey || event.metaKey;
|
|
@@ -1825,6 +2627,28 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1825
2627
|
insertSegments(list);
|
|
1826
2628
|
return;
|
|
1827
2629
|
}
|
|
2630
|
+
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
|
2631
|
+
if (event.altKey || event.shiftKey || chord) return;
|
|
2632
|
+
const element = event.currentTarget ?? event.target;
|
|
2633
|
+
const value = typeof element?.value === 'string' ? element.value : '';
|
|
2634
|
+
const start = typeof element?.selectionStart === 'number' ? element.selectionStart : null;
|
|
2635
|
+
const end = typeof element?.selectionEnd === 'number' ? element.selectionEnd : start;
|
|
2636
|
+
if (start === null || start !== end) return;
|
|
2637
|
+
const firstBreak = value.indexOf('\n');
|
|
2638
|
+
const lastBreak = value.lastIndexOf('\n');
|
|
2639
|
+
const up = event.key === 'ArrowUp';
|
|
2640
|
+
const atEdge = up
|
|
2641
|
+
? (firstBreak === -1 || start <= firstBreak)
|
|
2642
|
+
: (lastBreak === -1 || start > lastBreak);
|
|
2643
|
+
if (!atEdge) return;
|
|
2644
|
+
const active = segmentsRef.current.filter((segment) => segment.archived !== true);
|
|
2645
|
+
const position = active.findIndex((segment) => segment.id === id);
|
|
2646
|
+
const neighbor = position < 0 ? undefined : active[position + (up ? -1 : 1)];
|
|
2647
|
+
if (neighbor !== undefined && focusSegmentAt(neighbor.id, up ? neighbor.text.length : 0)) {
|
|
2648
|
+
event.preventDefault?.();
|
|
2649
|
+
}
|
|
2650
|
+
return;
|
|
2651
|
+
}
|
|
1828
2652
|
if (event.key !== 'Enter' || event.shiftKey || event.altKey || chord) return;
|
|
1829
2653
|
event.preventDefault();
|
|
1830
2654
|
splitSegment(id, event.currentTarget ?? event.target);
|
|
@@ -1841,8 +2665,11 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1841
2665
|
const vw = window.innerWidth || 1280;
|
|
1842
2666
|
const vh = window.innerHeight || 800;
|
|
1843
2667
|
const height = ui.minimized ? HEAD_H : ui.height;
|
|
1844
|
-
const
|
|
1845
|
-
const
|
|
2668
|
+
const minY = hostChromeTop() + EDGE_GAP;
|
|
2669
|
+
const x = Math.min(Math.max(EDGE_GAP, start.x + moveEvent.clientX - start.px),
|
|
2670
|
+
Math.max(EDGE_GAP, vw - ui.width - EDGE_GAP));
|
|
2671
|
+
const y = Math.min(Math.max(minY, start.y + moveEvent.clientY - start.py),
|
|
2672
|
+
Math.max(minY, vh - height - EDGE_GAP));
|
|
1846
2673
|
setUi((previous) => ({ ...previous, x, y }));
|
|
1847
2674
|
};
|
|
1848
2675
|
const stop = () => {
|
|
@@ -1868,17 +2695,20 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1868
2695
|
const move = (moveEvent) => {
|
|
1869
2696
|
const vw = window.innerWidth || 1280;
|
|
1870
2697
|
const vh = window.innerHeight || 800;
|
|
1871
|
-
const
|
|
1872
|
-
const
|
|
2698
|
+
const minY = hostChromeTop() + EDGE_GAP;
|
|
2699
|
+
const width = Math.min(Math.max(MIN_W, start.width + moveEvent.clientX - start.px),
|
|
2700
|
+
Math.max(MIN_W, vw - EDGE_GAP * 2));
|
|
2701
|
+
const height = Math.min(Math.max(MIN_H, start.height + moveEvent.clientY - start.py),
|
|
2702
|
+
Math.max(MIN_H, vh - minY - EDGE_GAP));
|
|
1873
2703
|
setUi((previous) => {
|
|
1874
|
-
const maxX = Math.max(
|
|
1875
|
-
const maxY = Math.max(
|
|
2704
|
+
const maxX = Math.max(EDGE_GAP, vw - width - EDGE_GAP);
|
|
2705
|
+
const maxY = Math.max(minY, vh - height - EDGE_GAP);
|
|
1876
2706
|
return {
|
|
1877
2707
|
...previous,
|
|
1878
2708
|
width,
|
|
1879
2709
|
height,
|
|
1880
|
-
x: typeof previous.x === 'number' ? Math.min(previous.x, maxX) : previous.x,
|
|
1881
|
-
y: typeof previous.y === 'number' ? Math.min(previous.y, maxY) : previous.y,
|
|
2710
|
+
x: typeof previous.x === 'number' ? Math.min(Math.max(EDGE_GAP, previous.x), maxX) : previous.x,
|
|
2711
|
+
y: typeof previous.y === 'number' ? Math.min(Math.max(minY, previous.y), maxY) : previous.y,
|
|
1882
2712
|
};
|
|
1883
2713
|
});
|
|
1884
2714
|
};
|
|
@@ -1902,11 +2732,13 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1902
2732
|
const selectedActive = activeSegments.filter((segment) => selected.has(segment.id));
|
|
1903
2733
|
const selectedCount = selectedActive.length;
|
|
1904
2734
|
const allSelected = activeSegments.length > 0 && selectedCount === activeSegments.length;
|
|
2735
|
+
const unsentCount = countUnsent(segments);
|
|
1905
2736
|
const totalChars = segments.reduce((sum, segment) => sum + segment.text.length, 0);
|
|
1906
2737
|
const displayWidth = ui.width;
|
|
1907
2738
|
const displayHeight = ui.minimized ? HEAD_H : ui.height;
|
|
1908
|
-
const fallbackX = Math.max(
|
|
1909
|
-
const fallbackY = Math.max(
|
|
2739
|
+
const fallbackX = Math.max(EDGE_GAP, ((typeof window !== 'undefined' && window.innerWidth) || 1280) - displayWidth - 24);
|
|
2740
|
+
const fallbackY = Math.max(hostChromeTop() + EDGE_GAP,
|
|
2741
|
+
((typeof window !== 'undefined' && window.innerHeight) || 800) - displayHeight - 24);
|
|
1910
2742
|
const left = typeof ui.x === 'number' ? ui.x : fallbackX;
|
|
1911
2743
|
const top = typeof ui.y === 'number' ? ui.y : fallbackY;
|
|
1912
2744
|
|
|
@@ -2045,6 +2877,42 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2045
2877
|
},
|
|
2046
2878
|
h(SendIcon, { size: 14 }),
|
|
2047
2879
|
),
|
|
2880
|
+
h(
|
|
2881
|
+
'button',
|
|
2882
|
+
{
|
|
2883
|
+
type: 'button',
|
|
2884
|
+
className: 'dsh-np-btn',
|
|
2885
|
+
title: selectedCount > 1 ? t('mergeSelected', { n: selectedCount }) : t('mergeHint'),
|
|
2886
|
+
'aria-label': t('merge'),
|
|
2887
|
+
disabled: selectedCount < 2,
|
|
2888
|
+
onClick: mergeSelected,
|
|
2889
|
+
},
|
|
2890
|
+
h(MergeIcon, { size: 14 }),
|
|
2891
|
+
),
|
|
2892
|
+
h(
|
|
2893
|
+
'button',
|
|
2894
|
+
{
|
|
2895
|
+
type: 'button',
|
|
2896
|
+
className: 'dsh-np-btn',
|
|
2897
|
+
'data-active': optimizeBatch !== null || batchUndo !== null ? 'true' : 'false',
|
|
2898
|
+
title: optimizeBatch !== null
|
|
2899
|
+
? t('optimizeAllProgress', { done: optimizeBatch.done, total: optimizeBatch.total })
|
|
2900
|
+
: batchUndo !== null
|
|
2901
|
+
? t('undoOptimize')
|
|
2902
|
+
: selectedCount > 0
|
|
2903
|
+
? t('optimizeSelected', { n: selectedCount })
|
|
2904
|
+
: t('optimizeSelectHint'),
|
|
2905
|
+
'aria-label': batchUndo !== null
|
|
2906
|
+
? t('undoOptimize')
|
|
2907
|
+
: selectedCount > 0 ? t('optimizeSelected', { n: selectedCount }) : t('optimizeSelectHint'),
|
|
2908
|
+
disabled: optimizeBatch !== null
|
|
2909
|
+
|| (batchUndo === null && selectedCount === 0),
|
|
2910
|
+
onClick: batchUndo !== null ? undoBatchOptimization : optimizeSelectedSegments,
|
|
2911
|
+
},
|
|
2912
|
+
optimizeBatch !== null
|
|
2913
|
+
? h('span', { className: 'dsh-np-mini-spinner' })
|
|
2914
|
+
: batchUndo !== null ? h(UndoIcon, { size: 14 }) : h(SparkleIcon, { size: 14 }),
|
|
2915
|
+
),
|
|
2048
2916
|
h(
|
|
2049
2917
|
'button',
|
|
2050
2918
|
{
|
|
@@ -2131,6 +2999,15 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2131
2999
|
const activeCards = activeSegments.map((segment, index) => {
|
|
2132
3000
|
const images = Array.isArray(segment.images) ? segment.images : [];
|
|
2133
3001
|
const empty = segment.text.trim() === '' && images.length === 0;
|
|
3002
|
+
const titleValue = segment.title ?? '';
|
|
3003
|
+
const showTitleInput = titleValue !== '' || titleEditing === segment.id;
|
|
3004
|
+
const undoRecord = segmentUndo.get(segment.id);
|
|
3005
|
+
const canUndoOptimize = undoRecord !== undefined && undoRecord.after === segment.text;
|
|
3006
|
+
const optimizingNow = optimizing.has(segment.id);
|
|
3007
|
+
const aiBusy = optimizingNow || optimizeBatch !== null;
|
|
3008
|
+
const aiTitle = aiBusy
|
|
3009
|
+
? t('optimizing')
|
|
3010
|
+
: canUndoOptimize ? t('undoOptimize') : t('optimizeSegment');
|
|
2134
3011
|
return h(
|
|
2135
3012
|
'article',
|
|
2136
3013
|
{
|
|
@@ -2139,6 +3016,14 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2139
3016
|
'data-seg-id': segment.id,
|
|
2140
3017
|
'data-archived': 'false',
|
|
2141
3018
|
'data-selected': selected.has(segment.id) ? 'true' : 'false',
|
|
3019
|
+
'data-reorder-drag': reorderDrag === segment.id ? 'true' : 'false',
|
|
3020
|
+
'data-drop-before': reorderOver?.id === segment.id && reorderOver?.after === false ? 'true' : 'false',
|
|
3021
|
+
'data-drop-after': reorderOver?.id === segment.id && reorderOver?.after === true ? 'true' : 'false',
|
|
3022
|
+
onDragOver: (event) => onSegmentReorderOver(event, segment.id),
|
|
3023
|
+
onDragLeave: () => {
|
|
3024
|
+
if (reorderOverRef.current?.id === segment.id) setReorderTarget(null);
|
|
3025
|
+
},
|
|
3026
|
+
onDrop: (event) => onSegmentReorderDrop(event, segment.id),
|
|
2142
3027
|
},
|
|
2143
3028
|
h('input', {
|
|
2144
3029
|
key: 'file-input',
|
|
@@ -2170,8 +3055,62 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2170
3055
|
onChange: () => toggleSelected(segment.id),
|
|
2171
3056
|
}),
|
|
2172
3057
|
),
|
|
2173
|
-
h(
|
|
2174
|
-
|
|
3058
|
+
h(
|
|
3059
|
+
'button',
|
|
3060
|
+
{
|
|
3061
|
+
type: 'button',
|
|
3062
|
+
className: 'dsh-np-index',
|
|
3063
|
+
title: t('indexHint'),
|
|
3064
|
+
'aria-label': t('selectSegment'),
|
|
3065
|
+
'aria-pressed': selected.has(segment.id) ? 'true' : 'false',
|
|
3066
|
+
draggable: true,
|
|
3067
|
+
onDragStart: (event) => onSegmentReorderStart(event, segment.id),
|
|
3068
|
+
onDragEnd: () => {
|
|
3069
|
+
reorderDragRef.current = '';
|
|
3070
|
+
setReorderDrag('');
|
|
3071
|
+
setReorderTarget(null);
|
|
3072
|
+
},
|
|
3073
|
+
onClick: () => toggleSelected(segment.id),
|
|
3074
|
+
},
|
|
3075
|
+
String(index + 1).padStart(2, '0'),
|
|
3076
|
+
),
|
|
3077
|
+
showTitleInput
|
|
3078
|
+
? h('input', {
|
|
3079
|
+
type: 'text',
|
|
3080
|
+
className: 'dsh-np-title-input',
|
|
3081
|
+
value: titleValue,
|
|
3082
|
+
placeholder: t('titlePlaceholder'),
|
|
3083
|
+
maxLength: MAX_TITLE_CHARS,
|
|
3084
|
+
autoFocus: titleValue === '',
|
|
3085
|
+
'aria-label': t('segmentTitle'),
|
|
3086
|
+
spellCheck: false,
|
|
3087
|
+
onFocus: () => setTitleEditing(segment.id),
|
|
3088
|
+
onChange: (event) => updateSegmentTitle(segment.id, event.target?.value ?? ''),
|
|
3089
|
+
onKeyDown: (event) => {
|
|
3090
|
+
if (event.key !== 'Enter') return;
|
|
3091
|
+
event.preventDefault();
|
|
3092
|
+
try {
|
|
3093
|
+
event.currentTarget?.blur?.();
|
|
3094
|
+
} catch {
|
|
3095
|
+
/* ignore */
|
|
3096
|
+
}
|
|
3097
|
+
},
|
|
3098
|
+
onBlur: () => {
|
|
3099
|
+
if (dirtyRef.current) saveNowRef.current?.();
|
|
3100
|
+
if ((segment.title ?? '') === '') setTitleEditing('');
|
|
3101
|
+
},
|
|
3102
|
+
})
|
|
3103
|
+
: h(
|
|
3104
|
+
'button',
|
|
3105
|
+
{
|
|
3106
|
+
type: 'button',
|
|
3107
|
+
className: 'dsh-np-title-add',
|
|
3108
|
+
title: t('segmentTitle'),
|
|
3109
|
+
'aria-label': t('segmentTitle'),
|
|
3110
|
+
onClick: () => setTitleEditing(segment.id),
|
|
3111
|
+
},
|
|
3112
|
+
t('titlePlaceholder'),
|
|
3113
|
+
),
|
|
2175
3114
|
h(
|
|
2176
3115
|
'button',
|
|
2177
3116
|
{
|
|
@@ -2219,6 +3158,23 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2219
3158
|
},
|
|
2220
3159
|
h(SendIcon, { size: 14 }),
|
|
2221
3160
|
),
|
|
3161
|
+
h(
|
|
3162
|
+
'button',
|
|
3163
|
+
{
|
|
3164
|
+
type: 'button',
|
|
3165
|
+
className: 'dsh-np-segbtn',
|
|
3166
|
+
'data-ai': 'true',
|
|
3167
|
+
title: aiTitle,
|
|
3168
|
+
'aria-label': aiTitle,
|
|
3169
|
+
disabled: aiBusy || (canUndoOptimize ? false : empty),
|
|
3170
|
+
onClick: canUndoOptimize
|
|
3171
|
+
? () => undoOptimizeSegment(segment.id)
|
|
3172
|
+
: () => optimizeSegment(segment.id),
|
|
3173
|
+
},
|
|
3174
|
+
optimizingNow
|
|
3175
|
+
? h('span', { className: 'dsh-np-mini-spinner' })
|
|
3176
|
+
: canUndoOptimize ? h(UndoIcon, { size: 14 }) : h(SparkleIcon, { size: 14 }),
|
|
3177
|
+
),
|
|
2222
3178
|
h(
|
|
2223
3179
|
'button',
|
|
2224
3180
|
{
|
|
@@ -2336,6 +3292,9 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2336
3292
|
'div',
|
|
2337
3293
|
{ className: 'dsh-np-seg-head' },
|
|
2338
3294
|
h('span', { className: 'dsh-np-archived-chip' }, t('archived')),
|
|
3295
|
+
typeof segment.title === 'string' && segment.title.trim() !== ''
|
|
3296
|
+
? h('span', { className: 'dsh-np-seg-title' }, segment.title)
|
|
3297
|
+
: null,
|
|
2339
3298
|
h('span', { className: 'dsh-np-spacer' }),
|
|
2340
3299
|
h(
|
|
2341
3300
|
'button',
|
|
@@ -2502,6 +3461,12 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2502
3461
|
onClick: toggleHidden,
|
|
2503
3462
|
},
|
|
2504
3463
|
h(NotepadIcon, { size: 16 }),
|
|
3464
|
+
ui.hidden && unsentCount > 0
|
|
3465
|
+
? h('span', {
|
|
3466
|
+
className: 'dsh-np-restore-badge',
|
|
3467
|
+
title: t('unsent', { n: unsentCount }),
|
|
3468
|
+
}, unsentCount > 99 ? '99+' : String(unsentCount))
|
|
3469
|
+
: null,
|
|
2505
3470
|
);
|
|
2506
3471
|
|
|
2507
3472
|
return [card, restore];
|