dsh-side-memo 1.2.0 → 1.3.1
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 +13 -4
- package/README.zh-CN.md +14 -5
- package/README.zh-TW.md +14 -5
- package/client.js +1141 -22
- package/package.json +2 -2
package/client.js
CHANGED
|
@@ -134,6 +134,12 @@ window.__ModuleLoader__.load({
|
|
|
134
134
|
'favorite.remove': '取消收藏',
|
|
135
135
|
'favorite.pinned': '已收藏并置顶',
|
|
136
136
|
'favorite.unpinned': '已取消收藏',
|
|
137
|
+
// Right-click menu on a memo row.
|
|
138
|
+
'menu.label': '便签操作',
|
|
139
|
+
'menu.open': '打开',
|
|
140
|
+
'menu.duplicate': '复制一份',
|
|
141
|
+
'menu.deleted': '已删除便签',
|
|
142
|
+
'menu.duplicated': '已复制为新便签',
|
|
137
143
|
'image.add': '插入图片',
|
|
138
144
|
'image.remove': '移除图片',
|
|
139
145
|
'image.memo': '图片便签',
|
|
@@ -153,7 +159,24 @@ window.__ModuleLoader__.load({
|
|
|
153
159
|
'image.resize': '调整图片大小',
|
|
154
160
|
'image.size': '尺寸 {width} × {height}',
|
|
155
161
|
'image.resetSize': '还原原始大小',
|
|
156
|
-
'image.sizeReset': '已还原图片大小'
|
|
162
|
+
'image.sizeReset': '已还原图片大小',
|
|
163
|
+
// Saving a whole conversation as a memo.
|
|
164
|
+
'chat.create': '保存为便签',
|
|
165
|
+
'chat.createAria': '把这段对话保存为便签',
|
|
166
|
+
'chat.saved': '已保存为便签',
|
|
167
|
+
'chat.empty': '这段对话还没有可保存的内容。',
|
|
168
|
+
'chat.limit': '便签数量已达上限,先删除一些再保存。',
|
|
169
|
+
'chat.fallbackTitle': '对话便签',
|
|
170
|
+
'chat.you': '你',
|
|
171
|
+
'chat.assistant': '助手',
|
|
172
|
+
'chat.emptyPrompts': '你还没有发送任何内容。',
|
|
173
|
+
'chat.menu.label': '保存对话的方式',
|
|
174
|
+
'chat.menu.chat': '保存整段对话',
|
|
175
|
+
'chat.menu.chatNote': '我的提问和助手的回答',
|
|
176
|
+
'chat.menu.prompts': '只保存我的提问',
|
|
177
|
+
'chat.menu.promptsNote': '保留你发送过的内容',
|
|
178
|
+
'prompt.create': '把这条提问存成便签',
|
|
179
|
+
'prompt.createAria': '把这条提问保存为便签'
|
|
157
180
|
};
|
|
158
181
|
var en = {
|
|
159
182
|
'type.label': 'Side Memo',
|
|
@@ -221,6 +244,12 @@ window.__ModuleLoader__.load({
|
|
|
221
244
|
'favorite.remove': 'Remove from favorites',
|
|
222
245
|
'favorite.pinned': 'Added to favorites and pinned to top',
|
|
223
246
|
'favorite.unpinned': 'Removed from favorites',
|
|
247
|
+
// Right-click menu on a memo row.
|
|
248
|
+
'menu.label': 'Memo actions',
|
|
249
|
+
'menu.open': 'Open',
|
|
250
|
+
'menu.duplicate': 'Duplicate',
|
|
251
|
+
'menu.deleted': 'Memo deleted',
|
|
252
|
+
'menu.duplicated': 'Duplicated as a new memo',
|
|
224
253
|
'image.add': 'Insert image',
|
|
225
254
|
'image.remove': 'Remove image',
|
|
226
255
|
'image.memo': 'Image memo',
|
|
@@ -240,13 +269,30 @@ window.__ModuleLoader__.load({
|
|
|
240
269
|
'image.resize': 'Resize image',
|
|
241
270
|
'image.size': '{width} × {height}',
|
|
242
271
|
'image.resetSize': 'Reset to original size',
|
|
243
|
-
'image.sizeReset': 'Image size reset'
|
|
272
|
+
'image.sizeReset': 'Image size reset',
|
|
273
|
+
// Saving a whole conversation as a memo.
|
|
274
|
+
'chat.create': 'Save chat as memo',
|
|
275
|
+
'chat.createAria': 'Save this conversation as a memo',
|
|
276
|
+
'chat.saved': 'Saved to memos',
|
|
277
|
+
'chat.empty': 'This conversation has nothing to save yet.',
|
|
278
|
+
'chat.limit': 'Memo limit reached. Delete a few before saving.',
|
|
279
|
+
'chat.fallbackTitle': 'Chat memo',
|
|
280
|
+
'chat.you': 'You',
|
|
281
|
+
'chat.assistant': 'Assistant',
|
|
282
|
+
'chat.emptyPrompts': 'You have not sent anything yet.',
|
|
283
|
+
'chat.menu.label': 'How to save this conversation',
|
|
284
|
+
'chat.menu.chat': 'Save whole conversation',
|
|
285
|
+
'chat.menu.chatNote': 'My prompts and the replies',
|
|
286
|
+
'chat.menu.prompts': 'Save only my prompts',
|
|
287
|
+
'chat.menu.promptsNote': 'What I sent, nothing else',
|
|
288
|
+
'prompt.create': 'Save this prompt as memo',
|
|
289
|
+
'prompt.createAria': 'Save this prompt as a memo'
|
|
244
290
|
};
|
|
245
291
|
// ---------------------------------------------------------------------
|
|
246
292
|
// Styles (theme tokens only; literal colors are reserved for artwork)
|
|
247
293
|
// ---------------------------------------------------------------------
|
|
248
294
|
var STYLE = [
|
|
249
|
-
'.dsm-root{display:flex;flex-direction:column;height:100%;min-height:0;box-sizing:border-box;',
|
|
295
|
+
'.dsm-root{position:relative;display:flex;flex-direction:column;height:100%;min-height:0;box-sizing:border-box;',
|
|
250
296
|
'color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13.5px;line-height:1.45;background:transparent}',
|
|
251
297
|
'.dsm-toolbar{display:flex;align-items:center;gap:6px;padding:10px 10px 8px;flex:0 0 auto}',
|
|
252
298
|
'.dsm-toolbar-actions{display:flex;align-items:center;gap:4px;flex:0 0 auto}',
|
|
@@ -304,6 +350,25 @@ window.__ModuleLoader__.load({
|
|
|
304
350
|
'.dsm-row[draggable="true"]{cursor:grab}',
|
|
305
351
|
'.dsm-row[draggable="true"]:active{cursor:grabbing}',
|
|
306
352
|
'.dsm-row[data-dragging="true"]{opacity:.55;background:var(--dsw-alias-bg-layer-2)}',
|
|
353
|
+
// Rows far outside the panel viewport skip layout and paint; the intrinsic
|
|
354
|
+
// size keeps the scrollbar stable until a row has been rendered once. This
|
|
355
|
+
// is what keeps a 300-memo library cheap when the chat column re-layouts.
|
|
356
|
+
'.dsm-row{content-visibility:auto;contain-intrinsic-size:auto 78px}',
|
|
357
|
+
'.dsm-menu{position:absolute;z-index:20;min-width:172px;max-width:240px;padding:4px;box-sizing:border-box;',
|
|
358
|
+
'display:flex;flex-direction:column;gap:1px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;',
|
|
359
|
+
'background:var(--dsw-specific-menu);backdrop-filter:var(--dsw-menu-backdrop-filter);',
|
|
360
|
+
'box-shadow:var(--dsw-elevation-prominent)}',
|
|
361
|
+
'.dsm-menu-item{display:flex;align-items:center;gap:8px;width:100%;height:30px;padding:0 8px;box-sizing:border-box;',
|
|
362
|
+
'border:0;border-radius:7px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;',
|
|
363
|
+
'font-size:12.5px;line-height:1.2;text-align:left;white-space:nowrap;cursor:pointer}',
|
|
364
|
+
'.dsm-menu-item:hover{background:var(--dsw-alias-bg-layer-1)}',
|
|
365
|
+
'.dsm-menu-item:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px;',
|
|
366
|
+
'background:var(--dsw-alias-bg-layer-1)}',
|
|
367
|
+
'.dsm-menu-item[data-danger="true"]{color:var(--dsw-alias-state-error-primary)}',
|
|
368
|
+
'.dsm-menu-icon{display:flex;flex:0 0 auto;color:var(--dsw-alias-label-secondary)}',
|
|
369
|
+
'.dsm-menu-item[data-danger="true"] .dsm-menu-icon{color:inherit}',
|
|
370
|
+
'.dsm-menu-label{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis}',
|
|
371
|
+
'.dsm-menu-sep{flex:0 0 auto;height:1px;margin:3px 4px;background:var(--dsw-alias-border-l1)}',
|
|
307
372
|
'.dsm-foot-hint{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
308
373
|
'[data-composer-input].dsm-composer-drop{outline:2px dashed var(--dsw-alias-brand-primary);outline-offset:2px;border-radius:10px}',
|
|
309
374
|
'.dsm-icon-btn[data-favorite="true"]{color:var(--dsw-alias-brand-primary)}',
|
|
@@ -420,6 +485,26 @@ window.__ModuleLoader__.load({
|
|
|
420
485
|
'.dsm-toggle:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1)}',
|
|
421
486
|
'.dsm-toggle[data-active="true"]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1)}',
|
|
422
487
|
'.dsm-toggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}',
|
|
488
|
+
'.dsm-toggle[data-state="saved"]{color:var(--dsw-alias-brand-primary)}',
|
|
489
|
+
'.dsm-toggle[data-state="failed"]{color:var(--dsw-alias-label-primary);opacity:.85}',
|
|
490
|
+
'.dsm-chat-memo{display:inline-flex;align-items:center;position:relative}',
|
|
491
|
+
'.dsm-chat-memo-menu{position:fixed;z-index:120;min-width:214px;padding:5px;display:flex;flex-direction:column;gap:2px;',
|
|
492
|
+
'box-sizing:border-box;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-specific-menu);',
|
|
493
|
+
'backdrop-filter:var(--dsw-menu-backdrop-filter);box-shadow:var(--dsw-elevation-prominent)}',
|
|
494
|
+
'.dsm-chat-memo-item{display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:100%;padding:7px 9px;',
|
|
495
|
+
'border:0;border-radius:7px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;',
|
|
496
|
+
'line-height:1.35;text-align:left;cursor:pointer}',
|
|
497
|
+
'.dsm-chat-memo-item:hover{background:var(--dsw-alias-bg-layer-2)}',
|
|
498
|
+
'.dsm-chat-memo-item:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}',
|
|
499
|
+
'.dsm-chat-memo-note{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:1.3}',
|
|
500
|
+
'.dsm-prompt-memo{position:fixed;z-index:115;display:inline-flex;align-items:center;justify-content:center;',
|
|
501
|
+
'width:22px;height:22px;padding:0;pointer-events:auto;-webkit-app-region:no-drag;cursor:pointer;',
|
|
502
|
+
'border:1px solid var(--dsw-alias-border-l1);border-radius:6px;background:var(--dsw-specific-menu);',
|
|
503
|
+
'color:var(--dsw-alias-label-secondary);box-shadow:var(--dsw-elevation-prominent)}',
|
|
504
|
+
'.dsm-prompt-memo:hover{color:var(--dsw-alias-label-primary)}',
|
|
505
|
+
'.dsm-prompt-memo:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}',
|
|
506
|
+
'.dsm-prompt-memo[data-state="saved"]{color:var(--dsw-alias-brand-primary)}',
|
|
507
|
+
'.dsm-prompt-memo[data-state="failed"]{color:var(--dsw-alias-label-primary);opacity:.85}',
|
|
423
508
|
'.dsm-header-toggle{position:absolute;top:11px;right:62px;z-index:2}',
|
|
424
509
|
'.dsm-drawer-layer{position:absolute;inset:0;pointer-events:none}',
|
|
425
510
|
'.dsm-drawer{position:absolute;top:max(56px,var(--dsh-frame-overlay-top,12px));right:12px;bottom:12px;',
|
|
@@ -461,6 +546,10 @@ window.__ModuleLoader__.load({
|
|
|
461
546
|
function IconNote(props) {
|
|
462
547
|
return h(IconBase, props, h('path', { d: 'M5.5 5.25A1.75 1.75 0 0 1 7.25 3.5h6.9L19 8.35v10.4a1.75 1.75 0 0 1-1.75 1.75h-10A1.75 1.75 0 0 1 5.5 18.75z' }), h('path', { d: 'M14 3.5v5h5' }), h('path', { d: 'M8.75 12.25h6.5' }), h('path', { d: 'M8.75 15.75h4.25' }));
|
|
463
548
|
}
|
|
549
|
+
/** A speech bubble with memo lines: "turn this conversation into a memo". */
|
|
550
|
+
function IconChatMemo(props) {
|
|
551
|
+
return h(IconBase, props, h('path', { d: 'M4.75 6.25A2.5 2.5 0 0 1 7.25 3.75h9.5a2.5 2.5 0 0 1 2.5 2.5v6.5a2.5 2.5 0 0 1-2.5 2.5h-6.4L5.5 19.5v-4.25h-.75a2.5 2.5 0 0 1-2.5-2.5z' }), h('path', { d: 'M8.5 7.75h7' }), h('path', { d: 'M8.5 10.75h4.5' }));
|
|
552
|
+
}
|
|
464
553
|
function IconSearch(props) {
|
|
465
554
|
return h(IconBase, props, h('circle', { cx: 10.5, cy: 10.5, r: 6.25 }), h('path', { d: 'M15.25 15.25 20 20' }));
|
|
466
555
|
}
|
|
@@ -1366,20 +1455,35 @@ window.__ModuleLoader__.load({
|
|
|
1366
1455
|
});
|
|
1367
1456
|
}
|
|
1368
1457
|
function addImage(record) {
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
if (imageBytes() + image.size
|
|
1379
|
-
return
|
|
1380
|
-
return
|
|
1458
|
+
// The image cache is lazy, but an insert still needs the open database
|
|
1459
|
+
// handle and the in-memory budget, so force hydration before writing.
|
|
1460
|
+
return ensureImages().then(function () {
|
|
1461
|
+
if (!imagesAvailable)
|
|
1462
|
+
return { ok: false, reason: 'unavailable' };
|
|
1463
|
+
var clean = normalizeImageRecord(record);
|
|
1464
|
+
if (clean === undefined)
|
|
1465
|
+
return { ok: false, reason: 'unsupported' };
|
|
1466
|
+
var image = clean;
|
|
1467
|
+
if (imageBytes() + image.size <= MAX_TOTAL_IMAGE_BYTES)
|
|
1468
|
+
return persistImage(image);
|
|
1469
|
+
return collectOrphans().then(function () {
|
|
1470
|
+
if (imageBytes() + image.size > MAX_TOTAL_IMAGE_BYTES)
|
|
1471
|
+
return { ok: false, reason: 'budget' };
|
|
1472
|
+
return persistImage(image);
|
|
1473
|
+
});
|
|
1381
1474
|
});
|
|
1382
1475
|
}
|
|
1476
|
+
var imagesRequest = null;
|
|
1477
|
+
/**
|
|
1478
|
+
* Open the image database on first use instead of at plugin boot. Records
|
|
1479
|
+
* can be tens of megabytes, and most Sessions never open the notes surface,
|
|
1480
|
+
* so this work belongs to the panel mount rather than to `apply()`.
|
|
1481
|
+
*/
|
|
1482
|
+
function ensureImages() {
|
|
1483
|
+
if (imagesRequest === null)
|
|
1484
|
+
imagesRequest = loadImages();
|
|
1485
|
+
return imagesRequest;
|
|
1486
|
+
}
|
|
1383
1487
|
function loadImages() {
|
|
1384
1488
|
return openImageDb().then(function (db) {
|
|
1385
1489
|
imageDb = db;
|
|
@@ -1439,7 +1543,7 @@ window.__ModuleLoader__.load({
|
|
|
1439
1543
|
notes = [];
|
|
1440
1544
|
}
|
|
1441
1545
|
emit();
|
|
1442
|
-
|
|
1546
|
+
// Images hydrate on first panel/drawer mount (see ensureImages), not here.
|
|
1443
1547
|
return {
|
|
1444
1548
|
subscribe: function (listener) {
|
|
1445
1549
|
listeners.add(listener);
|
|
@@ -1549,6 +1653,46 @@ window.__ModuleLoader__.load({
|
|
|
1549
1653
|
emit();
|
|
1550
1654
|
return next;
|
|
1551
1655
|
},
|
|
1656
|
+
duplicate: function (id) {
|
|
1657
|
+
var index = -1;
|
|
1658
|
+
for (var at = 0; at < notes.length; at += 1) {
|
|
1659
|
+
if (notes[at].id === id) {
|
|
1660
|
+
index = at;
|
|
1661
|
+
break;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
if (index < 0)
|
|
1665
|
+
return undefined;
|
|
1666
|
+
if (notes.length >= MAX_NOTES)
|
|
1667
|
+
return undefined;
|
|
1668
|
+
var source = notes[index];
|
|
1669
|
+
var now = Date.now();
|
|
1670
|
+
var clonedContent = source.content.map(function (block) {
|
|
1671
|
+
if (block.type === 'image') {
|
|
1672
|
+
var image = { type: 'image', id: block.id };
|
|
1673
|
+
if (typeof block.width === 'number')
|
|
1674
|
+
image.width = block.width;
|
|
1675
|
+
if (typeof block.height === 'number')
|
|
1676
|
+
image.height = block.height;
|
|
1677
|
+
return image;
|
|
1678
|
+
}
|
|
1679
|
+
return { type: 'text', text: block.text };
|
|
1680
|
+
});
|
|
1681
|
+
var copy = {
|
|
1682
|
+
id: newNoteId(),
|
|
1683
|
+
title: source.title,
|
|
1684
|
+
body: source.body,
|
|
1685
|
+
content: clonedContent,
|
|
1686
|
+
createdAt: now,
|
|
1687
|
+
updatedAt: now,
|
|
1688
|
+
favorite: false,
|
|
1689
|
+
labels: normalizeLabels(source.labels)
|
|
1690
|
+
};
|
|
1691
|
+
notes = [copy].concat(notes);
|
|
1692
|
+
schedule();
|
|
1693
|
+
emit();
|
|
1694
|
+
return copy;
|
|
1695
|
+
},
|
|
1552
1696
|
remove: function (id) {
|
|
1553
1697
|
var next = notes.filter(function (note) {
|
|
1554
1698
|
return note.id !== id;
|
|
@@ -1604,6 +1748,7 @@ window.__ModuleLoader__.load({
|
|
|
1604
1748
|
write();
|
|
1605
1749
|
},
|
|
1606
1750
|
addImage: addImage,
|
|
1751
|
+
ensureImages: ensureImages,
|
|
1607
1752
|
collectOrphans: collectOrphans,
|
|
1608
1753
|
limits: { maxNotes: MAX_NOTES, maxTitle: MAX_TITLE, maxBody: MAX_BODY, maxImages: MAX_IMAGES }
|
|
1609
1754
|
};
|
|
@@ -2036,6 +2181,199 @@ window.__ModuleLoader__.load({
|
|
|
2036
2181
|
}
|
|
2037
2182
|
};
|
|
2038
2183
|
}
|
|
2184
|
+
/**
|
|
2185
|
+
* One-shot "open this memo" hand-off used by the chat-memo button.
|
|
2186
|
+
*
|
|
2187
|
+
* It is a store rather than local state because the notes surface may not be
|
|
2188
|
+
* mounted yet when the memo is written: the button reveals the panel first,
|
|
2189
|
+
* and the notes body picks the request up as it mounts.
|
|
2190
|
+
*/
|
|
2191
|
+
function createOpenRequestStore() {
|
|
2192
|
+
var requested = null;
|
|
2193
|
+
var listeners = new Set();
|
|
2194
|
+
function emit() {
|
|
2195
|
+
listeners.forEach(function (listener) {
|
|
2196
|
+
try {
|
|
2197
|
+
listener();
|
|
2198
|
+
}
|
|
2199
|
+
catch (error) {
|
|
2200
|
+
console.error('[dsh-side-memo] open-request listener failed', error);
|
|
2201
|
+
}
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
return {
|
|
2205
|
+
subscribe: function (listener) {
|
|
2206
|
+
listeners.add(listener);
|
|
2207
|
+
return function () {
|
|
2208
|
+
listeners.delete(listener);
|
|
2209
|
+
};
|
|
2210
|
+
},
|
|
2211
|
+
getSnapshot: function () {
|
|
2212
|
+
return requested;
|
|
2213
|
+
},
|
|
2214
|
+
set: function (id) {
|
|
2215
|
+
var next = typeof id === 'string' && id.length > 0 ? id : null;
|
|
2216
|
+
if (next === requested)
|
|
2217
|
+
return;
|
|
2218
|
+
requested = next;
|
|
2219
|
+
emit();
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
/** Inert store for notes surfaces rendered without a chat-memo hand-off. */
|
|
2224
|
+
var nullOpenRequest = {
|
|
2225
|
+
subscribe: function () {
|
|
2226
|
+
return function () { };
|
|
2227
|
+
},
|
|
2228
|
+
getSnapshot: function () {
|
|
2229
|
+
return null;
|
|
2230
|
+
},
|
|
2231
|
+
set: function () { }
|
|
2232
|
+
};
|
|
2233
|
+
// ---------------------------------------------------------------------
|
|
2234
|
+
// Chat → memo: condense the visible conversation into one memo draft.
|
|
2235
|
+
// ---------------------------------------------------------------------
|
|
2236
|
+
// The title comes from the first user message; long prompts make hostile
|
|
2237
|
+
// titles, so cap the derived title well below the stored MAX_TITLE.
|
|
2238
|
+
var CHAT_MEMO_TITLE_MAX = 80;
|
|
2239
|
+
function chatMemoText(value) {
|
|
2240
|
+
return typeof value === 'string' ? value.replace(/\r\n?/g, '\n').trim() : '';
|
|
2241
|
+
}
|
|
2242
|
+
/** Join the text entries of one chat block list (images, tools, reasoning skipped). */
|
|
2243
|
+
function chatMemoBlocks(value, key) {
|
|
2244
|
+
if (!Array.isArray(value))
|
|
2245
|
+
return '';
|
|
2246
|
+
var parts = [];
|
|
2247
|
+
for (var index = 0; index < value.length; index += 1) {
|
|
2248
|
+
var block = value[index];
|
|
2249
|
+
if (block === null || typeof block !== 'object')
|
|
2250
|
+
continue;
|
|
2251
|
+
if (block[key] !== 'text')
|
|
2252
|
+
continue;
|
|
2253
|
+
var text = chatMemoText(block.text);
|
|
2254
|
+
if (text !== '')
|
|
2255
|
+
parts.push(text);
|
|
2256
|
+
}
|
|
2257
|
+
return parts.join('\n\n');
|
|
2258
|
+
}
|
|
2259
|
+
/**
|
|
2260
|
+
* Reduce visible chat nodes to ordered user/assistant text turns.
|
|
2261
|
+
* `mode` "prompts" keeps only what the user sent.
|
|
2262
|
+
*/
|
|
2263
|
+
function chatMemoEntries(nodes, mode) {
|
|
2264
|
+
var entries = [];
|
|
2265
|
+
if (!Array.isArray(nodes))
|
|
2266
|
+
return entries;
|
|
2267
|
+
for (var index = 0; index < nodes.length; index += 1) {
|
|
2268
|
+
var node = nodes[index];
|
|
2269
|
+
if (node === null || typeof node !== 'object')
|
|
2270
|
+
continue;
|
|
2271
|
+
var kind = typeof node.kind === 'string' ? node.kind : '';
|
|
2272
|
+
var data = node.data === null || typeof node.data !== 'object' ? {} : node.data;
|
|
2273
|
+
var role = null;
|
|
2274
|
+
var text = '';
|
|
2275
|
+
if (kind === 'user' || kind === 'steering') {
|
|
2276
|
+
role = 'user';
|
|
2277
|
+
text = chatMemoBlocks(data.content, 'type');
|
|
2278
|
+
}
|
|
2279
|
+
else if (kind === 'assistant' || kind === 'assistant-step') {
|
|
2280
|
+
role = 'assistant';
|
|
2281
|
+
text = chatMemoBlocks(data.blocks, 'kind');
|
|
2282
|
+
}
|
|
2283
|
+
if (role === null || text === '')
|
|
2284
|
+
continue;
|
|
2285
|
+
if (mode === 'prompts' && role !== 'user')
|
|
2286
|
+
continue;
|
|
2287
|
+
entries.push({ role: role, text: text });
|
|
2288
|
+
}
|
|
2289
|
+
return entries;
|
|
2290
|
+
}
|
|
2291
|
+
function chatMemoHeading(entry, labels) {
|
|
2292
|
+
return (entry.role === 'user' ? labels.you : labels.assistant) + ':\n';
|
|
2293
|
+
}
|
|
2294
|
+
function chatMemoTitle(entries, labels) {
|
|
2295
|
+
var limit = Math.min(CHAT_MEMO_TITLE_MAX, MAX_TITLE);
|
|
2296
|
+
for (var index = 0; index < entries.length; index += 1) {
|
|
2297
|
+
if (entries[index].role !== 'user')
|
|
2298
|
+
continue;
|
|
2299
|
+
var collapsed = entries[index].text.replace(/\s+/g, ' ').trim();
|
|
2300
|
+
if (collapsed === '')
|
|
2301
|
+
continue;
|
|
2302
|
+
return collapsed.length > limit ? collapsed.slice(0, limit).trim() : collapsed;
|
|
2303
|
+
}
|
|
2304
|
+
return labels.fallbackTitle;
|
|
2305
|
+
}
|
|
2306
|
+
function chatMemoBody(entries, labels) {
|
|
2307
|
+
var parts = [];
|
|
2308
|
+
for (var index = 0; index < entries.length; index += 1) {
|
|
2309
|
+
parts.push(chatMemoHeading(entries[index], labels) + entries[index].text);
|
|
2310
|
+
}
|
|
2311
|
+
return parts.join('\n\n');
|
|
2312
|
+
}
|
|
2313
|
+
/**
|
|
2314
|
+
* Build the memo title/body for one conversation. Returns null when the chat
|
|
2315
|
+
* holds no user or assistant text (blank session, tools only, …).
|
|
2316
|
+
*
|
|
2317
|
+
* Transcripts are bounded twice: whole turns are dropped from the old end
|
|
2318
|
+
* until the body fits MAX_BODY, and a single oversized turn keeps its tail so
|
|
2319
|
+
* the newest words survive. `truncated` reports either adjustment.
|
|
2320
|
+
*
|
|
2321
|
+
* `mode` "prompts" saves only the user's messages; the title still comes from
|
|
2322
|
+
* the first prompt, and a chat with no prompts returns null.
|
|
2323
|
+
*/
|
|
2324
|
+
function chatMemoDraft(nodes, labels, mode) {
|
|
2325
|
+
var entries = chatMemoEntries(nodes, mode);
|
|
2326
|
+
if (entries.length === 0)
|
|
2327
|
+
return null;
|
|
2328
|
+
var maxBody = MAX_BODY;
|
|
2329
|
+
var kept = entries.slice();
|
|
2330
|
+
var truncated = false;
|
|
2331
|
+
var body = chatMemoBody(kept, labels);
|
|
2332
|
+
while (body.length > maxBody && kept.length > 1) {
|
|
2333
|
+
kept.shift();
|
|
2334
|
+
truncated = true;
|
|
2335
|
+
body = chatMemoBody(kept, labels);
|
|
2336
|
+
}
|
|
2337
|
+
if (body.length > maxBody) {
|
|
2338
|
+
var last = kept[0];
|
|
2339
|
+
var room = maxBody - chatMemoHeading(last, labels).length;
|
|
2340
|
+
if (room > 0 && last.text.length > room) {
|
|
2341
|
+
kept = [{ role: last.role, text: last.text.slice(last.text.length - room) }];
|
|
2342
|
+
truncated = true;
|
|
2343
|
+
body = chatMemoBody(kept, labels);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
if (body.length > maxBody) {
|
|
2347
|
+
body = body.slice(body.length - maxBody);
|
|
2348
|
+
truncated = true;
|
|
2349
|
+
}
|
|
2350
|
+
if (body.trim() === '')
|
|
2351
|
+
return null;
|
|
2352
|
+
return {
|
|
2353
|
+
title: chatMemoTitle(entries, labels),
|
|
2354
|
+
body: body,
|
|
2355
|
+
messages: kept.length,
|
|
2356
|
+
truncated: truncated
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Memo title/body for one prompt captured from the transcript. The body keeps
|
|
2361
|
+
* the prompt verbatim (only normalised line endings); the title is collapsed
|
|
2362
|
+
* and capped, and an oversized prompt keeps its tail so a memo can never
|
|
2363
|
+
* exceed the stored body limit.
|
|
2364
|
+
*/
|
|
2365
|
+
function promptMemoDraft(value, labels) {
|
|
2366
|
+
var text = chatMemoText(value);
|
|
2367
|
+
if (text === '')
|
|
2368
|
+
return null;
|
|
2369
|
+
var body = text.length > MAX_BODY ? text.slice(text.length - MAX_BODY) : text;
|
|
2370
|
+
return {
|
|
2371
|
+
title: chatMemoTitle([{ role: 'user', text: text }], labels),
|
|
2372
|
+
body: body,
|
|
2373
|
+
messages: 1,
|
|
2374
|
+
truncated: body.length !== text.length
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2039
2377
|
/**
|
|
2040
2378
|
* Read the Session the right sidebar can act on. `ctx.sidebarRight.mounted`
|
|
2041
2379
|
* is the observable of the on-screen Session — undefined while no Session is
|
|
@@ -2159,6 +2497,38 @@ window.__ModuleLoader__.load({
|
|
|
2159
2497
|
return right.updatedAt - left.updatedAt;
|
|
2160
2498
|
});
|
|
2161
2499
|
}
|
|
2500
|
+
/**
|
|
2501
|
+
* Fit a context menu inside its container. Coordinates are container-relative,
|
|
2502
|
+
* so the menu flips above the anchor when it would overflow the bottom edge and
|
|
2503
|
+
* is clamped to the container with a small margin on every side.
|
|
2504
|
+
*/
|
|
2505
|
+
function placeContextMenu(anchor, size, bounds, margin) {
|
|
2506
|
+
var gap = typeof margin === 'number' && isFinite(margin) ? Math.max(0, margin) : 4;
|
|
2507
|
+
var anchorX = typeof anchor.x === 'number' && isFinite(anchor.x) ? anchor.x : gap;
|
|
2508
|
+
var anchorY = typeof anchor.y === 'number' && isFinite(anchor.y) ? anchor.y : gap;
|
|
2509
|
+
var width = typeof size.width === 'number' && isFinite(size.width) ? Math.max(0, size.width) : 0;
|
|
2510
|
+
var height = typeof size.height === 'number' && isFinite(size.height) ? Math.max(0, size.height) : 0;
|
|
2511
|
+
var boundWidth = typeof bounds.width === 'number' && isFinite(bounds.width) ? Math.max(0, bounds.width) : 0;
|
|
2512
|
+
var boundHeight = typeof bounds.height === 'number' && isFinite(bounds.height) ? Math.max(0, bounds.height) : 0;
|
|
2513
|
+
var left = Math.min(Math.max(anchorX, gap), Math.max(gap, boundWidth - width - gap));
|
|
2514
|
+
var top = anchorY;
|
|
2515
|
+
if (top + height + gap > boundHeight)
|
|
2516
|
+
top = anchorY - height - gap;
|
|
2517
|
+
top = Math.min(Math.max(top, gap), Math.max(gap, boundHeight - height - gap));
|
|
2518
|
+
return { left: Math.round(left), top: Math.round(top) };
|
|
2519
|
+
}
|
|
2520
|
+
/** One row of the memo context menu; keyboard focus is managed by the menu. */
|
|
2521
|
+
function contextMenuItem(key, label, icon, danger, onSelect) {
|
|
2522
|
+
return h('button', {
|
|
2523
|
+
type: 'button',
|
|
2524
|
+
role: 'menuitem',
|
|
2525
|
+
key: key,
|
|
2526
|
+
className: 'dsm-menu-item',
|
|
2527
|
+
'data-danger': danger ? 'true' : undefined,
|
|
2528
|
+
tabIndex: -1,
|
|
2529
|
+
onClick: onSelect
|
|
2530
|
+
}, h('span', { className: 'dsm-menu-icon' }, icon), h('span', { className: 'dsm-menu-label' }, label));
|
|
2531
|
+
}
|
|
2162
2532
|
function copyToClipboard(text) {
|
|
2163
2533
|
if (typeof navigator !== 'undefined' && navigator.clipboard !== undefined && typeof navigator.clipboard.writeText === 'function') {
|
|
2164
2534
|
return navigator.clipboard.writeText(text).then(function () {
|
|
@@ -2928,6 +3298,50 @@ window.__ModuleLoader__.load({
|
|
|
2928
3298
|
var labelNoticeState = React.useState('');
|
|
2929
3299
|
var labelNotice = labelNoticeState[0];
|
|
2930
3300
|
var setLabelNotice = labelNoticeState[1];
|
|
3301
|
+
// Right-click menu on a memo row. Coordinates are relative to `.dsm-root`.
|
|
3302
|
+
var menuState = React.useState(null);
|
|
3303
|
+
var menu = menuState[0];
|
|
3304
|
+
var setMenu = menuState[1];
|
|
3305
|
+
var menuPositionState = React.useState(null);
|
|
3306
|
+
var menuPosition = menuPositionState[0];
|
|
3307
|
+
var setMenuPosition = menuPositionState[1];
|
|
3308
|
+
var menuArmedState = React.useState(false);
|
|
3309
|
+
var menuDeleteArmed = menuArmedState[0];
|
|
3310
|
+
var setMenuDeleteArmed = menuArmedState[1];
|
|
3311
|
+
var rootRef = React.useRef(null);
|
|
3312
|
+
var menuRef = React.useRef(null);
|
|
3313
|
+
var menuReturnFocusRef = React.useRef(null);
|
|
3314
|
+
var openRequest = props.openRequest === undefined ? nullOpenRequest : props.openRequest;
|
|
3315
|
+
var requestedNoteId = useSyncExternalStore(openRequest.subscribe, openRequest.getSnapshot, openRequest.getSnapshot);
|
|
3316
|
+
// Inline images live in IndexedDB and can be large. Hydrate them when this
|
|
3317
|
+
// surface mounts (panel tab or pre-Session drawer), not at plugin boot.
|
|
3318
|
+
React.useEffect(function () {
|
|
3319
|
+
notes.ensureImages();
|
|
3320
|
+
}, [notes]);
|
|
3321
|
+
// A memo created from the chat header asks to be opened here. The panel can
|
|
3322
|
+
// mount after the request was filed, so the store is read on mount as well;
|
|
3323
|
+
// the request is consumed exactly once.
|
|
3324
|
+
React.useEffect(function () {
|
|
3325
|
+
if (requestedNoteId === null)
|
|
3326
|
+
return;
|
|
3327
|
+
openRequest.set(null);
|
|
3328
|
+
var found = false;
|
|
3329
|
+
for (var index = 0; index < snapshot.notes.length; index += 1) {
|
|
3330
|
+
if (snapshot.notes[index].id === requestedNoteId) {
|
|
3331
|
+
found = true;
|
|
3332
|
+
break;
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
if (!found)
|
|
3336
|
+
return;
|
|
3337
|
+
setTransfer(null);
|
|
3338
|
+
setQuery('');
|
|
3339
|
+
setCopiedId(null);
|
|
3340
|
+
setEditorCopied(false);
|
|
3341
|
+
setDeleteArmed(false);
|
|
3342
|
+
setNewId(null);
|
|
3343
|
+
setEditingId(requestedNoteId);
|
|
3344
|
+
}, [requestedNoteId, openRequest, snapshot.notes]);
|
|
2931
3345
|
var now = Date.now();
|
|
2932
3346
|
var normalizedQuery = query.trim().toLocaleLowerCase();
|
|
2933
3347
|
var labelFilterActive = activeLabels.length > 0;
|
|
@@ -3183,6 +3597,103 @@ window.__ModuleLoader__.load({
|
|
|
3183
3597
|
}
|
|
3184
3598
|
}
|
|
3185
3599
|
}
|
|
3600
|
+
var menuTarget;
|
|
3601
|
+
if (menu !== null) {
|
|
3602
|
+
for (var menuAt = 0; menuAt < snapshot.notes.length; menuAt += 1) {
|
|
3603
|
+
if (snapshot.notes[menuAt].id === menu.id) {
|
|
3604
|
+
menuTarget = snapshot.notes[menuAt];
|
|
3605
|
+
break;
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
// Drop the menu when its memo disappears (deleted elsewhere, imported over, ...).
|
|
3610
|
+
React.useEffect(function () {
|
|
3611
|
+
if (menu === null || menuTarget !== undefined)
|
|
3612
|
+
return;
|
|
3613
|
+
setMenu(null);
|
|
3614
|
+
setMenuDeleteArmed(false);
|
|
3615
|
+
setMenuPosition(null);
|
|
3616
|
+
}, [menu, menuTarget, setMenu, setMenuDeleteArmed, setMenuPosition]);
|
|
3617
|
+
// Measure after render so the menu can be clamped or flipped inside the panel.
|
|
3618
|
+
React.useLayoutEffect(function () {
|
|
3619
|
+
if (menu === null)
|
|
3620
|
+
return;
|
|
3621
|
+
var root = rootRef.current;
|
|
3622
|
+
var element = menuRef.current;
|
|
3623
|
+
if (root === null || element === null)
|
|
3624
|
+
return;
|
|
3625
|
+
var next = placeContextMenu({ x: menu.x, y: menu.y }, { width: element.offsetWidth, height: element.offsetHeight }, { width: root.clientWidth, height: root.clientHeight });
|
|
3626
|
+
setMenuPosition(function (current) {
|
|
3627
|
+
if (current !== null && current.left === next.left && current.top === next.top)
|
|
3628
|
+
return current;
|
|
3629
|
+
return next;
|
|
3630
|
+
});
|
|
3631
|
+
}, [menu, menuDeleteArmed, setMenuPosition]);
|
|
3632
|
+
React.useLayoutEffect(function () {
|
|
3633
|
+
if (menu === null)
|
|
3634
|
+
return;
|
|
3635
|
+
var element = menuRef.current;
|
|
3636
|
+
if (element === null)
|
|
3637
|
+
return;
|
|
3638
|
+
var items = element.querySelectorAll('[role="menuitem"]');
|
|
3639
|
+
if (items.length === 0)
|
|
3640
|
+
return;
|
|
3641
|
+
try {
|
|
3642
|
+
items[0].focus();
|
|
3643
|
+
}
|
|
3644
|
+
catch (error) {
|
|
3645
|
+
// Focus is best effort; the menu still works with the mouse.
|
|
3646
|
+
}
|
|
3647
|
+
}, [menu]);
|
|
3648
|
+
React.useEffect(function () {
|
|
3649
|
+
if (menu === null)
|
|
3650
|
+
return undefined;
|
|
3651
|
+
if (typeof document === 'undefined' || typeof window === 'undefined')
|
|
3652
|
+
return undefined;
|
|
3653
|
+
function isInsideMenu(target) {
|
|
3654
|
+
var element = menuRef.current;
|
|
3655
|
+
if (element === null || target === null)
|
|
3656
|
+
return false;
|
|
3657
|
+
return element.contains(target);
|
|
3658
|
+
}
|
|
3659
|
+
function onPointerDown(event) {
|
|
3660
|
+
if (isInsideMenu(event.target))
|
|
3661
|
+
return;
|
|
3662
|
+
closeRowMenu(false);
|
|
3663
|
+
}
|
|
3664
|
+
function onKeyDown(event) {
|
|
3665
|
+
if (event.key !== 'Escape')
|
|
3666
|
+
return;
|
|
3667
|
+
event.preventDefault();
|
|
3668
|
+
event.stopPropagation();
|
|
3669
|
+
closeRowMenu(true);
|
|
3670
|
+
}
|
|
3671
|
+
function onDismiss() {
|
|
3672
|
+
closeRowMenu(false);
|
|
3673
|
+
}
|
|
3674
|
+
document.addEventListener('pointerdown', onPointerDown, true);
|
|
3675
|
+
document.addEventListener('keydown', onKeyDown, true);
|
|
3676
|
+
document.addEventListener('scroll', onDismiss, true);
|
|
3677
|
+
window.addEventListener('resize', onDismiss);
|
|
3678
|
+
window.addEventListener('blur', onDismiss);
|
|
3679
|
+
return function () {
|
|
3680
|
+
document.removeEventListener('pointerdown', onPointerDown, true);
|
|
3681
|
+
document.removeEventListener('keydown', onKeyDown, true);
|
|
3682
|
+
document.removeEventListener('scroll', onDismiss, true);
|
|
3683
|
+
window.removeEventListener('resize', onDismiss);
|
|
3684
|
+
window.removeEventListener('blur', onDismiss);
|
|
3685
|
+
};
|
|
3686
|
+
}, [menu]);
|
|
3687
|
+
React.useEffect(function () {
|
|
3688
|
+
if (!menuDeleteArmed)
|
|
3689
|
+
return undefined;
|
|
3690
|
+
var timer = setTimeout(function () {
|
|
3691
|
+
setMenuDeleteArmed(false);
|
|
3692
|
+
}, DELETE_ARM_MS);
|
|
3693
|
+
return function () {
|
|
3694
|
+
clearTimeout(timer);
|
|
3695
|
+
};
|
|
3696
|
+
}, [menuDeleteArmed, setMenuDeleteArmed]);
|
|
3186
3697
|
React.useEffect(function () {
|
|
3187
3698
|
if (editingId !== null && editing === undefined) {
|
|
3188
3699
|
setEditingId(null);
|
|
@@ -3492,6 +4003,130 @@ window.__ModuleLoader__.load({
|
|
|
3492
4003
|
setNewId(null);
|
|
3493
4004
|
setDeleteArmed(false);
|
|
3494
4005
|
}
|
|
4006
|
+
function openRowMenu(note, clientX, clientY, returnFocus) {
|
|
4007
|
+
var root = rootRef.current;
|
|
4008
|
+
var rect = root === null ? null : root.getBoundingClientRect();
|
|
4009
|
+
var x = rect === null ? clientX : clientX - rect.left;
|
|
4010
|
+
var y = rect === null ? clientY : clientY - rect.top;
|
|
4011
|
+
menuReturnFocusRef.current = returnFocus;
|
|
4012
|
+
setMenuDeleteArmed(false);
|
|
4013
|
+
setMenuPosition(null);
|
|
4014
|
+
setMenu({ id: note.id, x: x, y: y });
|
|
4015
|
+
}
|
|
4016
|
+
function closeRowMenu(restoreFocus) {
|
|
4017
|
+
var target = menuReturnFocusRef.current;
|
|
4018
|
+
menuReturnFocusRef.current = null;
|
|
4019
|
+
setMenu(null);
|
|
4020
|
+
setMenuDeleteArmed(false);
|
|
4021
|
+
setMenuPosition(null);
|
|
4022
|
+
if (!restoreFocus || target === null)
|
|
4023
|
+
return;
|
|
4024
|
+
if (typeof target.isConnected === 'boolean' && !target.isConnected)
|
|
4025
|
+
return;
|
|
4026
|
+
try {
|
|
4027
|
+
target.focus();
|
|
4028
|
+
}
|
|
4029
|
+
catch (error) {
|
|
4030
|
+
/* focusing is best effort */
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
function menuNote() {
|
|
4034
|
+
if (menu === null)
|
|
4035
|
+
return undefined;
|
|
4036
|
+
for (var index = 0; index < snapshot.notes.length; index += 1) {
|
|
4037
|
+
if (snapshot.notes[index].id === menu.id)
|
|
4038
|
+
return snapshot.notes[index];
|
|
4039
|
+
}
|
|
4040
|
+
return undefined;
|
|
4041
|
+
}
|
|
4042
|
+
function handleMenuOpen() {
|
|
4043
|
+
var note = menuNote();
|
|
4044
|
+
closeRowMenu(true);
|
|
4045
|
+
if (note !== undefined)
|
|
4046
|
+
openNote(note.id);
|
|
4047
|
+
}
|
|
4048
|
+
function handleMenuCopy() {
|
|
4049
|
+
var note = menuNote();
|
|
4050
|
+
closeRowMenu(true);
|
|
4051
|
+
if (note !== undefined)
|
|
4052
|
+
copyNote(note);
|
|
4053
|
+
}
|
|
4054
|
+
function handleMenuDuplicate() {
|
|
4055
|
+
var note = menuNote();
|
|
4056
|
+
closeRowMenu(true);
|
|
4057
|
+
if (note === undefined)
|
|
4058
|
+
return;
|
|
4059
|
+
var copy = notes.duplicate(note.id);
|
|
4060
|
+
if (copy === undefined) {
|
|
4061
|
+
setAnnounce(t('limit.reached', { max: limits.maxNotes }));
|
|
4062
|
+
return;
|
|
4063
|
+
}
|
|
4064
|
+
setAnnounce(t('menu.duplicated'));
|
|
4065
|
+
}
|
|
4066
|
+
function handleMenuFavorite() {
|
|
4067
|
+
var note = menuNote();
|
|
4068
|
+
closeRowMenu(true);
|
|
4069
|
+
if (note !== undefined)
|
|
4070
|
+
toggleFavorite(note);
|
|
4071
|
+
}
|
|
4072
|
+
function handleMenuDelete() {
|
|
4073
|
+
var note = menuNote();
|
|
4074
|
+
if (note === undefined) {
|
|
4075
|
+
closeRowMenu(false);
|
|
4076
|
+
return;
|
|
4077
|
+
}
|
|
4078
|
+
if (!menuDeleteArmed) {
|
|
4079
|
+
setMenuDeleteArmed(true);
|
|
4080
|
+
return;
|
|
4081
|
+
}
|
|
4082
|
+
closeRowMenu(false);
|
|
4083
|
+
notes.remove(note.id);
|
|
4084
|
+
notes.flush();
|
|
4085
|
+
setAnnounce(t('menu.deleted'));
|
|
4086
|
+
}
|
|
4087
|
+
function moveMenuFocus(event) {
|
|
4088
|
+
if (event.key === 'Tab') {
|
|
4089
|
+
closeRowMenu(false);
|
|
4090
|
+
return;
|
|
4091
|
+
}
|
|
4092
|
+
var forward = event.key === 'ArrowDown';
|
|
4093
|
+
var backward = event.key === 'ArrowUp';
|
|
4094
|
+
var home = event.key === 'Home';
|
|
4095
|
+
var end = event.key === 'End';
|
|
4096
|
+
if (!forward && !backward && !home && !end)
|
|
4097
|
+
return;
|
|
4098
|
+
event.preventDefault();
|
|
4099
|
+
event.stopPropagation();
|
|
4100
|
+
var element = menuRef.current;
|
|
4101
|
+
if (element === null)
|
|
4102
|
+
return;
|
|
4103
|
+
var items = element.querySelectorAll('[role="menuitem"]');
|
|
4104
|
+
if (items.length === 0)
|
|
4105
|
+
return;
|
|
4106
|
+
var active = document.activeElement;
|
|
4107
|
+
var current = -1;
|
|
4108
|
+
for (var index = 0; index < items.length; index += 1) {
|
|
4109
|
+
if (items[index] === active) {
|
|
4110
|
+
current = index;
|
|
4111
|
+
break;
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
4114
|
+
var next;
|
|
4115
|
+
if (home)
|
|
4116
|
+
next = 0;
|
|
4117
|
+
else if (end)
|
|
4118
|
+
next = items.length - 1;
|
|
4119
|
+
else if (forward)
|
|
4120
|
+
next = current < 0 ? 0 : (current + 1) % items.length;
|
|
4121
|
+
else
|
|
4122
|
+
next = current < 0 ? items.length - 1 : (current - 1 + items.length) % items.length;
|
|
4123
|
+
try {
|
|
4124
|
+
items[next].focus();
|
|
4125
|
+
}
|
|
4126
|
+
catch (error) {
|
|
4127
|
+
/* focusing is best effort */
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
3495
4130
|
function updateEditing(field, value) {
|
|
3496
4131
|
if (editing === undefined)
|
|
3497
4132
|
return;
|
|
@@ -4266,6 +4901,20 @@ window.__ModuleLoader__.load({
|
|
|
4266
4901
|
}
|
|
4267
4902
|
fillMemoDragData(event.dataTransfer, note);
|
|
4268
4903
|
setDraggingId(note.id);
|
|
4904
|
+
},
|
|
4905
|
+
onContextMenu: function (event) {
|
|
4906
|
+
event.preventDefault();
|
|
4907
|
+
event.stopPropagation();
|
|
4908
|
+
var focusTarget = event.currentTarget.querySelector('.dsm-row-main');
|
|
4909
|
+
var x = event.clientX;
|
|
4910
|
+
var y = event.clientY;
|
|
4911
|
+
if (x === 0 && y === 0) {
|
|
4912
|
+
// Keyboard-triggered context menu: anchor it to the row instead of 0,0.
|
|
4913
|
+
var rect = event.currentTarget.getBoundingClientRect();
|
|
4914
|
+
x = rect.left + Math.min(24, rect.width / 2);
|
|
4915
|
+
y = rect.bottom - 6;
|
|
4916
|
+
}
|
|
4917
|
+
openRowMenu(note, x, y, focusTarget);
|
|
4269
4918
|
}
|
|
4270
4919
|
}, h('button', {
|
|
4271
4920
|
type: 'button',
|
|
@@ -4273,6 +4922,15 @@ window.__ModuleLoader__.load({
|
|
|
4273
4922
|
title: title,
|
|
4274
4923
|
onClick: function () {
|
|
4275
4924
|
openNote(note.id);
|
|
4925
|
+
},
|
|
4926
|
+
onKeyDown: function (event) {
|
|
4927
|
+
var isMenuKey = event.key === 'ContextMenu' || (event.shiftKey && event.key === 'F10');
|
|
4928
|
+
if (!isMenuKey)
|
|
4929
|
+
return;
|
|
4930
|
+
event.preventDefault();
|
|
4931
|
+
event.stopPropagation();
|
|
4932
|
+
var rect = event.currentTarget.getBoundingClientRect();
|
|
4933
|
+
openRowMenu(note, rect.left + Math.min(24, rect.width / 2), rect.bottom - 6, event.currentTarget);
|
|
4276
4934
|
}
|
|
4277
4935
|
}, h('span', { className: 'dsm-row-title' }, h('span', { className: 'dsm-row-title-text' }, title), imageTotal > 0
|
|
4278
4936
|
? h('span', { className: 'dsm-row-images', title: imageCountLabel(t, imageTotal), 'aria-label': imageCountLabel(t, imageTotal) }, h(IconImage, { size: 11 }), String(imageTotal))
|
|
@@ -4333,7 +4991,33 @@ window.__ModuleLoader__.load({
|
|
|
4333
4991
|
? t('count.one')
|
|
4334
4992
|
: t('count.other', { count: snapshot.notes.length });
|
|
4335
4993
|
}
|
|
4336
|
-
|
|
4994
|
+
var menuElement = null;
|
|
4995
|
+
if (menu !== null && menuTarget !== undefined) {
|
|
4996
|
+
var targetNote = menuTarget;
|
|
4997
|
+
var menuItems = [
|
|
4998
|
+
contextMenuItem('open', t('menu.open'), h(IconNote, { size: 14 }), false, handleMenuOpen),
|
|
4999
|
+
contextMenuItem('copy', t('copy'), h(IconCopy, { size: 14 }), false, handleMenuCopy),
|
|
5000
|
+
contextMenuItem('duplicate', t('menu.duplicate'), h(IconPlus, { size: 14 }), false, handleMenuDuplicate),
|
|
5001
|
+
contextMenuItem('favorite', targetNote.favorite === true ? t('favorite.remove') : t('favorite.add'), targetNote.favorite === true ? h(IconStarFilled, { size: 14 }) : h(IconStar, { size: 14 }), false, handleMenuFavorite),
|
|
5002
|
+
h('div', { className: 'dsm-menu-sep', role: 'separator', key: 'sep' }),
|
|
5003
|
+
contextMenuItem('delete', menuDeleteArmed ? t('delete.confirm') : t('delete'), h(IconX, { size: 14 }), true, handleMenuDelete)
|
|
5004
|
+
];
|
|
5005
|
+
menuElement = h('div', {
|
|
5006
|
+
className: 'dsm-menu',
|
|
5007
|
+
role: 'menu',
|
|
5008
|
+
'aria-label': t('menu.label'),
|
|
5009
|
+
ref: menuRef,
|
|
5010
|
+
style: {
|
|
5011
|
+
left: String(menuPosition === null ? menu.x : menuPosition.left) + 'px',
|
|
5012
|
+
top: String(menuPosition === null ? menu.y : menuPosition.top) + 'px'
|
|
5013
|
+
},
|
|
5014
|
+
onKeyDown: moveMenuFocus,
|
|
5015
|
+
onContextMenu: function (event) {
|
|
5016
|
+
event.preventDefault();
|
|
5017
|
+
}
|
|
5018
|
+
}, menuItems);
|
|
5019
|
+
}
|
|
5020
|
+
return h('div', { className: 'dsm-root', 'data-side-memo': 'list', ref: rootRef }, h('div', { className: 'dsm-toolbar' }, h('div', { className: 'dsm-search' }, h('span', { className: 'dsm-search-icon' }, h(IconSearch, { size: 14 })), h('input', {
|
|
4337
5021
|
type: 'search',
|
|
4338
5022
|
value: query,
|
|
4339
5023
|
placeholder: t('search.placeholder'),
|
|
@@ -4420,7 +5104,7 @@ window.__ModuleLoader__.load({
|
|
|
4420
5104
|
}, t('labels.all'))
|
|
4421
5105
|
: null), h('div', { className: 'dsm-list-scroll' }, listContent), h('div', { className: 'dsm-foot' }, h('span', null, countLabel), visibleNotes.length === 0 || props.inputActions === undefined
|
|
4422
5106
|
? null
|
|
4423
|
-
: h('span', { className: 'dsm-foot-hint', title: t('drag.hint') }, t('drag.hint')), atLimit ? h('span', { title: t('limit.hint', { max: limits.maxNotes }) }, t('limit.reached', { max: limits.maxNotes })) : null), liveRegion);
|
|
5107
|
+
: h('span', { className: 'dsm-foot-hint', title: t('drag.hint') }, t('drag.hint')), atLimit ? h('span', { title: t('limit.hint', { max: limits.maxNotes }) }, t('limit.reached', { max: limits.maxNotes })) : null), liveRegion, menuElement);
|
|
4424
5108
|
}
|
|
4425
5109
|
// ---------------------------------------------------------------------
|
|
4426
5110
|
// Chip title and header toggle
|
|
@@ -4445,6 +5129,387 @@ window.__ModuleLoader__.load({
|
|
|
4445
5129
|
}
|
|
4446
5130
|
}, h(IconNote, { size: 16 }));
|
|
4447
5131
|
}
|
|
5132
|
+
/**
|
|
5133
|
+
* "Save this conversation as a memo" header button.
|
|
5134
|
+
*
|
|
5135
|
+
* The chat snapshot hook only exists inside a Session-scope slot, so this
|
|
5136
|
+
* wrapper renders nothing on hosts that do not supply it and defers the real
|
|
5137
|
+
* button — whose hook calls must be unconditional — to a child component.
|
|
5138
|
+
*/
|
|
5139
|
+
function NotesChatMemo(props) {
|
|
5140
|
+
if (typeof props.useChat !== 'function')
|
|
5141
|
+
return null;
|
|
5142
|
+
return h(NotesChatMemoButton, props);
|
|
5143
|
+
}
|
|
5144
|
+
function chatVisibleNodes(chat) {
|
|
5145
|
+
var list = [];
|
|
5146
|
+
if (chat === null || chat === undefined)
|
|
5147
|
+
return list;
|
|
5148
|
+
var order = Array.isArray(chat.order) ? chat.order : [];
|
|
5149
|
+
var nodes = chat.nodes;
|
|
5150
|
+
if (nodes === null || nodes === undefined || typeof nodes.get !== 'function')
|
|
5151
|
+
return list;
|
|
5152
|
+
for (var index = 0; index < order.length; index += 1) {
|
|
5153
|
+
var node = nodes.get(order[index]);
|
|
5154
|
+
if (node !== null && node !== undefined)
|
|
5155
|
+
list.push(node);
|
|
5156
|
+
}
|
|
5157
|
+
return list;
|
|
5158
|
+
}
|
|
5159
|
+
/**
|
|
5160
|
+
* Menu body for "Save chat as memo". The chat subscription lives here so it
|
|
5161
|
+
* exists only while the menu is open; the closed header button subscribes to
|
|
5162
|
+
* nothing and therefore never re-renders when the conversation changes.
|
|
5163
|
+
*/
|
|
5164
|
+
function ChatMemoMenu(props) {
|
|
5165
|
+
var t = props.t;
|
|
5166
|
+
var chat = props.useChat(function (snapshot) {
|
|
5167
|
+
return snapshot;
|
|
5168
|
+
});
|
|
5169
|
+
var labels = {
|
|
5170
|
+
you: t('chat.you'),
|
|
5171
|
+
assistant: t('chat.assistant'),
|
|
5172
|
+
fallbackTitle: t('chat.fallbackTitle')
|
|
5173
|
+
};
|
|
5174
|
+
function pick(mode) {
|
|
5175
|
+
props.onPick(mode, chatMemoDraft(chatVisibleNodes(chat), labels, mode));
|
|
5176
|
+
}
|
|
5177
|
+
var menuStyle = props.anchor === null ? undefined : { left: props.anchor.left + 'px', top: props.anchor.top + 'px' };
|
|
5178
|
+
return h('div', {
|
|
5179
|
+
className: 'dsm-chat-memo-menu',
|
|
5180
|
+
role: 'menu',
|
|
5181
|
+
'aria-label': t('chat.menu.label'),
|
|
5182
|
+
style: menuStyle
|
|
5183
|
+
}, h('button', {
|
|
5184
|
+
type: 'button',
|
|
5185
|
+
role: 'menuitem',
|
|
5186
|
+
className: 'dsm-chat-memo-item',
|
|
5187
|
+
onClick: function () {
|
|
5188
|
+
pick('chat');
|
|
5189
|
+
}
|
|
5190
|
+
}, h('span', null, t('chat.menu.chat')), h('span', { className: 'dsm-chat-memo-note' }, t('chat.menu.chatNote'))), h('button', {
|
|
5191
|
+
type: 'button',
|
|
5192
|
+
role: 'menuitem',
|
|
5193
|
+
className: 'dsm-chat-memo-item',
|
|
5194
|
+
onClick: function () {
|
|
5195
|
+
pick('prompts');
|
|
5196
|
+
}
|
|
5197
|
+
}, h('span', null, t('chat.menu.prompts')), h('span', { className: 'dsm-chat-memo-note' }, t('chat.menu.promptsNote'))));
|
|
5198
|
+
}
|
|
5199
|
+
function NotesChatMemoButton(props) {
|
|
5200
|
+
var t = translateFor(props);
|
|
5201
|
+
var notes = props.notes;
|
|
5202
|
+
var openRequest = props.openRequest;
|
|
5203
|
+
var feedbackState = React.useState('');
|
|
5204
|
+
var feedback = feedbackState[0];
|
|
5205
|
+
var setFeedback = feedbackState[1];
|
|
5206
|
+
var menuState = React.useState(false);
|
|
5207
|
+
var menuOpen = menuState[0];
|
|
5208
|
+
var setMenuOpen = menuState[1];
|
|
5209
|
+
var anchorState = React.useState(null);
|
|
5210
|
+
var anchor = anchorState[0];
|
|
5211
|
+
var setAnchor = anchorState[1];
|
|
5212
|
+
var rootRef = React.useRef(null);
|
|
5213
|
+
var buttonRef = React.useRef(null);
|
|
5214
|
+
React.useEffect(function () {
|
|
5215
|
+
if (feedback === '')
|
|
5216
|
+
return undefined;
|
|
5217
|
+
var timer = setTimeout(function () {
|
|
5218
|
+
setFeedback('');
|
|
5219
|
+
}, feedback === 'saved' ? COPY_FEEDBACK_MS : COPY_FAILED_MS);
|
|
5220
|
+
return function () {
|
|
5221
|
+
clearTimeout(timer);
|
|
5222
|
+
};
|
|
5223
|
+
}, [feedback]);
|
|
5224
|
+
// The header clips its own children, so the menu is fixed-positioned under
|
|
5225
|
+
// the button and dismissed like a native menu: outside press, Escape, or
|
|
5226
|
+
// any scroll/resize that would leave it floating in the wrong place.
|
|
5227
|
+
React.useEffect(function () {
|
|
5228
|
+
if (!menuOpen)
|
|
5229
|
+
return undefined;
|
|
5230
|
+
function close() {
|
|
5231
|
+
setMenuOpen(false);
|
|
5232
|
+
}
|
|
5233
|
+
function onPointerDown(event) {
|
|
5234
|
+
var root = rootRef.current;
|
|
5235
|
+
if (root !== null && typeof root.contains === 'function' && root.contains(event.target))
|
|
5236
|
+
return;
|
|
5237
|
+
close();
|
|
5238
|
+
}
|
|
5239
|
+
function onKeyDown(event) {
|
|
5240
|
+
if (event.key === 'Escape')
|
|
5241
|
+
close();
|
|
5242
|
+
}
|
|
5243
|
+
document.addEventListener('pointerdown', onPointerDown, true);
|
|
5244
|
+
document.addEventListener('keydown', onKeyDown);
|
|
5245
|
+
window.addEventListener('resize', close);
|
|
5246
|
+
window.addEventListener('scroll', close, true);
|
|
5247
|
+
return function () {
|
|
5248
|
+
document.removeEventListener('pointerdown', onPointerDown, true);
|
|
5249
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
5250
|
+
window.removeEventListener('resize', close);
|
|
5251
|
+
window.removeEventListener('scroll', close, true);
|
|
5252
|
+
};
|
|
5253
|
+
}, [menuOpen]);
|
|
5254
|
+
function openMenu() {
|
|
5255
|
+
var button = buttonRef.current;
|
|
5256
|
+
if (button !== null && typeof button.getBoundingClientRect === 'function') {
|
|
5257
|
+
var rect = button.getBoundingClientRect();
|
|
5258
|
+
var width = 226;
|
|
5259
|
+
var viewport = typeof window === 'undefined' ? width : window.innerWidth;
|
|
5260
|
+
var left = Math.max(8, Math.min(rect.right - width, viewport - width - 8));
|
|
5261
|
+
setAnchor({ left: left, top: rect.bottom + 6 });
|
|
5262
|
+
}
|
|
5263
|
+
setMenuOpen(true);
|
|
5264
|
+
}
|
|
5265
|
+
function take(mode, draft) {
|
|
5266
|
+
setMenuOpen(false);
|
|
5267
|
+
if (draft === null) {
|
|
5268
|
+
setFeedback(mode === 'prompts' ? 'empty-prompts' : 'empty');
|
|
5269
|
+
return;
|
|
5270
|
+
}
|
|
5271
|
+
var note = notes.create();
|
|
5272
|
+
if (note === undefined) {
|
|
5273
|
+
setFeedback('limit');
|
|
5274
|
+
return;
|
|
5275
|
+
}
|
|
5276
|
+
notes.update(note.id, { title: draft.title, body: draft.body });
|
|
5277
|
+
notes.flush();
|
|
5278
|
+
openRequest.set(note.id);
|
|
5279
|
+
if (typeof props.reveal === 'function')
|
|
5280
|
+
props.reveal();
|
|
5281
|
+
setFeedback('saved');
|
|
5282
|
+
}
|
|
5283
|
+
var saved = feedback === 'saved';
|
|
5284
|
+
var failed = feedback !== '' && !saved;
|
|
5285
|
+
var label = saved
|
|
5286
|
+
? t('chat.saved')
|
|
5287
|
+
: feedback === 'limit'
|
|
5288
|
+
? t('chat.limit')
|
|
5289
|
+
: feedback === 'empty-prompts'
|
|
5290
|
+
? t('chat.emptyPrompts')
|
|
5291
|
+
: feedback === 'empty'
|
|
5292
|
+
? t('chat.empty')
|
|
5293
|
+
: t('chat.create');
|
|
5294
|
+
return h('div', { className: 'dsm-chat-memo', ref: rootRef }, h('button', {
|
|
5295
|
+
type: 'button',
|
|
5296
|
+
className: 'dsm-toggle',
|
|
5297
|
+
ref: buttonRef,
|
|
5298
|
+
'data-side-memo-chat-memo': 'true',
|
|
5299
|
+
'data-state': saved ? 'saved' : failed ? 'failed' : 'idle',
|
|
5300
|
+
'data-active': menuOpen ? 'true' : 'false',
|
|
5301
|
+
'aria-haspopup': 'menu',
|
|
5302
|
+
'aria-expanded': menuOpen ? 'true' : 'false',
|
|
5303
|
+
title: label,
|
|
5304
|
+
'aria-label': saved || failed ? label : t('chat.createAria'),
|
|
5305
|
+
onClick: function () {
|
|
5306
|
+
if (menuOpen)
|
|
5307
|
+
setMenuOpen(false);
|
|
5308
|
+
else
|
|
5309
|
+
openMenu();
|
|
5310
|
+
}
|
|
5311
|
+
}, saved ? h(IconCheck, { size: 16 }) : h(IconChatMemo, { size: 16 })), menuOpen
|
|
5312
|
+
? h(ChatMemoMenu, {
|
|
5313
|
+
t: t,
|
|
5314
|
+
useChat: props.useChat,
|
|
5315
|
+
anchor: anchor,
|
|
5316
|
+
onPick: take
|
|
5317
|
+
})
|
|
5318
|
+
: null, h('span', { className: 'dsm-sr-only', role: 'status', 'aria-live': 'polite' }, saved ? t('chat.saved') : ''));
|
|
5319
|
+
}
|
|
5320
|
+
/**
|
|
5321
|
+
* Quick "save this prompt as a memo" affordance.
|
|
5322
|
+
*
|
|
5323
|
+
* DSH renders a user message's action row itself and exposes no slot for it,
|
|
5324
|
+
* so the button is fixed-positioned next to that row and follows the pointer
|
|
5325
|
+
* from message to message. The prompt text is read from the
|
|
5326
|
+
* `conversation.chat.user.content` slot anchor, and the button lives in the
|
|
5327
|
+
* click-through `shell.overlay` layer so the message list's overflow and
|
|
5328
|
+
* hover scopes cannot clip it.
|
|
5329
|
+
*/
|
|
5330
|
+
function NotesPromptMemo(props) {
|
|
5331
|
+
var t = translateFor(props);
|
|
5332
|
+
var labels = {
|
|
5333
|
+
you: t('chat.you'),
|
|
5334
|
+
assistant: t('chat.assistant'),
|
|
5335
|
+
fallbackTitle: t('chat.fallbackTitle')
|
|
5336
|
+
};
|
|
5337
|
+
var notes = props.notes;
|
|
5338
|
+
var openRequest = props.openRequest;
|
|
5339
|
+
var labelsRef = React.useRef(labels);
|
|
5340
|
+
labelsRef.current = labels;
|
|
5341
|
+
var targetState = React.useState(null);
|
|
5342
|
+
var target = targetState[0];
|
|
5343
|
+
var setTarget = targetState[1];
|
|
5344
|
+
var targetRef = React.useRef(target);
|
|
5345
|
+
targetRef.current = target;
|
|
5346
|
+
var feedbackState = React.useState('');
|
|
5347
|
+
var feedback = feedbackState[0];
|
|
5348
|
+
var setFeedback = feedbackState[1];
|
|
5349
|
+
var feedbackRef = React.useRef(feedback);
|
|
5350
|
+
feedbackRef.current = feedback;
|
|
5351
|
+
var hideTimer = React.useRef(null);
|
|
5352
|
+
function clearHide() {
|
|
5353
|
+
var timer = hideTimer.current;
|
|
5354
|
+
if (timer === null)
|
|
5355
|
+
return;
|
|
5356
|
+
clearTimeout(timer);
|
|
5357
|
+
hideTimer.current = null;
|
|
5358
|
+
}
|
|
5359
|
+
function hide() {
|
|
5360
|
+
clearHide();
|
|
5361
|
+
setTarget(null);
|
|
5362
|
+
}
|
|
5363
|
+
function scheduleHide() {
|
|
5364
|
+
clearHide();
|
|
5365
|
+
hideTimer.current = window.setTimeout(function () {
|
|
5366
|
+
hideTimer.current = null;
|
|
5367
|
+
setTarget(null);
|
|
5368
|
+
}, 140);
|
|
5369
|
+
}
|
|
5370
|
+
React.useEffect(function () {
|
|
5371
|
+
if (feedback === '')
|
|
5372
|
+
return undefined;
|
|
5373
|
+
var timer = setTimeout(function () {
|
|
5374
|
+
setFeedback('');
|
|
5375
|
+
}, feedback === 'saved' ? COPY_FEEDBACK_MS : COPY_FAILED_MS);
|
|
5376
|
+
return function () {
|
|
5377
|
+
clearTimeout(timer);
|
|
5378
|
+
};
|
|
5379
|
+
}, [feedback]);
|
|
5380
|
+
React.useEffect(function () {
|
|
5381
|
+
if (typeof document === 'undefined')
|
|
5382
|
+
return undefined;
|
|
5383
|
+
function elementOf(value) {
|
|
5384
|
+
return value !== null && typeof value === 'object' && 'closest' in value ? value : null;
|
|
5385
|
+
}
|
|
5386
|
+
function locate(item) {
|
|
5387
|
+
var row = item.querySelector('[data-clock="start"]');
|
|
5388
|
+
if (row === null)
|
|
5389
|
+
return null;
|
|
5390
|
+
var copy = row.querySelector('button');
|
|
5391
|
+
if (copy === null)
|
|
5392
|
+
return null;
|
|
5393
|
+
var copyBox = copy.getBoundingClientRect();
|
|
5394
|
+
if (copyBox.width === 0 && copyBox.height === 0)
|
|
5395
|
+
return null;
|
|
5396
|
+
var first = row.children.length === 0 ? null : row.children[0];
|
|
5397
|
+
var time = first !== null && first.tagName === 'SPAN' ? first : null;
|
|
5398
|
+
var left = Math.round(copyBox.right + 5);
|
|
5399
|
+
if (time !== null) {
|
|
5400
|
+
var timeBox = time.getBoundingClientRect();
|
|
5401
|
+
if (timeBox.right + 26 <= copyBox.left)
|
|
5402
|
+
left = Math.round(timeBox.right + 4);
|
|
5403
|
+
else
|
|
5404
|
+
left = Math.round((timeBox.right + copyBox.left) / 2 - 11);
|
|
5405
|
+
}
|
|
5406
|
+
var content = item.querySelector('[data-slot="conversation.chat.user.content"]');
|
|
5407
|
+
var text = content === null ? '' : content.innerText || content.textContent || '';
|
|
5408
|
+
if (promptMemoDraft(text, labelsRef.current) === null)
|
|
5409
|
+
return null;
|
|
5410
|
+
return {
|
|
5411
|
+
text: text,
|
|
5412
|
+
left: left,
|
|
5413
|
+
top: Math.round(copyBox.top + copyBox.height / 2 - 11)
|
|
5414
|
+
};
|
|
5415
|
+
}
|
|
5416
|
+
function onPointerOver(event) {
|
|
5417
|
+
var node = elementOf(event.target);
|
|
5418
|
+
if (node === null)
|
|
5419
|
+
return;
|
|
5420
|
+
if (node.closest('[data-side-memo-prompt-memo]') !== null) {
|
|
5421
|
+
clearHide();
|
|
5422
|
+
return;
|
|
5423
|
+
}
|
|
5424
|
+
var item = node.closest('[data-chat-flow-kind="user"]');
|
|
5425
|
+
if (item === null)
|
|
5426
|
+
return;
|
|
5427
|
+
var next = locate(item);
|
|
5428
|
+
if (next === null)
|
|
5429
|
+
return;
|
|
5430
|
+
var current = targetRef.current;
|
|
5431
|
+
if (current !== null && current.text === next.text && current.left === next.left && current.top === next.top)
|
|
5432
|
+
return;
|
|
5433
|
+
clearHide();
|
|
5434
|
+
if (feedbackRef.current !== '')
|
|
5435
|
+
setFeedback('');
|
|
5436
|
+
setTarget(next);
|
|
5437
|
+
}
|
|
5438
|
+
function onPointerOut(event) {
|
|
5439
|
+
var node = elementOf(event.target);
|
|
5440
|
+
if (node === null)
|
|
5441
|
+
return;
|
|
5442
|
+
var item = node.closest('[data-chat-flow-kind="user"]');
|
|
5443
|
+
if (item === null)
|
|
5444
|
+
return;
|
|
5445
|
+
var related = elementOf(event.relatedTarget);
|
|
5446
|
+
if (related !== null && (related.closest('[data-chat-flow-kind="user"]') === item || related.closest('[data-side-memo-prompt-memo]') !== null))
|
|
5447
|
+
return;
|
|
5448
|
+
scheduleHide();
|
|
5449
|
+
}
|
|
5450
|
+
function onPointerDown(event) {
|
|
5451
|
+
var node = elementOf(event.target);
|
|
5452
|
+
if (node !== null && node.closest('[data-side-memo-prompt-memo]') !== null)
|
|
5453
|
+
return;
|
|
5454
|
+
hide();
|
|
5455
|
+
}
|
|
5456
|
+
function onKeyDown(event) {
|
|
5457
|
+
if (event.key === 'Escape')
|
|
5458
|
+
hide();
|
|
5459
|
+
}
|
|
5460
|
+
document.addEventListener('pointerover', onPointerOver, true);
|
|
5461
|
+
document.addEventListener('pointerout', onPointerOut, true);
|
|
5462
|
+
document.addEventListener('pointerdown', onPointerDown, true);
|
|
5463
|
+
document.addEventListener('keydown', onKeyDown);
|
|
5464
|
+
window.addEventListener('scroll', hide, true);
|
|
5465
|
+
window.addEventListener('resize', hide);
|
|
5466
|
+
return function () {
|
|
5467
|
+
clearHide();
|
|
5468
|
+
document.removeEventListener('pointerover', onPointerOver, true);
|
|
5469
|
+
document.removeEventListener('pointerout', onPointerOut, true);
|
|
5470
|
+
document.removeEventListener('pointerdown', onPointerDown, true);
|
|
5471
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
5472
|
+
window.removeEventListener('scroll', hide, true);
|
|
5473
|
+
window.removeEventListener('resize', hide);
|
|
5474
|
+
};
|
|
5475
|
+
}, []);
|
|
5476
|
+
function save() {
|
|
5477
|
+
if (target === null)
|
|
5478
|
+
return;
|
|
5479
|
+
var draft = promptMemoDraft(target.text, labels);
|
|
5480
|
+
if (draft === null)
|
|
5481
|
+
return;
|
|
5482
|
+
var note = notes.create();
|
|
5483
|
+
if (note === undefined) {
|
|
5484
|
+
setFeedback('limit');
|
|
5485
|
+
return;
|
|
5486
|
+
}
|
|
5487
|
+
notes.update(note.id, { title: draft.title, body: draft.body });
|
|
5488
|
+
notes.flush();
|
|
5489
|
+
openRequest.set(note.id);
|
|
5490
|
+
if (typeof props.reveal === 'function')
|
|
5491
|
+
props.reveal();
|
|
5492
|
+
setFeedback('saved');
|
|
5493
|
+
}
|
|
5494
|
+
var saved = feedback === 'saved';
|
|
5495
|
+
var failed = feedback !== '' && !saved;
|
|
5496
|
+
var status = saved ? t('chat.saved') : feedback === 'limit' ? t('chat.limit') : '';
|
|
5497
|
+
if (target === null) {
|
|
5498
|
+
return h('span', { className: 'dsm-sr-only', role: 'status', 'aria-live': 'polite' }, status);
|
|
5499
|
+
}
|
|
5500
|
+
return h(React.Fragment, null, h('button', {
|
|
5501
|
+
type: 'button',
|
|
5502
|
+
className: 'dsm-prompt-memo',
|
|
5503
|
+
'data-side-memo-prompt-memo': 'true',
|
|
5504
|
+
'data-state': saved ? 'saved' : failed ? 'failed' : 'idle',
|
|
5505
|
+
style: { left: target.left + 'px', top: target.top + 'px' },
|
|
5506
|
+
title: saved ? t('chat.saved') : t('prompt.create'),
|
|
5507
|
+
'aria-label': t('prompt.createAria'),
|
|
5508
|
+
onClick: save,
|
|
5509
|
+
onPointerEnter: clearHide,
|
|
5510
|
+
onPointerLeave: scheduleHide
|
|
5511
|
+
}, saved ? h(IconCheck, { size: 13 }) : h(IconChatMemo, { size: 13 })), h('span', { className: 'dsm-sr-only', role: 'status', 'aria-live': 'polite' }, status));
|
|
5512
|
+
}
|
|
4448
5513
|
/**
|
|
4449
5514
|
* The header toggle for the phases without the normal Session chrome.
|
|
4450
5515
|
*
|
|
@@ -4594,7 +5659,7 @@ window.__ModuleLoader__.load({
|
|
|
4594
5659
|
onClick: function () {
|
|
4595
5660
|
drawer.set(false);
|
|
4596
5661
|
}
|
|
4597
|
-
}, h(IconX, { size: 16 }))), h('div', { className: 'dsm-drawer-body' }, h(NotesBody, { t: t, notes: props.notes }))));
|
|
5662
|
+
}, h(IconX, { size: 16 }))), h('div', { className: 'dsm-drawer-body' }, h(NotesBody, { t: t, notes: props.notes, openRequest: props.openRequest }))));
|
|
4598
5663
|
}
|
|
4599
5664
|
// ---------------------------------------------------------------------
|
|
4600
5665
|
// Plugin body
|
|
@@ -4620,6 +5685,7 @@ window.__ModuleLoader__.load({
|
|
|
4620
5685
|
fallbackT = ctx.locale.bind(NS);
|
|
4621
5686
|
var notes = createNotesStore();
|
|
4622
5687
|
var drawer = createDrawerStore();
|
|
5688
|
+
var openRequest = createOpenRequestStore();
|
|
4623
5689
|
var mountedBinding = createMountedBinding(function () {
|
|
4624
5690
|
return ctx.sidebarRight;
|
|
4625
5691
|
});
|
|
@@ -4719,6 +5785,22 @@ window.__ModuleLoader__.load({
|
|
|
4719
5785
|
}, 'could not toggle the notes panel in the right sidebar');
|
|
4720
5786
|
drawer.set(false);
|
|
4721
5787
|
}
|
|
5788
|
+
// Unlike toggleNotes, this never collapses an already-open panel: the
|
|
5789
|
+
// chat-memo button saved a memo and has to show it, not hide it.
|
|
5790
|
+
function revealNotes() {
|
|
5791
|
+
var mounted = readMountedSession(ctx.sidebarRight);
|
|
5792
|
+
if (mounted === undefined) {
|
|
5793
|
+
drawer.set(true);
|
|
5794
|
+
return;
|
|
5795
|
+
}
|
|
5796
|
+
retrySidebarCommand(function () {
|
|
5797
|
+
var right = ctx.sidebarRight;
|
|
5798
|
+
right.openTab(KIND);
|
|
5799
|
+
if (!right.isExpanded())
|
|
5800
|
+
right.toggleExpanded();
|
|
5801
|
+
}, 'could not open the notes panel in the right sidebar');
|
|
5802
|
+
drawer.set(false);
|
|
5803
|
+
}
|
|
4722
5804
|
ctx.effect(function () {
|
|
4723
5805
|
return ctx.slots.inject('conversation.session.header.utilities', function () {
|
|
4724
5806
|
return ctx.slots.register({
|
|
@@ -4732,6 +5814,37 @@ window.__ModuleLoader__.load({
|
|
|
4732
5814
|
}, NotesToggle);
|
|
4733
5815
|
});
|
|
4734
5816
|
}, 'side-memo: header toggle');
|
|
5817
|
+
// Turn the visible conversation into a memo in one click. The slot is
|
|
5818
|
+
// Session-scope, which is what supplies the standard `useChat` hook.
|
|
5819
|
+
ctx.effect(function () {
|
|
5820
|
+
return ctx.slots.inject('conversation.session.header.utilities', function () {
|
|
5821
|
+
return ctx.slots.register({
|
|
5822
|
+
name: 'conversation.session.header.utilities',
|
|
5823
|
+
id: 'side-memo-chat-memo',
|
|
5824
|
+
order: 31,
|
|
5825
|
+
locale: NS,
|
|
5826
|
+
inject: function () {
|
|
5827
|
+
return { reveal: revealNotes, notes: notes, openRequest: openRequest };
|
|
5828
|
+
}
|
|
5829
|
+
}, NotesChatMemo);
|
|
5830
|
+
});
|
|
5831
|
+
}, 'side-memo: chat memo');
|
|
5832
|
+
// One click next to a prompt turns that prompt into a memo. User messages
|
|
5833
|
+
// have no action slot, so the button rides the click-through overlay layer
|
|
5834
|
+
// and locates the hovered prompt in the message list.
|
|
5835
|
+
ctx.effect(function () {
|
|
5836
|
+
return ctx.slots.inject('shell.overlay', function () {
|
|
5837
|
+
return ctx.slots.register({
|
|
5838
|
+
name: 'shell.overlay',
|
|
5839
|
+
id: 'side-memo.prompt-memo',
|
|
5840
|
+
order: 45,
|
|
5841
|
+
locale: NS,
|
|
5842
|
+
inject: function () {
|
|
5843
|
+
return { notes: notes, openRequest: openRequest, reveal: revealNotes };
|
|
5844
|
+
}
|
|
5845
|
+
}, NotesPromptMemo);
|
|
5846
|
+
});
|
|
5847
|
+
}, 'side-memo: prompt memo');
|
|
4735
5848
|
// The resident header's leading seat is the one place that exists before a
|
|
4736
5849
|
// Session and while the new-Session (blank) chrome is up, so the toggle is
|
|
4737
5850
|
// there exactly for those phases; once the Session is established the
|
|
@@ -4760,7 +5873,7 @@ window.__ModuleLoader__.load({
|
|
|
4760
5873
|
order: 40,
|
|
4761
5874
|
locale: NS,
|
|
4762
5875
|
inject: function () {
|
|
4763
|
-
return { notes: notes, drawer: drawer };
|
|
5876
|
+
return { notes: notes, drawer: drawer, openRequest: openRequest };
|
|
4764
5877
|
}
|
|
4765
5878
|
}, NotesDrawer);
|
|
4766
5879
|
});
|
|
@@ -4798,7 +5911,7 @@ window.__ModuleLoader__.load({
|
|
|
4798
5911
|
locale: NS,
|
|
4799
5912
|
// Slot inject is a factory called as inject(bindingKey, actions).
|
|
4800
5913
|
inject: function () {
|
|
4801
|
-
return { notes: notes };
|
|
5914
|
+
return { notes: notes, openRequest: openRequest };
|
|
4802
5915
|
},
|
|
4803
5916
|
children: {
|
|
4804
5917
|
'sidebar.right.tab.notes.actions': {
|
|
@@ -4887,6 +6000,7 @@ window.__ModuleLoader__.load({
|
|
|
4887
6000
|
noteImageIds: noteImageIds,
|
|
4888
6001
|
normalizeImageRecord: normalizeImageRecord,
|
|
4889
6002
|
sortNotes: sortNotes,
|
|
6003
|
+
placeContextMenu: placeContextMenu,
|
|
4890
6004
|
ageLabel: ageLabel,
|
|
4891
6005
|
crc32: crc32,
|
|
4892
6006
|
encodeText: encodeText,
|
|
@@ -4904,7 +6018,12 @@ window.__ModuleLoader__.load({
|
|
|
4904
6018
|
importFavorite: importFavorite,
|
|
4905
6019
|
parseLabelCell: parseLabelCell,
|
|
4906
6020
|
normalizedHeader: normalizedHeader,
|
|
4907
|
-
stripXmlPrefixes: stripXmlPrefixes
|
|
6021
|
+
stripXmlPrefixes: stripXmlPrefixes,
|
|
6022
|
+
// Chat → memo helpers (also used by the Node unit suite).
|
|
6023
|
+
chatMemoEntries: chatMemoEntries,
|
|
6024
|
+
chatMemoDraft: chatMemoDraft,
|
|
6025
|
+
createOpenRequestStore: createOpenRequestStore,
|
|
6026
|
+
promptMemoDraft: promptMemoDraft
|
|
4908
6027
|
};
|
|
4909
6028
|
}
|
|
4910
6029
|
return module.exports;
|