@restregular/dsh-notepad 0.3.1 → 0.4.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 +41 -20
- package/client.js +680 -20
- package/index.js +194 -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
|
@@ -50,8 +50,12 @@ 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;
|
|
55
59
|
const IMAGE_MIME = {
|
|
56
60
|
'image/png': 'png',
|
|
57
61
|
'image/jpeg': 'jpg',
|
|
@@ -68,6 +72,9 @@ window.__ModuleLoader__.load({
|
|
|
68
72
|
editor: '分段内容',
|
|
69
73
|
placeholder: '记录开发中遇到的问题,或下一阶段要做的事……',
|
|
70
74
|
segmentPlaceholder: '新分段…',
|
|
75
|
+
titlePlaceholder: '分段标题(可选)',
|
|
76
|
+
segmentTitle: '分段标题',
|
|
77
|
+
indexHint: '点击选择此段 · 拖动可排序',
|
|
71
78
|
loading: '读取中…',
|
|
72
79
|
empty: '还没有内容',
|
|
73
80
|
unsaved: '未保存',
|
|
@@ -102,6 +109,22 @@ window.__ModuleLoader__.load({
|
|
|
102
109
|
selectHint: '勾选分段可批量操作',
|
|
103
110
|
selected: '已选 {n} 段',
|
|
104
111
|
selectSegment: '选择此段',
|
|
112
|
+
merge: '合并分段',
|
|
113
|
+
mergeSelected: '合并选中的 {n} 段',
|
|
114
|
+
mergeHint: '至少选择 2 段才能合并',
|
|
115
|
+
mergedToast: '已合并 {n} 段',
|
|
116
|
+
optimizeSegment: 'AI 优化此段',
|
|
117
|
+
optimizeAll: 'AI 优化全部分段',
|
|
118
|
+
optimizeAllProgress: 'AI 优化中 {done}/{total}',
|
|
119
|
+
optimizing: '优化中…',
|
|
120
|
+
optimizedToast: '已优化此段',
|
|
121
|
+
optimizedCount: '已优化 {n} 段',
|
|
122
|
+
optimizeEmpty: '没有可优化的文字',
|
|
123
|
+
optimizeTooLong: '内容太长,无法优化',
|
|
124
|
+
optimizeUnavailable: 'AI 服务不可用',
|
|
125
|
+
optimizeFailed: '优化失败',
|
|
126
|
+
dragSegment: '拖动排序',
|
|
127
|
+
unsent: '{n} 条未发送',
|
|
105
128
|
archive: '归档',
|
|
106
129
|
archiveSegment: '归档此段',
|
|
107
130
|
archiveSelected: '归档选中的 {n} 段',
|
|
@@ -153,6 +176,9 @@ window.__ModuleLoader__.load({
|
|
|
153
176
|
editor: 'Segment content',
|
|
154
177
|
placeholder: 'Jot down problems, or what to build next…',
|
|
155
178
|
segmentPlaceholder: 'New segment…',
|
|
179
|
+
titlePlaceholder: 'Segment title (optional)',
|
|
180
|
+
segmentTitle: 'Segment title',
|
|
181
|
+
indexHint: 'Click to select · drag to reorder',
|
|
156
182
|
loading: 'Loading…',
|
|
157
183
|
empty: 'Nothing here yet',
|
|
158
184
|
unsaved: 'Unsaved',
|
|
@@ -187,6 +213,22 @@ window.__ModuleLoader__.load({
|
|
|
187
213
|
selectHint: 'Check segments to batch',
|
|
188
214
|
selected: '{n} selected',
|
|
189
215
|
selectSegment: 'Select this segment',
|
|
216
|
+
merge: 'Merge segments',
|
|
217
|
+
mergeSelected: 'Merge {n} selected',
|
|
218
|
+
mergeHint: 'Select at least 2 segments to merge',
|
|
219
|
+
mergedToast: 'Merged {n} segments',
|
|
220
|
+
optimizeSegment: 'Improve this segment with AI',
|
|
221
|
+
optimizeAll: 'Improve all segments with AI',
|
|
222
|
+
optimizeAllProgress: 'Improving {done}/{total}',
|
|
223
|
+
optimizing: 'Improving…',
|
|
224
|
+
optimizedToast: 'Segment improved',
|
|
225
|
+
optimizedCount: 'Improved {n} segments',
|
|
226
|
+
optimizeEmpty: 'Nothing to improve',
|
|
227
|
+
optimizeTooLong: 'Too long to improve',
|
|
228
|
+
optimizeUnavailable: 'AI service unavailable',
|
|
229
|
+
optimizeFailed: 'Improve failed',
|
|
230
|
+
dragSegment: 'Drag to reorder',
|
|
231
|
+
unsent: '{n} unsent',
|
|
190
232
|
archive: 'Archive',
|
|
191
233
|
archiveSegment: 'Archive this segment',
|
|
192
234
|
archiveSelected: 'Archive {n} selected',
|
|
@@ -488,6 +530,42 @@ window.__ModuleLoader__.load({
|
|
|
488
530
|
);
|
|
489
531
|
}
|
|
490
532
|
|
|
533
|
+
/** Four-point sparkle: an AI improvement action. */
|
|
534
|
+
function SparkleIcon({ size = 16 }) {
|
|
535
|
+
return h(
|
|
536
|
+
Svg,
|
|
537
|
+
{ size, viewBox: '0 0 16 16' },
|
|
538
|
+
h('path', {
|
|
539
|
+
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',
|
|
540
|
+
stroke: 'currentColor', strokeWidth: 1.2,
|
|
541
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
542
|
+
}),
|
|
543
|
+
h('path', {
|
|
544
|
+
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',
|
|
545
|
+
stroke: 'currentColor', strokeWidth: 1.1,
|
|
546
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
547
|
+
}),
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** Two arrows converging: merge selected segments into one. */
|
|
552
|
+
function MergeIcon({ size = 16 }) {
|
|
553
|
+
return h(
|
|
554
|
+
Svg,
|
|
555
|
+
{ size, viewBox: '0 0 16 16' },
|
|
556
|
+
h('path', {
|
|
557
|
+
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',
|
|
558
|
+
stroke: 'currentColor', strokeWidth: 1.3,
|
|
559
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
560
|
+
}),
|
|
561
|
+
h('path', {
|
|
562
|
+
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',
|
|
563
|
+
stroke: 'currentColor', strokeWidth: 1.3,
|
|
564
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
565
|
+
}),
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
491
569
|
/** Arrow towards the bottom-right corner: minimize to the corner icon. */
|
|
492
570
|
function MinimizeIcon({ size = 16 }) {
|
|
493
571
|
return h(
|
|
@@ -533,7 +611,7 @@ window.__ModuleLoader__.load({
|
|
|
533
611
|
.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}
|
|
534
612
|
.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
613
|
.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))}
|
|
614
|
+
.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
615
|
.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
616
|
.dsh-np-selectall input{width:14px;height:14px;margin:0;accent-color:var(--np-accent);cursor:pointer;flex:none}
|
|
539
617
|
.dsh-np-selectall input:disabled{cursor:default;opacity:.45}
|
|
@@ -543,12 +621,23 @@ window.__ModuleLoader__.load({
|
|
|
543
621
|
.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
622
|
.dsh-np-seg[data-archived=true]{border-style:dashed;background:transparent;opacity:.82;animation:none}
|
|
545
623
|
.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}
|
|
624
|
+
.dsh-np-seg[data-reorder-drag=true]{opacity:.45}
|
|
625
|
+
.dsh-np-seg[data-drop-before=true]{box-shadow:0 -2px 0 0 var(--np-accent)}
|
|
626
|
+
.dsh-np-seg[data-drop-after=true]{box-shadow:0 2px 0 0 var(--np-accent)}
|
|
546
627
|
.dsh-np-check{display:inline-flex;align-items:center;justify-content:center;flex:none;width:20px;height:20px;cursor:pointer}
|
|
547
628
|
.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:
|
|
629
|
+
.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}
|
|
630
|
+
.dsh-np-index:hover{background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06));color:var(--dsw-alias-label-primary,#1f2430)}
|
|
631
|
+
.dsh-np-index[aria-pressed=true]{background:color-mix(in srgb,var(--np-accent) 14%,transparent);color:var(--np-accent)}
|
|
632
|
+
.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}
|
|
633
|
+
.dsh-np-title-input::placeholder{color:var(--dsw-alias-label-caption,#9aa3b2);font-weight:400}
|
|
634
|
+
.dsh-np-title-input:hover{background:var(--dsw-alias-interactive-bg-hover,rgb(38 49 72/.06))}
|
|
635
|
+
.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)}
|
|
636
|
+
.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
637
|
.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
638
|
.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
639
|
.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}
|
|
640
|
+
.dsh-np-segbtn[data-ai=true]{color:var(--np-accent);opacity:1}
|
|
552
641
|
.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
642
|
.dsh-np-segbtn:disabled{opacity:.3;cursor:default}
|
|
554
643
|
.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 +655,7 @@ window.__ModuleLoader__.load({
|
|
|
566
655
|
.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
656
|
.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
657
|
.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}
|
|
658
|
+
.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
659
|
.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
660
|
.dsh-np-seg[data-archived=true] .dsh-np-medias{padding:0 10px 8px}
|
|
571
661
|
.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 +687,9 @@ window.__ModuleLoader__.load({
|
|
|
597
687
|
.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
688
|
.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
689
|
.dsh-np-restore[data-visible=true]:hover{opacity:1;transform:translateY(-1px)}
|
|
690
|
+
.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))}
|
|
691
|
+
[data-row-key^="workspace:"][data-np-unsent]{position:relative}
|
|
692
|
+
[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
693
|
@keyframes dsh-np-pulse{0%,100%{opacity:1}50%{opacity:.35}}
|
|
601
694
|
@keyframes dsh-np-rise{from{opacity:0;transform:translate(-50%,4px)}to{opacity:1;transform:translate(-50%,0)}}
|
|
602
695
|
@keyframes dsh-np-item-in{from{opacity:0;transform:translateY(-3px)}to{opacity:1;transform:none}}
|
|
@@ -605,7 +698,7 @@ body[data-ds-dark-theme] .dsh-np-card{--np-accent-fg:#0f1115;box-shadow:0 18px 4
|
|
|
605
698
|
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
699
|
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
700
|
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}}`;
|
|
701
|
+
@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
702
|
|
|
610
703
|
// ------------------------------------------------------------------
|
|
611
704
|
// Helpers
|
|
@@ -625,14 +718,38 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
625
718
|
return `seg-${Date.now().toString(36)}-${segmentSequence.toString(36)}`;
|
|
626
719
|
}
|
|
627
720
|
|
|
628
|
-
function
|
|
629
|
-
|
|
721
|
+
function nowIso() {
|
|
722
|
+
try {
|
|
723
|
+
return new Date().toISOString();
|
|
724
|
+
} catch {
|
|
725
|
+
return '';
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function newSegment(text, options = {}) {
|
|
730
|
+
return {
|
|
731
|
+
id: newSegmentId(),
|
|
732
|
+
text,
|
|
733
|
+
title: typeof options.title === 'string' ? options.title.slice(0, MAX_TITLE_CHARS) : '',
|
|
734
|
+
createdAt: typeof options.createdAt === 'string' && options.createdAt !== ''
|
|
735
|
+
? options.createdAt
|
|
736
|
+
: nowIso(),
|
|
737
|
+
images: [],
|
|
738
|
+
archived: false,
|
|
739
|
+
archivedAt: null,
|
|
740
|
+
};
|
|
630
741
|
}
|
|
631
742
|
|
|
632
743
|
function serializeSegments(segments) {
|
|
633
744
|
return segments.map((segment) => ({
|
|
634
745
|
id: segment.id,
|
|
635
746
|
text: segment.text,
|
|
747
|
+
...(typeof segment.title === 'string' && segment.title !== ''
|
|
748
|
+
? { title: segment.title }
|
|
749
|
+
: {}),
|
|
750
|
+
...(typeof segment.createdAt === 'string' && segment.createdAt !== ''
|
|
751
|
+
? { createdAt: segment.createdAt }
|
|
752
|
+
: {}),
|
|
636
753
|
...(Array.isArray(segment.images) && segment.images.length > 0
|
|
637
754
|
? { images: segment.images.map((image) => ({ path: image.path, name: image.name })) }
|
|
638
755
|
: {}),
|
|
@@ -654,6 +771,8 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
654
771
|
out.push({
|
|
655
772
|
id,
|
|
656
773
|
text: item.text,
|
|
774
|
+
title: typeof item.title === 'string' ? item.title.slice(0, MAX_TITLE_CHARS) : '',
|
|
775
|
+
createdAt: typeof item.createdAt === 'string' && item.createdAt !== '' ? item.createdAt : null,
|
|
657
776
|
images: normalizeImages(item.images),
|
|
658
777
|
archived,
|
|
659
778
|
archivedAt: archived && typeof item.archivedAt === 'string' ? item.archivedAt : null,
|
|
@@ -737,6 +856,46 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
737
856
|
.join('\n\n');
|
|
738
857
|
}
|
|
739
858
|
|
|
859
|
+
/** Whether a segment still counts as unsent work (active + has content). */
|
|
860
|
+
function isUnsent(segment) {
|
|
861
|
+
if (segment === null || segment === undefined || segment.archived === true) return false;
|
|
862
|
+
if (typeof segment.text === 'string' && segment.text.trim() !== '') return true;
|
|
863
|
+
return Array.isArray(segment.images) && segment.images.length > 0;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function countUnsent(segments) {
|
|
867
|
+
let count = 0;
|
|
868
|
+
for (const segment of segments) {
|
|
869
|
+
if (isUnsent(segment)) count += 1;
|
|
870
|
+
}
|
|
871
|
+
return count;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/** Whether a whole server payload predates per-segment creation times. */
|
|
875
|
+
function isLegacyPayload(data) {
|
|
876
|
+
const segments = Array.isArray(data?.segments) ? data.segments : [];
|
|
877
|
+
return segments.length > 0 && segments.every((segment) =>
|
|
878
|
+
segment !== null && typeof segment === 'object'
|
|
879
|
+
&& (typeof segment.createdAt !== 'string' || segment.createdAt === ''));
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Adopt server content without moving cards the user is looking at.
|
|
884
|
+
* Known ids keep the local order; cards created elsewhere land on top.
|
|
885
|
+
*/
|
|
886
|
+
function mergeServerOrder(current, incoming) {
|
|
887
|
+
const byId = new Map();
|
|
888
|
+
for (const segment of incoming) byId.set(segment.id, segment);
|
|
889
|
+
const kept = [];
|
|
890
|
+
for (const segment of current) {
|
|
891
|
+
const next = byId.get(segment.id);
|
|
892
|
+
if (next !== undefined) kept.push(next);
|
|
893
|
+
}
|
|
894
|
+
const known = new Set(kept.map((segment) => segment.id));
|
|
895
|
+
const added = incoming.filter((segment) => !known.has(segment.id));
|
|
896
|
+
return [...added, ...kept];
|
|
897
|
+
}
|
|
898
|
+
|
|
740
899
|
function isAbsolutePath(value) {
|
|
741
900
|
const text = String(value ?? '');
|
|
742
901
|
return text.startsWith('/') || /^[A-Za-z]:\//u.test(text) || text.startsWith('//');
|
|
@@ -950,6 +1109,32 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
950
1109
|
return readResponse(response);
|
|
951
1110
|
}
|
|
952
1111
|
|
|
1112
|
+
async function optimizeNoteText(root, sessionId, text, title) {
|
|
1113
|
+
const response = await fetch(`${API}/optimize`, {
|
|
1114
|
+
method: 'POST',
|
|
1115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1116
|
+
body: JSON.stringify({ root, sessionId, text, title }),
|
|
1117
|
+
});
|
|
1118
|
+
return readResponse(response);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
async function removeStoredImage(root, sessionId, pathValue) {
|
|
1122
|
+
const response = await fetch(`${API}/image`, {
|
|
1123
|
+
method: 'DELETE',
|
|
1124
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1125
|
+
body: JSON.stringify({ root, sessionId, path: pathValue }),
|
|
1126
|
+
});
|
|
1127
|
+
return readResponse(response);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
async function fetchUnsentCounts(sessionId) {
|
|
1131
|
+
const response = await fetch(`${API}/unsent?sessionId=${encodeURIComponent(sessionId)}`, {
|
|
1132
|
+
method: 'GET',
|
|
1133
|
+
cache: 'no-store',
|
|
1134
|
+
});
|
|
1135
|
+
return readResponse(response);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
953
1138
|
async function copyText(value) {
|
|
954
1139
|
try {
|
|
955
1140
|
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
|
@@ -1021,6 +1206,12 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1021
1206
|
const [resizing, setResizing] = useState(false);
|
|
1022
1207
|
const [uploading, setUploading] = useState({});
|
|
1023
1208
|
const [dragSegment, setDragSegment] = useState('');
|
|
1209
|
+
const [reorderDrag, setReorderDrag] = useState('');
|
|
1210
|
+
const [reorderOver, setReorderOver] = useState(null);
|
|
1211
|
+
const [optimizing, setOptimizing] = useState(() => new Set());
|
|
1212
|
+
const [optimizeAll, setOptimizeAll] = useState(null);
|
|
1213
|
+
const [wsCounts, setWsCounts] = useState({});
|
|
1214
|
+
const [currentWsId, setCurrentWsId] = useState('');
|
|
1024
1215
|
|
|
1025
1216
|
const bindingRef = useRef(null);
|
|
1026
1217
|
const rootSessionRef = useRef('');
|
|
@@ -1045,6 +1236,11 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1045
1236
|
const listRef = useRef(null);
|
|
1046
1237
|
const pendingFocusRef = useRef(null);
|
|
1047
1238
|
const undoRef = useRef(null);
|
|
1239
|
+
const optimizingRef = useRef(new Set());
|
|
1240
|
+
const optimizeAllRef = useRef(false);
|
|
1241
|
+
const reorderDragRef = useRef('');
|
|
1242
|
+
const reorderOverRef = useRef(null);
|
|
1243
|
+
const imageDeleteTimersRef = useRef(new Map());
|
|
1048
1244
|
|
|
1049
1245
|
const setSelection = (next) => {
|
|
1050
1246
|
selectedRef.current = next;
|
|
@@ -1227,6 +1423,9 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1227
1423
|
.then((data) => {
|
|
1228
1424
|
if (cancelled || rootRef.current !== root || mySeq !== loadSeqRef.current) return;
|
|
1229
1425
|
let value = normalizeServerSegments(data);
|
|
1426
|
+
// Notes written before 0.4 appended new cards at the bottom;
|
|
1427
|
+
// reverse that legacy order once so the newest card leads.
|
|
1428
|
+
if (isLegacyPayload(data)) value = value.slice().reverse();
|
|
1230
1429
|
if (value.length === 0) value = [newSegment('')];
|
|
1231
1430
|
segmentsRef.current = value;
|
|
1232
1431
|
setSegments(value);
|
|
@@ -1251,6 +1450,91 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1251
1450
|
};
|
|
1252
1451
|
}, [binding === null ? '' : binding.id, binding === null ? '' : binding.cwd]);
|
|
1253
1452
|
|
|
1453
|
+
// Sidebar workspace badges --------------------------------------------
|
|
1454
|
+
useEffect(() => {
|
|
1455
|
+
if (binding === null) {
|
|
1456
|
+
setCurrentWsId('');
|
|
1457
|
+
setWsCounts({});
|
|
1458
|
+
return undefined;
|
|
1459
|
+
}
|
|
1460
|
+
setCurrentWsId('');
|
|
1461
|
+
let disposed = false;
|
|
1462
|
+
const refresh = () => {
|
|
1463
|
+
const sessionId = binding.id;
|
|
1464
|
+
if (typeof sessionId !== 'string' || sessionId === '') return;
|
|
1465
|
+
fetchUnsentCounts(sessionId)
|
|
1466
|
+
.then((data) => {
|
|
1467
|
+
if (disposed || bindingRef.current?.id !== sessionId) return;
|
|
1468
|
+
const next = {};
|
|
1469
|
+
let matched = '';
|
|
1470
|
+
const target = normalizedCwd(binding.cwd);
|
|
1471
|
+
for (const workspace of Array.isArray(data?.workspaces) ? data.workspaces : []) {
|
|
1472
|
+
const id = typeof workspace?.id === 'string' ? workspace.id : '';
|
|
1473
|
+
if (id === '') continue;
|
|
1474
|
+
next[id] = Number.isFinite(workspace.count) ? Math.max(0, Math.floor(workspace.count)) : 0;
|
|
1475
|
+
if (matched === '' && normalizedCwd(workspace.path) === target) matched = id;
|
|
1476
|
+
}
|
|
1477
|
+
setWsCounts(next);
|
|
1478
|
+
if (matched !== '') setCurrentWsId(matched);
|
|
1479
|
+
})
|
|
1480
|
+
.catch(() => {});
|
|
1481
|
+
};
|
|
1482
|
+
refresh();
|
|
1483
|
+
const timer = setInterval(refresh, SIDEBAR_REFRESH_MS);
|
|
1484
|
+
const onFocus = () => refresh();
|
|
1485
|
+
const onVisibility = () => {
|
|
1486
|
+
if (typeof document !== 'undefined' && document.visibilityState === 'visible') refresh();
|
|
1487
|
+
};
|
|
1488
|
+
window.addEventListener('focus', onFocus);
|
|
1489
|
+
if (typeof document !== 'undefined') document.addEventListener('visibilitychange', onVisibility);
|
|
1490
|
+
return () => {
|
|
1491
|
+
disposed = true;
|
|
1492
|
+
clearInterval(timer);
|
|
1493
|
+
window.removeEventListener('focus', onFocus);
|
|
1494
|
+
if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', onVisibility);
|
|
1495
|
+
};
|
|
1496
|
+
}, [binding === null ? '' : binding.id, binding === null ? '' : binding.cwd]);
|
|
1497
|
+
|
|
1498
|
+
const localUnsentCount = countUnsent(segments);
|
|
1499
|
+
|
|
1500
|
+
useEffect(() => {
|
|
1501
|
+
if (currentWsId === '') return;
|
|
1502
|
+
setWsCounts((previous) =>
|
|
1503
|
+
(previous[currentWsId] === localUnsentCount
|
|
1504
|
+
? previous
|
|
1505
|
+
: { ...previous, [currentWsId]: localUnsentCount }));
|
|
1506
|
+
}, [currentWsId, localUnsentCount]);
|
|
1507
|
+
|
|
1508
|
+
useEffect(() => {
|
|
1509
|
+
if (typeof document === 'undefined' || typeof document.querySelectorAll !== 'function') return undefined;
|
|
1510
|
+
const apply = () => {
|
|
1511
|
+
let rows;
|
|
1512
|
+
try {
|
|
1513
|
+
rows = document.querySelectorAll('[data-row-key^="workspace:"]');
|
|
1514
|
+
} catch {
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
for (const row of rows) {
|
|
1518
|
+
const key = typeof row.getAttribute === 'function' ? String(row.getAttribute('data-row-key') ?? '') : '';
|
|
1519
|
+
const id = key.startsWith('workspace:') ? key.slice('workspace:'.length) : '';
|
|
1520
|
+
const count = id === '' ? 0 : (wsCounts[id] ?? 0);
|
|
1521
|
+
if (count > 0) row.setAttribute('data-np-unsent', count > 99 ? '99+' : String(count));
|
|
1522
|
+
else row.removeAttribute('data-np-unsent');
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
apply();
|
|
1526
|
+
const Observer = typeof globalThis !== 'undefined' ? globalThis.MutationObserver : undefined;
|
|
1527
|
+
if (typeof Observer !== 'function' || typeof document.body === 'undefined' || document.body === null) {
|
|
1528
|
+
return () => apply();
|
|
1529
|
+
}
|
|
1530
|
+
const observer = new Observer(apply);
|
|
1531
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
1532
|
+
return () => {
|
|
1533
|
+
observer.disconnect();
|
|
1534
|
+
apply();
|
|
1535
|
+
};
|
|
1536
|
+
}, [wsCounts]);
|
|
1537
|
+
|
|
1254
1538
|
// Focus a just-created or restored card ------------------------------
|
|
1255
1539
|
useEffect(() => {
|
|
1256
1540
|
const pending = pendingFocusRef.current;
|
|
@@ -1296,6 +1580,8 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1296
1580
|
window.removeEventListener('keydown', onKeyDown, true);
|
|
1297
1581
|
clearTimer();
|
|
1298
1582
|
if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
|
|
1583
|
+
for (const timer of imageDeleteTimersRef.current.values()) clearTimeout(timer);
|
|
1584
|
+
imageDeleteTimersRef.current.clear();
|
|
1299
1585
|
};
|
|
1300
1586
|
}, []);
|
|
1301
1587
|
|
|
@@ -1334,11 +1620,13 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1334
1620
|
.then((data) => {
|
|
1335
1621
|
if (rootRef.current !== root || dirtyRef.current) return;
|
|
1336
1622
|
if (typeof data.mtimeMs === 'number' && data.mtimeMs === versionRef.current) return;
|
|
1337
|
-
let
|
|
1338
|
-
if (
|
|
1623
|
+
let incoming = normalizeServerSegments(data);
|
|
1624
|
+
if (isLegacyPayload(data)) incoming = incoming.slice().reverse();
|
|
1625
|
+
if (incoming.length === 0) incoming = [newSegment('')];
|
|
1626
|
+
const value = mergeServerOrder(segmentsRef.current, incoming);
|
|
1339
1627
|
segmentsRef.current = value;
|
|
1340
1628
|
setSegments(value);
|
|
1341
|
-
|
|
1629
|
+
pruneSelectionFor(value);
|
|
1342
1630
|
versionRef.current = typeof data.mtimeMs === 'number' ? data.mtimeMs : null;
|
|
1343
1631
|
setSavedAt(typeof data.mtimeMs === 'number' ? data.mtimeMs : null);
|
|
1344
1632
|
setStatus('saved');
|
|
@@ -1371,6 +1659,19 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1371
1659
|
scheduleSave();
|
|
1372
1660
|
};
|
|
1373
1661
|
|
|
1662
|
+
const updateSegmentTitle = (id, value) => {
|
|
1663
|
+
const title = String(value ?? '').slice(0, MAX_TITLE_CHARS);
|
|
1664
|
+
const target = segmentsRef.current.find((segment) => segment.id === id);
|
|
1665
|
+
if (target === undefined || target.title === title) return;
|
|
1666
|
+
const next = segmentsRef.current.map((segment) => (segment.id === id ? { ...segment, title } : segment));
|
|
1667
|
+
segmentsRef.current = next;
|
|
1668
|
+
setSegments(next);
|
|
1669
|
+
dirtyRef.current = true;
|
|
1670
|
+
setStatus('dirty');
|
|
1671
|
+
setError('');
|
|
1672
|
+
scheduleSave();
|
|
1673
|
+
};
|
|
1674
|
+
|
|
1374
1675
|
// Image attachments ---------------------------------------------------
|
|
1375
1676
|
const imageErrorText = (reason) => {
|
|
1376
1677
|
const code = reason?.code;
|
|
@@ -1445,13 +1746,46 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1445
1746
|
}
|
|
1446
1747
|
};
|
|
1447
1748
|
|
|
1749
|
+
const clearPendingImageDeletes = () => {
|
|
1750
|
+
for (const timer of imageDeleteTimersRef.current.values()) {
|
|
1751
|
+
clearTimeout(timer);
|
|
1752
|
+
}
|
|
1753
|
+
imageDeleteTimersRef.current.clear();
|
|
1754
|
+
};
|
|
1755
|
+
|
|
1756
|
+
const deleteImageFile = (pathValue) => {
|
|
1757
|
+
const root = rootRef.current;
|
|
1758
|
+
const sessionId = rootSessionRef.current;
|
|
1759
|
+
if (root === '' || sessionId === '' || typeof pathValue !== 'string' || pathValue === '') return;
|
|
1760
|
+
removeStoredImage(root, sessionId, pathValue).catch(() => {});
|
|
1761
|
+
};
|
|
1762
|
+
|
|
1763
|
+
const scheduleImageCleanup = (images) => {
|
|
1764
|
+
const list = Array.isArray(images) ? images : [];
|
|
1765
|
+
for (const image of list) {
|
|
1766
|
+
const pathValue = typeof image?.path === 'string' ? image.path : '';
|
|
1767
|
+
if (pathValue === '' || imageDeleteTimersRef.current.has(pathValue)) continue;
|
|
1768
|
+
const timer = setTimeout(() => {
|
|
1769
|
+
imageDeleteTimersRef.current.delete(pathValue);
|
|
1770
|
+
if (!segmentsRef.current.some((segment) =>
|
|
1771
|
+
(Array.isArray(segment.images) ? segment.images : []).some((item) => item.path === pathValue))) {
|
|
1772
|
+
deleteImageFile(pathValue);
|
|
1773
|
+
}
|
|
1774
|
+
}, IMAGE_DELETE_DELAY);
|
|
1775
|
+
imageDeleteTimersRef.current.set(pathValue, timer);
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
|
|
1448
1779
|
const removeImage = (id, pathValue) => {
|
|
1449
1780
|
const next = segmentsRef.current.map((segment) => {
|
|
1450
1781
|
if (segment.id !== id) return segment;
|
|
1451
1782
|
const images = Array.isArray(segment.images) ? segment.images.filter((image) => image.path !== pathValue) : [];
|
|
1452
1783
|
return { ...segment, images };
|
|
1453
1784
|
});
|
|
1785
|
+
const stillReferenced = next.some((segment) =>
|
|
1786
|
+
(Array.isArray(segment.images) ? segment.images : []).some((image) => image.path === pathValue));
|
|
1454
1787
|
commitSegments(next);
|
|
1788
|
+
if (!stillReferenced) deleteImageFile(pathValue);
|
|
1455
1789
|
};
|
|
1456
1790
|
|
|
1457
1791
|
const triggerFilePicker = (id) => {
|
|
@@ -1524,7 +1858,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1524
1858
|
return;
|
|
1525
1859
|
}
|
|
1526
1860
|
const segment = newSegment('');
|
|
1527
|
-
commitSegments([...segmentsRef.current
|
|
1861
|
+
commitSegments([segment, ...segmentsRef.current], { focus: { id: segment.id, caret: 0 } });
|
|
1528
1862
|
};
|
|
1529
1863
|
|
|
1530
1864
|
const splitSegment = (id, element) => {
|
|
@@ -1534,15 +1868,21 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1534
1868
|
const head = current.text.slice(0, start);
|
|
1535
1869
|
const tail = current.text.slice(start);
|
|
1536
1870
|
const nextSegment = newSegment(tail);
|
|
1537
|
-
const
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1871
|
+
const updated = segmentsRef.current.map((segment) =>
|
|
1872
|
+
(segment.id === id ? { ...segment, text: head } : segment));
|
|
1873
|
+
// Enter at the end creates a fresh card, which belongs at the top.
|
|
1874
|
+
// Splitting in the middle keeps the tail next to its source card so
|
|
1875
|
+
// the caret does not jump away from what the user is editing.
|
|
1876
|
+
const next = tail.trim() === ''
|
|
1877
|
+
? [nextSegment, ...updated]
|
|
1878
|
+
: (() => {
|
|
1879
|
+
const out = [];
|
|
1880
|
+
for (const segment of updated) {
|
|
1881
|
+
out.push(segment);
|
|
1882
|
+
if (segment.id === id) out.push(nextSegment);
|
|
1883
|
+
}
|
|
1884
|
+
return out;
|
|
1885
|
+
})();
|
|
1546
1886
|
commitSegments(next, { focus: { id: nextSegment.id, caret: 0 } });
|
|
1547
1887
|
};
|
|
1548
1888
|
|
|
@@ -1594,6 +1934,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1594
1934
|
const record = undoRef.current;
|
|
1595
1935
|
if (record === null || record === undefined) return;
|
|
1596
1936
|
undoRef.current = null;
|
|
1937
|
+
clearPendingImageDeletes();
|
|
1597
1938
|
commitSegments(record.snapshot);
|
|
1598
1939
|
setToast(null);
|
|
1599
1940
|
};
|
|
@@ -1604,6 +1945,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1604
1945
|
undoRef.current = { snapshot: segmentsRef.current };
|
|
1605
1946
|
const next = segmentsRef.current.filter((segment) => segment.id !== id);
|
|
1606
1947
|
commitSegments(next);
|
|
1948
|
+
scheduleImageCleanup(target.images);
|
|
1607
1949
|
showToast(t('deletedToast'), { label: t('undo'), onClick: undoLast });
|
|
1608
1950
|
};
|
|
1609
1951
|
|
|
@@ -1613,9 +1955,226 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1613
1955
|
undoRef.current = { snapshot: segmentsRef.current };
|
|
1614
1956
|
const next = segmentsRef.current.filter((segment) => segment.archived !== true);
|
|
1615
1957
|
commitSegments(next);
|
|
1958
|
+
scheduleImageCleanup(archived.flatMap((segment) => (Array.isArray(segment.images) ? segment.images : [])));
|
|
1616
1959
|
showToast(t('clearedArchive', { n: archived.length }), { label: t('undo'), onClick: undoLast });
|
|
1617
1960
|
};
|
|
1618
1961
|
|
|
1962
|
+
// Merge / reorder -----------------------------------------------------
|
|
1963
|
+
const mergeSelected = () => {
|
|
1964
|
+
const ids = new Set(
|
|
1965
|
+
segmentsRef.current
|
|
1966
|
+
.filter((segment) => segment.archived !== true && selectedRef.current.has(segment.id))
|
|
1967
|
+
.map((segment) => segment.id),
|
|
1968
|
+
);
|
|
1969
|
+
if (ids.size < 2) {
|
|
1970
|
+
showToast(t('mergeHint'));
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
const chosen = segmentsRef.current.filter((segment) => ids.has(segment.id));
|
|
1974
|
+
const firstIndex = segmentsRef.current.findIndex((segment) => ids.has(segment.id));
|
|
1975
|
+
let insertAt = 0;
|
|
1976
|
+
for (let index = 0; index < firstIndex; index += 1) {
|
|
1977
|
+
if (!ids.has(segmentsRef.current[index].id)) insertAt += 1;
|
|
1978
|
+
}
|
|
1979
|
+
const seenPaths = new Set();
|
|
1980
|
+
const images = [];
|
|
1981
|
+
for (const segment of chosen) {
|
|
1982
|
+
for (const image of Array.isArray(segment.images) ? segment.images : []) {
|
|
1983
|
+
if (seenPaths.has(image.path) || images.length >= MAX_IMAGES) continue;
|
|
1984
|
+
seenPaths.add(image.path);
|
|
1985
|
+
images.push(image);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
const merged = {
|
|
1989
|
+
id: newSegmentId(),
|
|
1990
|
+
text: chosen.map((segment) => segment.text).filter((text) => text.trim() !== '').join('\n\n'),
|
|
1991
|
+
title: chosen
|
|
1992
|
+
.map((segment) => (typeof segment.title === 'string' ? segment.title.trim() : ''))
|
|
1993
|
+
.filter((title) => title !== '')
|
|
1994
|
+
.join(' · ')
|
|
1995
|
+
.slice(0, MAX_TITLE_CHARS),
|
|
1996
|
+
createdAt: nowIso(),
|
|
1997
|
+
images,
|
|
1998
|
+
archived: false,
|
|
1999
|
+
archivedAt: null,
|
|
2000
|
+
};
|
|
2001
|
+
undoRef.current = { snapshot: segmentsRef.current };
|
|
2002
|
+
const next = segmentsRef.current.filter((segment) => !ids.has(segment.id));
|
|
2003
|
+
next.splice(Math.min(insertAt, next.length), 0, merged);
|
|
2004
|
+
setSelection(new Set());
|
|
2005
|
+
commitSegments(next, { focus: { id: merged.id, caret: merged.text.length } });
|
|
2006
|
+
showToast(t('mergedToast', { n: chosen.length }), { label: t('undo'), onClick: undoLast });
|
|
2007
|
+
};
|
|
2008
|
+
|
|
2009
|
+
const reorderSegments = (sourceId, targetId, after) => {
|
|
2010
|
+
if (sourceId === '' || sourceId === targetId) return;
|
|
2011
|
+
const active = segmentsRef.current.filter((segment) => segment.archived !== true);
|
|
2012
|
+
const from = active.findIndex((segment) => segment.id === sourceId);
|
|
2013
|
+
const to = active.findIndex((segment) => segment.id === targetId);
|
|
2014
|
+
if (from < 0 || to < 0) return;
|
|
2015
|
+
const moved = active.splice(from, 1)[0];
|
|
2016
|
+
let insertAt = to + (after ? 1 : 0);
|
|
2017
|
+
if (from < insertAt) insertAt -= 1;
|
|
2018
|
+
insertAt = Math.max(0, Math.min(insertAt, active.length));
|
|
2019
|
+
active.splice(insertAt, 0, moved);
|
|
2020
|
+
let cursor = 0;
|
|
2021
|
+
const next = segmentsRef.current.map((segment) =>
|
|
2022
|
+
(segment.archived === true ? segment : active[cursor++]));
|
|
2023
|
+
if (next.every((segment, index) => segment.id === segmentsRef.current[index].id)) return;
|
|
2024
|
+
commitSegments(next);
|
|
2025
|
+
};
|
|
2026
|
+
|
|
2027
|
+
const setReorderTarget = (target) => {
|
|
2028
|
+
reorderOverRef.current = target;
|
|
2029
|
+
setReorderOver(target);
|
|
2030
|
+
};
|
|
2031
|
+
|
|
2032
|
+
const onSegmentReorderStart = (event, id) => {
|
|
2033
|
+
reorderDragRef.current = id;
|
|
2034
|
+
setReorderDrag(id);
|
|
2035
|
+
try {
|
|
2036
|
+
event.dataTransfer?.setData('application/x-dsh-notepad-segment', id);
|
|
2037
|
+
if (event.dataTransfer !== undefined && event.dataTransfer !== null) event.dataTransfer.effectAllowed = 'move';
|
|
2038
|
+
} catch {
|
|
2039
|
+
/* Some browsers expose a read-only transfer. */
|
|
2040
|
+
}
|
|
2041
|
+
};
|
|
2042
|
+
|
|
2043
|
+
const onSegmentReorderOver = (event, id) => {
|
|
2044
|
+
const source = reorderDragRef.current;
|
|
2045
|
+
if (source === '' || source === id) return;
|
|
2046
|
+
const types = event?.dataTransfer?.types;
|
|
2047
|
+
if (types !== undefined && types !== null && !Array.from(types).includes('application/x-dsh-notepad-segment')) return;
|
|
2048
|
+
event.preventDefault?.();
|
|
2049
|
+
event.stopPropagation?.();
|
|
2050
|
+
const rect = typeof event.currentTarget?.getBoundingClientRect === 'function'
|
|
2051
|
+
? event.currentTarget.getBoundingClientRect()
|
|
2052
|
+
: null;
|
|
2053
|
+
const after = rect !== null && Number(event.clientY) > rect.top + rect.height / 2;
|
|
2054
|
+
if (reorderOverRef.current?.id !== id || reorderOverRef.current?.after !== after) {
|
|
2055
|
+
setReorderTarget({ id, after });
|
|
2056
|
+
}
|
|
2057
|
+
};
|
|
2058
|
+
|
|
2059
|
+
const onSegmentReorderDrop = (event, id) => {
|
|
2060
|
+
const source = reorderDragRef.current
|
|
2061
|
+
|| (() => {
|
|
2062
|
+
try {
|
|
2063
|
+
return event?.dataTransfer?.getData('application/x-dsh-notepad-segment') ?? '';
|
|
2064
|
+
} catch {
|
|
2065
|
+
return '';
|
|
2066
|
+
}
|
|
2067
|
+
})();
|
|
2068
|
+
const target = reorderOverRef.current?.id === id ? reorderOverRef.current : null;
|
|
2069
|
+
reorderDragRef.current = '';
|
|
2070
|
+
setReorderDrag('');
|
|
2071
|
+
setReorderTarget(null);
|
|
2072
|
+
if (source === '') return;
|
|
2073
|
+
event.preventDefault?.();
|
|
2074
|
+
event.stopPropagation?.();
|
|
2075
|
+
reorderSegments(source, id, target?.after === true);
|
|
2076
|
+
};
|
|
2077
|
+
|
|
2078
|
+
// AI optimization -----------------------------------------------------
|
|
2079
|
+
const markOptimizing = (id, active) => {
|
|
2080
|
+
const next = new Set(optimizingRef.current);
|
|
2081
|
+
if (active) next.add(id);
|
|
2082
|
+
else next.delete(id);
|
|
2083
|
+
optimizingRef.current = next;
|
|
2084
|
+
setOptimizing(next);
|
|
2085
|
+
};
|
|
2086
|
+
|
|
2087
|
+
const requestOptimization = async (segment) => {
|
|
2088
|
+
const root = rootRef.current;
|
|
2089
|
+
const sessionId = rootSessionRef.current;
|
|
2090
|
+
if (root === '' || sessionId === '') throw new Error(t('optimizeUnavailable'));
|
|
2091
|
+
if (segment.text.length > MAX_OPTIMIZE_CHARS) throw new Error(t('optimizeTooLong'));
|
|
2092
|
+
const data = await optimizeNoteText(root, sessionId, segment.text, segment.title ?? '');
|
|
2093
|
+
const value = typeof data.text === 'string' ? data.text.trim() : '';
|
|
2094
|
+
if (value === '') throw new Error(t('optimizeFailed'));
|
|
2095
|
+
return value;
|
|
2096
|
+
};
|
|
2097
|
+
|
|
2098
|
+
const optimizeSegment = async (id) => {
|
|
2099
|
+
if (optimizingRef.current.has(id)) return;
|
|
2100
|
+
const target = segmentsRef.current.find((segment) => segment.id === id && segment.archived !== true);
|
|
2101
|
+
if (target === undefined || target.text.trim() === '') {
|
|
2102
|
+
showToast(t('optimizeEmpty'));
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
const snapshot = segmentsRef.current;
|
|
2106
|
+
const original = target.text;
|
|
2107
|
+
markOptimizing(id, true);
|
|
2108
|
+
try {
|
|
2109
|
+
const value = await requestOptimization(target);
|
|
2110
|
+
const current = segmentsRef.current.find((segment) => segment.id === id);
|
|
2111
|
+
if (current === undefined || current.text !== original || value === original) return;
|
|
2112
|
+
undoRef.current = { snapshot };
|
|
2113
|
+
const next = segmentsRef.current.map((segment) =>
|
|
2114
|
+
(segment.id === id ? { ...segment, text: value } : segment));
|
|
2115
|
+
commitSegments(next);
|
|
2116
|
+
showToast(t('optimizedToast'), { label: t('undo'), onClick: undoLast });
|
|
2117
|
+
} catch (reason) {
|
|
2118
|
+
const message = String(reason?.message ?? reason);
|
|
2119
|
+
showToast(message === t('optimizeTooLong') || message === t('optimizeUnavailable')
|
|
2120
|
+
? message
|
|
2121
|
+
: `${t('optimizeFailed')}: ${message}`);
|
|
2122
|
+
} finally {
|
|
2123
|
+
markOptimizing(id, false);
|
|
2124
|
+
}
|
|
2125
|
+
};
|
|
2126
|
+
|
|
2127
|
+
const optimizeAllSegments = async () => {
|
|
2128
|
+
if (optimizeAllRef.current) return;
|
|
2129
|
+
const targets = segmentsRef.current.filter((segment) =>
|
|
2130
|
+
segment.archived !== true && segment.text.trim() !== '');
|
|
2131
|
+
if (targets.length === 0) {
|
|
2132
|
+
showToast(t('optimizeEmpty'));
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
optimizeAllRef.current = true;
|
|
2136
|
+
const snapshot = segmentsRef.current;
|
|
2137
|
+
let done = 0;
|
|
2138
|
+
let changed = 0;
|
|
2139
|
+
let failed = 0;
|
|
2140
|
+
setOptimizeAll({ done, total: targets.length });
|
|
2141
|
+
try {
|
|
2142
|
+
for (const target of targets) {
|
|
2143
|
+
const current = segmentsRef.current.find((segment) =>
|
|
2144
|
+
segment.id === target.id && segment.archived !== true);
|
|
2145
|
+
if (current !== undefined && current.text === target.text) {
|
|
2146
|
+
markOptimizing(target.id, true);
|
|
2147
|
+
try {
|
|
2148
|
+
const value = await requestOptimization(current);
|
|
2149
|
+
if (value !== current.text) {
|
|
2150
|
+
const next = segmentsRef.current.map((segment) =>
|
|
2151
|
+
(segment.id === target.id && segment.text === current.text
|
|
2152
|
+
? { ...segment, text: value }
|
|
2153
|
+
: segment));
|
|
2154
|
+
commitSegments(next);
|
|
2155
|
+
changed += 1;
|
|
2156
|
+
}
|
|
2157
|
+
} catch {
|
|
2158
|
+
failed += 1;
|
|
2159
|
+
} finally {
|
|
2160
|
+
markOptimizing(target.id, false);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
done += 1;
|
|
2164
|
+
setOptimizeAll({ done, total: targets.length });
|
|
2165
|
+
}
|
|
2166
|
+
if (changed > 0) {
|
|
2167
|
+
undoRef.current = { snapshot };
|
|
2168
|
+
showToast(t('optimizedCount', { n: changed }), { label: t('undo'), onClick: undoLast });
|
|
2169
|
+
} else {
|
|
2170
|
+
showToast(failed > 0 ? t('optimizeFailed') : t('optimizedToast'));
|
|
2171
|
+
}
|
|
2172
|
+
} finally {
|
|
2173
|
+
optimizeAllRef.current = false;
|
|
2174
|
+
setOptimizeAll(null);
|
|
2175
|
+
}
|
|
2176
|
+
};
|
|
2177
|
+
|
|
1619
2178
|
// Selection -----------------------------------------------------------
|
|
1620
2179
|
const toggleSelected = (id) => {
|
|
1621
2180
|
const next = new Set(selectedRef.current);
|
|
@@ -1902,6 +2461,7 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
1902
2461
|
const selectedActive = activeSegments.filter((segment) => selected.has(segment.id));
|
|
1903
2462
|
const selectedCount = selectedActive.length;
|
|
1904
2463
|
const allSelected = activeSegments.length > 0 && selectedCount === activeSegments.length;
|
|
2464
|
+
const unsentCount = countUnsent(segments);
|
|
1905
2465
|
const totalChars = segments.reduce((sum, segment) => sum + segment.text.length, 0);
|
|
1906
2466
|
const displayWidth = ui.width;
|
|
1907
2467
|
const displayHeight = ui.minimized ? HEAD_H : ui.height;
|
|
@@ -2045,6 +2605,35 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2045
2605
|
},
|
|
2046
2606
|
h(SendIcon, { size: 14 }),
|
|
2047
2607
|
),
|
|
2608
|
+
h(
|
|
2609
|
+
'button',
|
|
2610
|
+
{
|
|
2611
|
+
type: 'button',
|
|
2612
|
+
className: 'dsh-np-btn',
|
|
2613
|
+
title: selectedCount > 1 ? t('mergeSelected', { n: selectedCount }) : t('mergeHint'),
|
|
2614
|
+
'aria-label': t('merge'),
|
|
2615
|
+
disabled: selectedCount < 2,
|
|
2616
|
+
onClick: mergeSelected,
|
|
2617
|
+
},
|
|
2618
|
+
h(MergeIcon, { size: 14 }),
|
|
2619
|
+
),
|
|
2620
|
+
h(
|
|
2621
|
+
'button',
|
|
2622
|
+
{
|
|
2623
|
+
type: 'button',
|
|
2624
|
+
className: 'dsh-np-btn',
|
|
2625
|
+
'data-active': optimizeAll !== null ? 'true' : 'false',
|
|
2626
|
+
title: optimizeAll !== null
|
|
2627
|
+
? t('optimizeAllProgress', { done: optimizeAll.done, total: optimizeAll.total })
|
|
2628
|
+
: t('optimizeAll'),
|
|
2629
|
+
'aria-label': t('optimizeAll'),
|
|
2630
|
+
disabled: activeSegments.length === 0 || optimizeAll !== null,
|
|
2631
|
+
onClick: optimizeAllSegments,
|
|
2632
|
+
},
|
|
2633
|
+
optimizeAll !== null
|
|
2634
|
+
? h('span', { className: 'dsh-np-mini-spinner' })
|
|
2635
|
+
: h(SparkleIcon, { size: 14 }),
|
|
2636
|
+
),
|
|
2048
2637
|
h(
|
|
2049
2638
|
'button',
|
|
2050
2639
|
{
|
|
@@ -2139,6 +2728,14 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2139
2728
|
'data-seg-id': segment.id,
|
|
2140
2729
|
'data-archived': 'false',
|
|
2141
2730
|
'data-selected': selected.has(segment.id) ? 'true' : 'false',
|
|
2731
|
+
'data-reorder-drag': reorderDrag === segment.id ? 'true' : 'false',
|
|
2732
|
+
'data-drop-before': reorderOver?.id === segment.id && reorderOver?.after === false ? 'true' : 'false',
|
|
2733
|
+
'data-drop-after': reorderOver?.id === segment.id && reorderOver?.after === true ? 'true' : 'false',
|
|
2734
|
+
onDragOver: (event) => onSegmentReorderOver(event, segment.id),
|
|
2735
|
+
onDragLeave: () => {
|
|
2736
|
+
if (reorderOverRef.current?.id === segment.id) setReorderTarget(null);
|
|
2737
|
+
},
|
|
2738
|
+
onDrop: (event) => onSegmentReorderDrop(event, segment.id),
|
|
2142
2739
|
},
|
|
2143
2740
|
h('input', {
|
|
2144
2741
|
key: 'file-input',
|
|
@@ -2170,8 +2767,47 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2170
2767
|
onChange: () => toggleSelected(segment.id),
|
|
2171
2768
|
}),
|
|
2172
2769
|
),
|
|
2173
|
-
h(
|
|
2174
|
-
|
|
2770
|
+
h(
|
|
2771
|
+
'button',
|
|
2772
|
+
{
|
|
2773
|
+
type: 'button',
|
|
2774
|
+
className: 'dsh-np-index',
|
|
2775
|
+
title: t('indexHint'),
|
|
2776
|
+
'aria-label': t('selectSegment'),
|
|
2777
|
+
'aria-pressed': selected.has(segment.id) ? 'true' : 'false',
|
|
2778
|
+
draggable: true,
|
|
2779
|
+
onDragStart: (event) => onSegmentReorderStart(event, segment.id),
|
|
2780
|
+
onDragEnd: () => {
|
|
2781
|
+
reorderDragRef.current = '';
|
|
2782
|
+
setReorderDrag('');
|
|
2783
|
+
setReorderTarget(null);
|
|
2784
|
+
},
|
|
2785
|
+
onClick: () => toggleSelected(segment.id),
|
|
2786
|
+
},
|
|
2787
|
+
String(index + 1).padStart(2, '0'),
|
|
2788
|
+
),
|
|
2789
|
+
h('input', {
|
|
2790
|
+
type: 'text',
|
|
2791
|
+
className: 'dsh-np-title-input',
|
|
2792
|
+
value: segment.title ?? '',
|
|
2793
|
+
placeholder: t('titlePlaceholder'),
|
|
2794
|
+
maxLength: MAX_TITLE_CHARS,
|
|
2795
|
+
'aria-label': t('segmentTitle'),
|
|
2796
|
+
spellCheck: false,
|
|
2797
|
+
onChange: (event) => updateSegmentTitle(segment.id, event.target?.value ?? ''),
|
|
2798
|
+
onKeyDown: (event) => {
|
|
2799
|
+
if (event.key !== 'Enter') return;
|
|
2800
|
+
event.preventDefault();
|
|
2801
|
+
try {
|
|
2802
|
+
event.currentTarget?.blur?.();
|
|
2803
|
+
} catch {
|
|
2804
|
+
/* ignore */
|
|
2805
|
+
}
|
|
2806
|
+
},
|
|
2807
|
+
onBlur: () => {
|
|
2808
|
+
if (dirtyRef.current) saveNowRef.current?.();
|
|
2809
|
+
},
|
|
2810
|
+
}),
|
|
2175
2811
|
h(
|
|
2176
2812
|
'button',
|
|
2177
2813
|
{
|
|
@@ -2219,6 +2855,21 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2219
2855
|
},
|
|
2220
2856
|
h(SendIcon, { size: 14 }),
|
|
2221
2857
|
),
|
|
2858
|
+
h(
|
|
2859
|
+
'button',
|
|
2860
|
+
{
|
|
2861
|
+
type: 'button',
|
|
2862
|
+
className: 'dsh-np-segbtn',
|
|
2863
|
+
'data-ai': 'true',
|
|
2864
|
+
title: optimizing.has(segment.id) ? t('optimizing') : t('optimizeSegment'),
|
|
2865
|
+
'aria-label': t('optimizeSegment'),
|
|
2866
|
+
disabled: empty || optimizing.has(segment.id) || optimizeAll !== null,
|
|
2867
|
+
onClick: () => optimizeSegment(segment.id),
|
|
2868
|
+
},
|
|
2869
|
+
optimizing.has(segment.id)
|
|
2870
|
+
? h('span', { className: 'dsh-np-mini-spinner' })
|
|
2871
|
+
: h(SparkleIcon, { size: 14 }),
|
|
2872
|
+
),
|
|
2222
2873
|
h(
|
|
2223
2874
|
'button',
|
|
2224
2875
|
{
|
|
@@ -2336,6 +2987,9 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2336
2987
|
'div',
|
|
2337
2988
|
{ className: 'dsh-np-seg-head' },
|
|
2338
2989
|
h('span', { className: 'dsh-np-archived-chip' }, t('archived')),
|
|
2990
|
+
typeof segment.title === 'string' && segment.title.trim() !== ''
|
|
2991
|
+
? h('span', { className: 'dsh-np-seg-title' }, segment.title)
|
|
2992
|
+
: null,
|
|
2339
2993
|
h('span', { className: 'dsh-np-spacer' }),
|
|
2340
2994
|
h(
|
|
2341
2995
|
'button',
|
|
@@ -2502,6 +3156,12 @@ body[data-ds-dark-theme] .dsh-np-seg[data-archived=true]{opacity:.7}
|
|
|
2502
3156
|
onClick: toggleHidden,
|
|
2503
3157
|
},
|
|
2504
3158
|
h(NotepadIcon, { size: 16 }),
|
|
3159
|
+
ui.hidden && unsentCount > 0
|
|
3160
|
+
? h('span', {
|
|
3161
|
+
className: 'dsh-np-restore-badge',
|
|
3162
|
+
title: t('unsent', { n: unsentCount }),
|
|
3163
|
+
}, unsentCount > 99 ? '99+' : String(unsentCount))
|
|
3164
|
+
: null,
|
|
2505
3165
|
);
|
|
2506
3166
|
|
|
2507
3167
|
return [card, restore];
|