dsh-side-memo 1.2.0 → 1.3.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 +11 -4
- package/README.zh-CN.md +12 -5
- package/README.zh-TW.md +12 -5
- package/client.js +1090 -9
- 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,21 @@ 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
|
+
'.dsm-menu{position:absolute;z-index:20;min-width:172px;max-width:240px;padding:4px;box-sizing:border-box;',
|
|
354
|
+
'display:flex;flex-direction:column;gap:1px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;',
|
|
355
|
+
'background:var(--dsw-specific-menu);backdrop-filter:var(--dsw-menu-backdrop-filter);',
|
|
356
|
+
'box-shadow:var(--dsw-elevation-prominent)}',
|
|
357
|
+
'.dsm-menu-item{display:flex;align-items:center;gap:8px;width:100%;height:30px;padding:0 8px;box-sizing:border-box;',
|
|
358
|
+
'border:0;border-radius:7px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;',
|
|
359
|
+
'font-size:12.5px;line-height:1.2;text-align:left;white-space:nowrap;cursor:pointer}',
|
|
360
|
+
'.dsm-menu-item:hover{background:var(--dsw-alias-bg-layer-1)}',
|
|
361
|
+
'.dsm-menu-item:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px;',
|
|
362
|
+
'background:var(--dsw-alias-bg-layer-1)}',
|
|
363
|
+
'.dsm-menu-item[data-danger="true"]{color:var(--dsw-alias-state-error-primary)}',
|
|
364
|
+
'.dsm-menu-icon{display:flex;flex:0 0 auto;color:var(--dsw-alias-label-secondary)}',
|
|
365
|
+
'.dsm-menu-item[data-danger="true"] .dsm-menu-icon{color:inherit}',
|
|
366
|
+
'.dsm-menu-label{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis}',
|
|
367
|
+
'.dsm-menu-sep{flex:0 0 auto;height:1px;margin:3px 4px;background:var(--dsw-alias-border-l1)}',
|
|
307
368
|
'.dsm-foot-hint{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
308
369
|
'[data-composer-input].dsm-composer-drop{outline:2px dashed var(--dsw-alias-brand-primary);outline-offset:2px;border-radius:10px}',
|
|
309
370
|
'.dsm-icon-btn[data-favorite="true"]{color:var(--dsw-alias-brand-primary)}',
|
|
@@ -420,6 +481,26 @@ window.__ModuleLoader__.load({
|
|
|
420
481
|
'.dsm-toggle:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1)}',
|
|
421
482
|
'.dsm-toggle[data-active="true"]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1)}',
|
|
422
483
|
'.dsm-toggle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}',
|
|
484
|
+
'.dsm-toggle[data-state="saved"]{color:var(--dsw-alias-brand-primary)}',
|
|
485
|
+
'.dsm-toggle[data-state="failed"]{color:var(--dsw-alias-label-primary);opacity:.85}',
|
|
486
|
+
'.dsm-chat-memo{display:inline-flex;align-items:center;position:relative}',
|
|
487
|
+
'.dsm-chat-memo-menu{position:fixed;z-index:120;min-width:214px;padding:5px;display:flex;flex-direction:column;gap:2px;',
|
|
488
|
+
'box-sizing:border-box;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-specific-menu);',
|
|
489
|
+
'backdrop-filter:var(--dsw-menu-backdrop-filter);box-shadow:var(--dsw-elevation-prominent)}',
|
|
490
|
+
'.dsm-chat-memo-item{display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:100%;padding:7px 9px;',
|
|
491
|
+
'border:0;border-radius:7px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;',
|
|
492
|
+
'line-height:1.35;text-align:left;cursor:pointer}',
|
|
493
|
+
'.dsm-chat-memo-item:hover{background:var(--dsw-alias-bg-layer-2)}',
|
|
494
|
+
'.dsm-chat-memo-item:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}',
|
|
495
|
+
'.dsm-chat-memo-note{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:1.3}',
|
|
496
|
+
'.dsm-prompt-memo{position:fixed;z-index:115;display:inline-flex;align-items:center;justify-content:center;',
|
|
497
|
+
'width:22px;height:22px;padding:0;pointer-events:auto;-webkit-app-region:no-drag;cursor:pointer;',
|
|
498
|
+
'border:1px solid var(--dsw-alias-border-l1);border-radius:6px;background:var(--dsw-specific-menu);',
|
|
499
|
+
'color:var(--dsw-alias-label-secondary);box-shadow:var(--dsw-elevation-prominent)}',
|
|
500
|
+
'.dsm-prompt-memo:hover{color:var(--dsw-alias-label-primary)}',
|
|
501
|
+
'.dsm-prompt-memo:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}',
|
|
502
|
+
'.dsm-prompt-memo[data-state="saved"]{color:var(--dsw-alias-brand-primary)}',
|
|
503
|
+
'.dsm-prompt-memo[data-state="failed"]{color:var(--dsw-alias-label-primary);opacity:.85}',
|
|
423
504
|
'.dsm-header-toggle{position:absolute;top:11px;right:62px;z-index:2}',
|
|
424
505
|
'.dsm-drawer-layer{position:absolute;inset:0;pointer-events:none}',
|
|
425
506
|
'.dsm-drawer{position:absolute;top:max(56px,var(--dsh-frame-overlay-top,12px));right:12px;bottom:12px;',
|
|
@@ -461,6 +542,10 @@ window.__ModuleLoader__.load({
|
|
|
461
542
|
function IconNote(props) {
|
|
462
543
|
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
544
|
}
|
|
545
|
+
/** A speech bubble with memo lines: "turn this conversation into a memo". */
|
|
546
|
+
function IconChatMemo(props) {
|
|
547
|
+
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' }));
|
|
548
|
+
}
|
|
464
549
|
function IconSearch(props) {
|
|
465
550
|
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
551
|
}
|
|
@@ -1549,6 +1634,46 @@ window.__ModuleLoader__.load({
|
|
|
1549
1634
|
emit();
|
|
1550
1635
|
return next;
|
|
1551
1636
|
},
|
|
1637
|
+
duplicate: function (id) {
|
|
1638
|
+
var index = -1;
|
|
1639
|
+
for (var at = 0; at < notes.length; at += 1) {
|
|
1640
|
+
if (notes[at].id === id) {
|
|
1641
|
+
index = at;
|
|
1642
|
+
break;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
if (index < 0)
|
|
1646
|
+
return undefined;
|
|
1647
|
+
if (notes.length >= MAX_NOTES)
|
|
1648
|
+
return undefined;
|
|
1649
|
+
var source = notes[index];
|
|
1650
|
+
var now = Date.now();
|
|
1651
|
+
var clonedContent = source.content.map(function (block) {
|
|
1652
|
+
if (block.type === 'image') {
|
|
1653
|
+
var image = { type: 'image', id: block.id };
|
|
1654
|
+
if (typeof block.width === 'number')
|
|
1655
|
+
image.width = block.width;
|
|
1656
|
+
if (typeof block.height === 'number')
|
|
1657
|
+
image.height = block.height;
|
|
1658
|
+
return image;
|
|
1659
|
+
}
|
|
1660
|
+
return { type: 'text', text: block.text };
|
|
1661
|
+
});
|
|
1662
|
+
var copy = {
|
|
1663
|
+
id: newNoteId(),
|
|
1664
|
+
title: source.title,
|
|
1665
|
+
body: source.body,
|
|
1666
|
+
content: clonedContent,
|
|
1667
|
+
createdAt: now,
|
|
1668
|
+
updatedAt: now,
|
|
1669
|
+
favorite: false,
|
|
1670
|
+
labels: normalizeLabels(source.labels)
|
|
1671
|
+
};
|
|
1672
|
+
notes = [copy].concat(notes);
|
|
1673
|
+
schedule();
|
|
1674
|
+
emit();
|
|
1675
|
+
return copy;
|
|
1676
|
+
},
|
|
1552
1677
|
remove: function (id) {
|
|
1553
1678
|
var next = notes.filter(function (note) {
|
|
1554
1679
|
return note.id !== id;
|
|
@@ -2036,6 +2161,199 @@ window.__ModuleLoader__.load({
|
|
|
2036
2161
|
}
|
|
2037
2162
|
};
|
|
2038
2163
|
}
|
|
2164
|
+
/**
|
|
2165
|
+
* One-shot "open this memo" hand-off used by the chat-memo button.
|
|
2166
|
+
*
|
|
2167
|
+
* It is a store rather than local state because the notes surface may not be
|
|
2168
|
+
* mounted yet when the memo is written: the button reveals the panel first,
|
|
2169
|
+
* and the notes body picks the request up as it mounts.
|
|
2170
|
+
*/
|
|
2171
|
+
function createOpenRequestStore() {
|
|
2172
|
+
var requested = null;
|
|
2173
|
+
var listeners = new Set();
|
|
2174
|
+
function emit() {
|
|
2175
|
+
listeners.forEach(function (listener) {
|
|
2176
|
+
try {
|
|
2177
|
+
listener();
|
|
2178
|
+
}
|
|
2179
|
+
catch (error) {
|
|
2180
|
+
console.error('[dsh-side-memo] open-request listener failed', error);
|
|
2181
|
+
}
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
return {
|
|
2185
|
+
subscribe: function (listener) {
|
|
2186
|
+
listeners.add(listener);
|
|
2187
|
+
return function () {
|
|
2188
|
+
listeners.delete(listener);
|
|
2189
|
+
};
|
|
2190
|
+
},
|
|
2191
|
+
getSnapshot: function () {
|
|
2192
|
+
return requested;
|
|
2193
|
+
},
|
|
2194
|
+
set: function (id) {
|
|
2195
|
+
var next = typeof id === 'string' && id.length > 0 ? id : null;
|
|
2196
|
+
if (next === requested)
|
|
2197
|
+
return;
|
|
2198
|
+
requested = next;
|
|
2199
|
+
emit();
|
|
2200
|
+
}
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
/** Inert store for notes surfaces rendered without a chat-memo hand-off. */
|
|
2204
|
+
var nullOpenRequest = {
|
|
2205
|
+
subscribe: function () {
|
|
2206
|
+
return function () { };
|
|
2207
|
+
},
|
|
2208
|
+
getSnapshot: function () {
|
|
2209
|
+
return null;
|
|
2210
|
+
},
|
|
2211
|
+
set: function () { }
|
|
2212
|
+
};
|
|
2213
|
+
// ---------------------------------------------------------------------
|
|
2214
|
+
// Chat → memo: condense the visible conversation into one memo draft.
|
|
2215
|
+
// ---------------------------------------------------------------------
|
|
2216
|
+
// The title comes from the first user message; long prompts make hostile
|
|
2217
|
+
// titles, so cap the derived title well below the stored MAX_TITLE.
|
|
2218
|
+
var CHAT_MEMO_TITLE_MAX = 80;
|
|
2219
|
+
function chatMemoText(value) {
|
|
2220
|
+
return typeof value === 'string' ? value.replace(/\r\n?/g, '\n').trim() : '';
|
|
2221
|
+
}
|
|
2222
|
+
/** Join the text entries of one chat block list (images, tools, reasoning skipped). */
|
|
2223
|
+
function chatMemoBlocks(value, key) {
|
|
2224
|
+
if (!Array.isArray(value))
|
|
2225
|
+
return '';
|
|
2226
|
+
var parts = [];
|
|
2227
|
+
for (var index = 0; index < value.length; index += 1) {
|
|
2228
|
+
var block = value[index];
|
|
2229
|
+
if (block === null || typeof block !== 'object')
|
|
2230
|
+
continue;
|
|
2231
|
+
if (block[key] !== 'text')
|
|
2232
|
+
continue;
|
|
2233
|
+
var text = chatMemoText(block.text);
|
|
2234
|
+
if (text !== '')
|
|
2235
|
+
parts.push(text);
|
|
2236
|
+
}
|
|
2237
|
+
return parts.join('\n\n');
|
|
2238
|
+
}
|
|
2239
|
+
/**
|
|
2240
|
+
* Reduce visible chat nodes to ordered user/assistant text turns.
|
|
2241
|
+
* `mode` "prompts" keeps only what the user sent.
|
|
2242
|
+
*/
|
|
2243
|
+
function chatMemoEntries(nodes, mode) {
|
|
2244
|
+
var entries = [];
|
|
2245
|
+
if (!Array.isArray(nodes))
|
|
2246
|
+
return entries;
|
|
2247
|
+
for (var index = 0; index < nodes.length; index += 1) {
|
|
2248
|
+
var node = nodes[index];
|
|
2249
|
+
if (node === null || typeof node !== 'object')
|
|
2250
|
+
continue;
|
|
2251
|
+
var kind = typeof node.kind === 'string' ? node.kind : '';
|
|
2252
|
+
var data = node.data === null || typeof node.data !== 'object' ? {} : node.data;
|
|
2253
|
+
var role = null;
|
|
2254
|
+
var text = '';
|
|
2255
|
+
if (kind === 'user' || kind === 'steering') {
|
|
2256
|
+
role = 'user';
|
|
2257
|
+
text = chatMemoBlocks(data.content, 'type');
|
|
2258
|
+
}
|
|
2259
|
+
else if (kind === 'assistant' || kind === 'assistant-step') {
|
|
2260
|
+
role = 'assistant';
|
|
2261
|
+
text = chatMemoBlocks(data.blocks, 'kind');
|
|
2262
|
+
}
|
|
2263
|
+
if (role === null || text === '')
|
|
2264
|
+
continue;
|
|
2265
|
+
if (mode === 'prompts' && role !== 'user')
|
|
2266
|
+
continue;
|
|
2267
|
+
entries.push({ role: role, text: text });
|
|
2268
|
+
}
|
|
2269
|
+
return entries;
|
|
2270
|
+
}
|
|
2271
|
+
function chatMemoHeading(entry, labels) {
|
|
2272
|
+
return (entry.role === 'user' ? labels.you : labels.assistant) + ':\n';
|
|
2273
|
+
}
|
|
2274
|
+
function chatMemoTitle(entries, labels) {
|
|
2275
|
+
var limit = Math.min(CHAT_MEMO_TITLE_MAX, MAX_TITLE);
|
|
2276
|
+
for (var index = 0; index < entries.length; index += 1) {
|
|
2277
|
+
if (entries[index].role !== 'user')
|
|
2278
|
+
continue;
|
|
2279
|
+
var collapsed = entries[index].text.replace(/\s+/g, ' ').trim();
|
|
2280
|
+
if (collapsed === '')
|
|
2281
|
+
continue;
|
|
2282
|
+
return collapsed.length > limit ? collapsed.slice(0, limit).trim() : collapsed;
|
|
2283
|
+
}
|
|
2284
|
+
return labels.fallbackTitle;
|
|
2285
|
+
}
|
|
2286
|
+
function chatMemoBody(entries, labels) {
|
|
2287
|
+
var parts = [];
|
|
2288
|
+
for (var index = 0; index < entries.length; index += 1) {
|
|
2289
|
+
parts.push(chatMemoHeading(entries[index], labels) + entries[index].text);
|
|
2290
|
+
}
|
|
2291
|
+
return parts.join('\n\n');
|
|
2292
|
+
}
|
|
2293
|
+
/**
|
|
2294
|
+
* Build the memo title/body for one conversation. Returns null when the chat
|
|
2295
|
+
* holds no user or assistant text (blank session, tools only, …).
|
|
2296
|
+
*
|
|
2297
|
+
* Transcripts are bounded twice: whole turns are dropped from the old end
|
|
2298
|
+
* until the body fits MAX_BODY, and a single oversized turn keeps its tail so
|
|
2299
|
+
* the newest words survive. `truncated` reports either adjustment.
|
|
2300
|
+
*
|
|
2301
|
+
* `mode` "prompts" saves only the user's messages; the title still comes from
|
|
2302
|
+
* the first prompt, and a chat with no prompts returns null.
|
|
2303
|
+
*/
|
|
2304
|
+
function chatMemoDraft(nodes, labels, mode) {
|
|
2305
|
+
var entries = chatMemoEntries(nodes, mode);
|
|
2306
|
+
if (entries.length === 0)
|
|
2307
|
+
return null;
|
|
2308
|
+
var maxBody = MAX_BODY;
|
|
2309
|
+
var kept = entries.slice();
|
|
2310
|
+
var truncated = false;
|
|
2311
|
+
var body = chatMemoBody(kept, labels);
|
|
2312
|
+
while (body.length > maxBody && kept.length > 1) {
|
|
2313
|
+
kept.shift();
|
|
2314
|
+
truncated = true;
|
|
2315
|
+
body = chatMemoBody(kept, labels);
|
|
2316
|
+
}
|
|
2317
|
+
if (body.length > maxBody) {
|
|
2318
|
+
var last = kept[0];
|
|
2319
|
+
var room = maxBody - chatMemoHeading(last, labels).length;
|
|
2320
|
+
if (room > 0 && last.text.length > room) {
|
|
2321
|
+
kept = [{ role: last.role, text: last.text.slice(last.text.length - room) }];
|
|
2322
|
+
truncated = true;
|
|
2323
|
+
body = chatMemoBody(kept, labels);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
if (body.length > maxBody) {
|
|
2327
|
+
body = body.slice(body.length - maxBody);
|
|
2328
|
+
truncated = true;
|
|
2329
|
+
}
|
|
2330
|
+
if (body.trim() === '')
|
|
2331
|
+
return null;
|
|
2332
|
+
return {
|
|
2333
|
+
title: chatMemoTitle(entries, labels),
|
|
2334
|
+
body: body,
|
|
2335
|
+
messages: kept.length,
|
|
2336
|
+
truncated: truncated
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Memo title/body for one prompt captured from the transcript. The body keeps
|
|
2341
|
+
* the prompt verbatim (only normalised line endings); the title is collapsed
|
|
2342
|
+
* and capped, and an oversized prompt keeps its tail so a memo can never
|
|
2343
|
+
* exceed the stored body limit.
|
|
2344
|
+
*/
|
|
2345
|
+
function promptMemoDraft(value, labels) {
|
|
2346
|
+
var text = chatMemoText(value);
|
|
2347
|
+
if (text === '')
|
|
2348
|
+
return null;
|
|
2349
|
+
var body = text.length > MAX_BODY ? text.slice(text.length - MAX_BODY) : text;
|
|
2350
|
+
return {
|
|
2351
|
+
title: chatMemoTitle([{ role: 'user', text: text }], labels),
|
|
2352
|
+
body: body,
|
|
2353
|
+
messages: 1,
|
|
2354
|
+
truncated: body.length !== text.length
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2039
2357
|
/**
|
|
2040
2358
|
* Read the Session the right sidebar can act on. `ctx.sidebarRight.mounted`
|
|
2041
2359
|
* is the observable of the on-screen Session — undefined while no Session is
|
|
@@ -2159,6 +2477,38 @@ window.__ModuleLoader__.load({
|
|
|
2159
2477
|
return right.updatedAt - left.updatedAt;
|
|
2160
2478
|
});
|
|
2161
2479
|
}
|
|
2480
|
+
/**
|
|
2481
|
+
* Fit a context menu inside its container. Coordinates are container-relative,
|
|
2482
|
+
* so the menu flips above the anchor when it would overflow the bottom edge and
|
|
2483
|
+
* is clamped to the container with a small margin on every side.
|
|
2484
|
+
*/
|
|
2485
|
+
function placeContextMenu(anchor, size, bounds, margin) {
|
|
2486
|
+
var gap = typeof margin === 'number' && isFinite(margin) ? Math.max(0, margin) : 4;
|
|
2487
|
+
var anchorX = typeof anchor.x === 'number' && isFinite(anchor.x) ? anchor.x : gap;
|
|
2488
|
+
var anchorY = typeof anchor.y === 'number' && isFinite(anchor.y) ? anchor.y : gap;
|
|
2489
|
+
var width = typeof size.width === 'number' && isFinite(size.width) ? Math.max(0, size.width) : 0;
|
|
2490
|
+
var height = typeof size.height === 'number' && isFinite(size.height) ? Math.max(0, size.height) : 0;
|
|
2491
|
+
var boundWidth = typeof bounds.width === 'number' && isFinite(bounds.width) ? Math.max(0, bounds.width) : 0;
|
|
2492
|
+
var boundHeight = typeof bounds.height === 'number' && isFinite(bounds.height) ? Math.max(0, bounds.height) : 0;
|
|
2493
|
+
var left = Math.min(Math.max(anchorX, gap), Math.max(gap, boundWidth - width - gap));
|
|
2494
|
+
var top = anchorY;
|
|
2495
|
+
if (top + height + gap > boundHeight)
|
|
2496
|
+
top = anchorY - height - gap;
|
|
2497
|
+
top = Math.min(Math.max(top, gap), Math.max(gap, boundHeight - height - gap));
|
|
2498
|
+
return { left: Math.round(left), top: Math.round(top) };
|
|
2499
|
+
}
|
|
2500
|
+
/** One row of the memo context menu; keyboard focus is managed by the menu. */
|
|
2501
|
+
function contextMenuItem(key, label, icon, danger, onSelect) {
|
|
2502
|
+
return h('button', {
|
|
2503
|
+
type: 'button',
|
|
2504
|
+
role: 'menuitem',
|
|
2505
|
+
key: key,
|
|
2506
|
+
className: 'dsm-menu-item',
|
|
2507
|
+
'data-danger': danger ? 'true' : undefined,
|
|
2508
|
+
tabIndex: -1,
|
|
2509
|
+
onClick: onSelect
|
|
2510
|
+
}, h('span', { className: 'dsm-menu-icon' }, icon), h('span', { className: 'dsm-menu-label' }, label));
|
|
2511
|
+
}
|
|
2162
2512
|
function copyToClipboard(text) {
|
|
2163
2513
|
if (typeof navigator !== 'undefined' && navigator.clipboard !== undefined && typeof navigator.clipboard.writeText === 'function') {
|
|
2164
2514
|
return navigator.clipboard.writeText(text).then(function () {
|
|
@@ -2928,6 +3278,45 @@ window.__ModuleLoader__.load({
|
|
|
2928
3278
|
var labelNoticeState = React.useState('');
|
|
2929
3279
|
var labelNotice = labelNoticeState[0];
|
|
2930
3280
|
var setLabelNotice = labelNoticeState[1];
|
|
3281
|
+
// Right-click menu on a memo row. Coordinates are relative to `.dsm-root`.
|
|
3282
|
+
var menuState = React.useState(null);
|
|
3283
|
+
var menu = menuState[0];
|
|
3284
|
+
var setMenu = menuState[1];
|
|
3285
|
+
var menuPositionState = React.useState(null);
|
|
3286
|
+
var menuPosition = menuPositionState[0];
|
|
3287
|
+
var setMenuPosition = menuPositionState[1];
|
|
3288
|
+
var menuArmedState = React.useState(false);
|
|
3289
|
+
var menuDeleteArmed = menuArmedState[0];
|
|
3290
|
+
var setMenuDeleteArmed = menuArmedState[1];
|
|
3291
|
+
var rootRef = React.useRef(null);
|
|
3292
|
+
var menuRef = React.useRef(null);
|
|
3293
|
+
var menuReturnFocusRef = React.useRef(null);
|
|
3294
|
+
var openRequest = props.openRequest === undefined ? nullOpenRequest : props.openRequest;
|
|
3295
|
+
var requestedNoteId = useSyncExternalStore(openRequest.subscribe, openRequest.getSnapshot, openRequest.getSnapshot);
|
|
3296
|
+
// A memo created from the chat header asks to be opened here. The panel can
|
|
3297
|
+
// mount after the request was filed, so the store is read on mount as well;
|
|
3298
|
+
// the request is consumed exactly once.
|
|
3299
|
+
React.useEffect(function () {
|
|
3300
|
+
if (requestedNoteId === null)
|
|
3301
|
+
return;
|
|
3302
|
+
openRequest.set(null);
|
|
3303
|
+
var found = false;
|
|
3304
|
+
for (var index = 0; index < snapshot.notes.length; index += 1) {
|
|
3305
|
+
if (snapshot.notes[index].id === requestedNoteId) {
|
|
3306
|
+
found = true;
|
|
3307
|
+
break;
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
if (!found)
|
|
3311
|
+
return;
|
|
3312
|
+
setTransfer(null);
|
|
3313
|
+
setQuery('');
|
|
3314
|
+
setCopiedId(null);
|
|
3315
|
+
setEditorCopied(false);
|
|
3316
|
+
setDeleteArmed(false);
|
|
3317
|
+
setNewId(null);
|
|
3318
|
+
setEditingId(requestedNoteId);
|
|
3319
|
+
}, [requestedNoteId, openRequest, snapshot.notes]);
|
|
2931
3320
|
var now = Date.now();
|
|
2932
3321
|
var normalizedQuery = query.trim().toLocaleLowerCase();
|
|
2933
3322
|
var labelFilterActive = activeLabels.length > 0;
|
|
@@ -3183,6 +3572,103 @@ window.__ModuleLoader__.load({
|
|
|
3183
3572
|
}
|
|
3184
3573
|
}
|
|
3185
3574
|
}
|
|
3575
|
+
var menuTarget;
|
|
3576
|
+
if (menu !== null) {
|
|
3577
|
+
for (var menuAt = 0; menuAt < snapshot.notes.length; menuAt += 1) {
|
|
3578
|
+
if (snapshot.notes[menuAt].id === menu.id) {
|
|
3579
|
+
menuTarget = snapshot.notes[menuAt];
|
|
3580
|
+
break;
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3584
|
+
// Drop the menu when its memo disappears (deleted elsewhere, imported over, ...).
|
|
3585
|
+
React.useEffect(function () {
|
|
3586
|
+
if (menu === null || menuTarget !== undefined)
|
|
3587
|
+
return;
|
|
3588
|
+
setMenu(null);
|
|
3589
|
+
setMenuDeleteArmed(false);
|
|
3590
|
+
setMenuPosition(null);
|
|
3591
|
+
}, [menu, menuTarget, setMenu, setMenuDeleteArmed, setMenuPosition]);
|
|
3592
|
+
// Measure after render so the menu can be clamped or flipped inside the panel.
|
|
3593
|
+
React.useLayoutEffect(function () {
|
|
3594
|
+
if (menu === null)
|
|
3595
|
+
return;
|
|
3596
|
+
var root = rootRef.current;
|
|
3597
|
+
var element = menuRef.current;
|
|
3598
|
+
if (root === null || element === null)
|
|
3599
|
+
return;
|
|
3600
|
+
var next = placeContextMenu({ x: menu.x, y: menu.y }, { width: element.offsetWidth, height: element.offsetHeight }, { width: root.clientWidth, height: root.clientHeight });
|
|
3601
|
+
setMenuPosition(function (current) {
|
|
3602
|
+
if (current !== null && current.left === next.left && current.top === next.top)
|
|
3603
|
+
return current;
|
|
3604
|
+
return next;
|
|
3605
|
+
});
|
|
3606
|
+
}, [menu, menuDeleteArmed, setMenuPosition]);
|
|
3607
|
+
React.useLayoutEffect(function () {
|
|
3608
|
+
if (menu === null)
|
|
3609
|
+
return;
|
|
3610
|
+
var element = menuRef.current;
|
|
3611
|
+
if (element === null)
|
|
3612
|
+
return;
|
|
3613
|
+
var items = element.querySelectorAll('[role="menuitem"]');
|
|
3614
|
+
if (items.length === 0)
|
|
3615
|
+
return;
|
|
3616
|
+
try {
|
|
3617
|
+
items[0].focus();
|
|
3618
|
+
}
|
|
3619
|
+
catch (error) {
|
|
3620
|
+
// Focus is best effort; the menu still works with the mouse.
|
|
3621
|
+
}
|
|
3622
|
+
}, [menu]);
|
|
3623
|
+
React.useEffect(function () {
|
|
3624
|
+
if (menu === null)
|
|
3625
|
+
return undefined;
|
|
3626
|
+
if (typeof document === 'undefined' || typeof window === 'undefined')
|
|
3627
|
+
return undefined;
|
|
3628
|
+
function isInsideMenu(target) {
|
|
3629
|
+
var element = menuRef.current;
|
|
3630
|
+
if (element === null || target === null)
|
|
3631
|
+
return false;
|
|
3632
|
+
return element.contains(target);
|
|
3633
|
+
}
|
|
3634
|
+
function onPointerDown(event) {
|
|
3635
|
+
if (isInsideMenu(event.target))
|
|
3636
|
+
return;
|
|
3637
|
+
closeRowMenu(false);
|
|
3638
|
+
}
|
|
3639
|
+
function onKeyDown(event) {
|
|
3640
|
+
if (event.key !== 'Escape')
|
|
3641
|
+
return;
|
|
3642
|
+
event.preventDefault();
|
|
3643
|
+
event.stopPropagation();
|
|
3644
|
+
closeRowMenu(true);
|
|
3645
|
+
}
|
|
3646
|
+
function onDismiss() {
|
|
3647
|
+
closeRowMenu(false);
|
|
3648
|
+
}
|
|
3649
|
+
document.addEventListener('pointerdown', onPointerDown, true);
|
|
3650
|
+
document.addEventListener('keydown', onKeyDown, true);
|
|
3651
|
+
document.addEventListener('scroll', onDismiss, true);
|
|
3652
|
+
window.addEventListener('resize', onDismiss);
|
|
3653
|
+
window.addEventListener('blur', onDismiss);
|
|
3654
|
+
return function () {
|
|
3655
|
+
document.removeEventListener('pointerdown', onPointerDown, true);
|
|
3656
|
+
document.removeEventListener('keydown', onKeyDown, true);
|
|
3657
|
+
document.removeEventListener('scroll', onDismiss, true);
|
|
3658
|
+
window.removeEventListener('resize', onDismiss);
|
|
3659
|
+
window.removeEventListener('blur', onDismiss);
|
|
3660
|
+
};
|
|
3661
|
+
}, [menu]);
|
|
3662
|
+
React.useEffect(function () {
|
|
3663
|
+
if (!menuDeleteArmed)
|
|
3664
|
+
return undefined;
|
|
3665
|
+
var timer = setTimeout(function () {
|
|
3666
|
+
setMenuDeleteArmed(false);
|
|
3667
|
+
}, DELETE_ARM_MS);
|
|
3668
|
+
return function () {
|
|
3669
|
+
clearTimeout(timer);
|
|
3670
|
+
};
|
|
3671
|
+
}, [menuDeleteArmed, setMenuDeleteArmed]);
|
|
3186
3672
|
React.useEffect(function () {
|
|
3187
3673
|
if (editingId !== null && editing === undefined) {
|
|
3188
3674
|
setEditingId(null);
|
|
@@ -3492,6 +3978,130 @@ window.__ModuleLoader__.load({
|
|
|
3492
3978
|
setNewId(null);
|
|
3493
3979
|
setDeleteArmed(false);
|
|
3494
3980
|
}
|
|
3981
|
+
function openRowMenu(note, clientX, clientY, returnFocus) {
|
|
3982
|
+
var root = rootRef.current;
|
|
3983
|
+
var rect = root === null ? null : root.getBoundingClientRect();
|
|
3984
|
+
var x = rect === null ? clientX : clientX - rect.left;
|
|
3985
|
+
var y = rect === null ? clientY : clientY - rect.top;
|
|
3986
|
+
menuReturnFocusRef.current = returnFocus;
|
|
3987
|
+
setMenuDeleteArmed(false);
|
|
3988
|
+
setMenuPosition(null);
|
|
3989
|
+
setMenu({ id: note.id, x: x, y: y });
|
|
3990
|
+
}
|
|
3991
|
+
function closeRowMenu(restoreFocus) {
|
|
3992
|
+
var target = menuReturnFocusRef.current;
|
|
3993
|
+
menuReturnFocusRef.current = null;
|
|
3994
|
+
setMenu(null);
|
|
3995
|
+
setMenuDeleteArmed(false);
|
|
3996
|
+
setMenuPosition(null);
|
|
3997
|
+
if (!restoreFocus || target === null)
|
|
3998
|
+
return;
|
|
3999
|
+
if (typeof target.isConnected === 'boolean' && !target.isConnected)
|
|
4000
|
+
return;
|
|
4001
|
+
try {
|
|
4002
|
+
target.focus();
|
|
4003
|
+
}
|
|
4004
|
+
catch (error) {
|
|
4005
|
+
/* focusing is best effort */
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
function menuNote() {
|
|
4009
|
+
if (menu === null)
|
|
4010
|
+
return undefined;
|
|
4011
|
+
for (var index = 0; index < snapshot.notes.length; index += 1) {
|
|
4012
|
+
if (snapshot.notes[index].id === menu.id)
|
|
4013
|
+
return snapshot.notes[index];
|
|
4014
|
+
}
|
|
4015
|
+
return undefined;
|
|
4016
|
+
}
|
|
4017
|
+
function handleMenuOpen() {
|
|
4018
|
+
var note = menuNote();
|
|
4019
|
+
closeRowMenu(true);
|
|
4020
|
+
if (note !== undefined)
|
|
4021
|
+
openNote(note.id);
|
|
4022
|
+
}
|
|
4023
|
+
function handleMenuCopy() {
|
|
4024
|
+
var note = menuNote();
|
|
4025
|
+
closeRowMenu(true);
|
|
4026
|
+
if (note !== undefined)
|
|
4027
|
+
copyNote(note);
|
|
4028
|
+
}
|
|
4029
|
+
function handleMenuDuplicate() {
|
|
4030
|
+
var note = menuNote();
|
|
4031
|
+
closeRowMenu(true);
|
|
4032
|
+
if (note === undefined)
|
|
4033
|
+
return;
|
|
4034
|
+
var copy = notes.duplicate(note.id);
|
|
4035
|
+
if (copy === undefined) {
|
|
4036
|
+
setAnnounce(t('limit.reached', { max: limits.maxNotes }));
|
|
4037
|
+
return;
|
|
4038
|
+
}
|
|
4039
|
+
setAnnounce(t('menu.duplicated'));
|
|
4040
|
+
}
|
|
4041
|
+
function handleMenuFavorite() {
|
|
4042
|
+
var note = menuNote();
|
|
4043
|
+
closeRowMenu(true);
|
|
4044
|
+
if (note !== undefined)
|
|
4045
|
+
toggleFavorite(note);
|
|
4046
|
+
}
|
|
4047
|
+
function handleMenuDelete() {
|
|
4048
|
+
var note = menuNote();
|
|
4049
|
+
if (note === undefined) {
|
|
4050
|
+
closeRowMenu(false);
|
|
4051
|
+
return;
|
|
4052
|
+
}
|
|
4053
|
+
if (!menuDeleteArmed) {
|
|
4054
|
+
setMenuDeleteArmed(true);
|
|
4055
|
+
return;
|
|
4056
|
+
}
|
|
4057
|
+
closeRowMenu(false);
|
|
4058
|
+
notes.remove(note.id);
|
|
4059
|
+
notes.flush();
|
|
4060
|
+
setAnnounce(t('menu.deleted'));
|
|
4061
|
+
}
|
|
4062
|
+
function moveMenuFocus(event) {
|
|
4063
|
+
if (event.key === 'Tab') {
|
|
4064
|
+
closeRowMenu(false);
|
|
4065
|
+
return;
|
|
4066
|
+
}
|
|
4067
|
+
var forward = event.key === 'ArrowDown';
|
|
4068
|
+
var backward = event.key === 'ArrowUp';
|
|
4069
|
+
var home = event.key === 'Home';
|
|
4070
|
+
var end = event.key === 'End';
|
|
4071
|
+
if (!forward && !backward && !home && !end)
|
|
4072
|
+
return;
|
|
4073
|
+
event.preventDefault();
|
|
4074
|
+
event.stopPropagation();
|
|
4075
|
+
var element = menuRef.current;
|
|
4076
|
+
if (element === null)
|
|
4077
|
+
return;
|
|
4078
|
+
var items = element.querySelectorAll('[role="menuitem"]');
|
|
4079
|
+
if (items.length === 0)
|
|
4080
|
+
return;
|
|
4081
|
+
var active = document.activeElement;
|
|
4082
|
+
var current = -1;
|
|
4083
|
+
for (var index = 0; index < items.length; index += 1) {
|
|
4084
|
+
if (items[index] === active) {
|
|
4085
|
+
current = index;
|
|
4086
|
+
break;
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
var next;
|
|
4090
|
+
if (home)
|
|
4091
|
+
next = 0;
|
|
4092
|
+
else if (end)
|
|
4093
|
+
next = items.length - 1;
|
|
4094
|
+
else if (forward)
|
|
4095
|
+
next = current < 0 ? 0 : (current + 1) % items.length;
|
|
4096
|
+
else
|
|
4097
|
+
next = current < 0 ? items.length - 1 : (current - 1 + items.length) % items.length;
|
|
4098
|
+
try {
|
|
4099
|
+
items[next].focus();
|
|
4100
|
+
}
|
|
4101
|
+
catch (error) {
|
|
4102
|
+
/* focusing is best effort */
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
3495
4105
|
function updateEditing(field, value) {
|
|
3496
4106
|
if (editing === undefined)
|
|
3497
4107
|
return;
|
|
@@ -4266,6 +4876,20 @@ window.__ModuleLoader__.load({
|
|
|
4266
4876
|
}
|
|
4267
4877
|
fillMemoDragData(event.dataTransfer, note);
|
|
4268
4878
|
setDraggingId(note.id);
|
|
4879
|
+
},
|
|
4880
|
+
onContextMenu: function (event) {
|
|
4881
|
+
event.preventDefault();
|
|
4882
|
+
event.stopPropagation();
|
|
4883
|
+
var focusTarget = event.currentTarget.querySelector('.dsm-row-main');
|
|
4884
|
+
var x = event.clientX;
|
|
4885
|
+
var y = event.clientY;
|
|
4886
|
+
if (x === 0 && y === 0) {
|
|
4887
|
+
// Keyboard-triggered context menu: anchor it to the row instead of 0,0.
|
|
4888
|
+
var rect = event.currentTarget.getBoundingClientRect();
|
|
4889
|
+
x = rect.left + Math.min(24, rect.width / 2);
|
|
4890
|
+
y = rect.bottom - 6;
|
|
4891
|
+
}
|
|
4892
|
+
openRowMenu(note, x, y, focusTarget);
|
|
4269
4893
|
}
|
|
4270
4894
|
}, h('button', {
|
|
4271
4895
|
type: 'button',
|
|
@@ -4273,6 +4897,15 @@ window.__ModuleLoader__.load({
|
|
|
4273
4897
|
title: title,
|
|
4274
4898
|
onClick: function () {
|
|
4275
4899
|
openNote(note.id);
|
|
4900
|
+
},
|
|
4901
|
+
onKeyDown: function (event) {
|
|
4902
|
+
var isMenuKey = event.key === 'ContextMenu' || (event.shiftKey && event.key === 'F10');
|
|
4903
|
+
if (!isMenuKey)
|
|
4904
|
+
return;
|
|
4905
|
+
event.preventDefault();
|
|
4906
|
+
event.stopPropagation();
|
|
4907
|
+
var rect = event.currentTarget.getBoundingClientRect();
|
|
4908
|
+
openRowMenu(note, rect.left + Math.min(24, rect.width / 2), rect.bottom - 6, event.currentTarget);
|
|
4276
4909
|
}
|
|
4277
4910
|
}, h('span', { className: 'dsm-row-title' }, h('span', { className: 'dsm-row-title-text' }, title), imageTotal > 0
|
|
4278
4911
|
? h('span', { className: 'dsm-row-images', title: imageCountLabel(t, imageTotal), 'aria-label': imageCountLabel(t, imageTotal) }, h(IconImage, { size: 11 }), String(imageTotal))
|
|
@@ -4333,7 +4966,33 @@ window.__ModuleLoader__.load({
|
|
|
4333
4966
|
? t('count.one')
|
|
4334
4967
|
: t('count.other', { count: snapshot.notes.length });
|
|
4335
4968
|
}
|
|
4336
|
-
|
|
4969
|
+
var menuElement = null;
|
|
4970
|
+
if (menu !== null && menuTarget !== undefined) {
|
|
4971
|
+
var targetNote = menuTarget;
|
|
4972
|
+
var menuItems = [
|
|
4973
|
+
contextMenuItem('open', t('menu.open'), h(IconNote, { size: 14 }), false, handleMenuOpen),
|
|
4974
|
+
contextMenuItem('copy', t('copy'), h(IconCopy, { size: 14 }), false, handleMenuCopy),
|
|
4975
|
+
contextMenuItem('duplicate', t('menu.duplicate'), h(IconPlus, { size: 14 }), false, handleMenuDuplicate),
|
|
4976
|
+
contextMenuItem('favorite', targetNote.favorite === true ? t('favorite.remove') : t('favorite.add'), targetNote.favorite === true ? h(IconStarFilled, { size: 14 }) : h(IconStar, { size: 14 }), false, handleMenuFavorite),
|
|
4977
|
+
h('div', { className: 'dsm-menu-sep', role: 'separator', key: 'sep' }),
|
|
4978
|
+
contextMenuItem('delete', menuDeleteArmed ? t('delete.confirm') : t('delete'), h(IconX, { size: 14 }), true, handleMenuDelete)
|
|
4979
|
+
];
|
|
4980
|
+
menuElement = h('div', {
|
|
4981
|
+
className: 'dsm-menu',
|
|
4982
|
+
role: 'menu',
|
|
4983
|
+
'aria-label': t('menu.label'),
|
|
4984
|
+
ref: menuRef,
|
|
4985
|
+
style: {
|
|
4986
|
+
left: String(menuPosition === null ? menu.x : menuPosition.left) + 'px',
|
|
4987
|
+
top: String(menuPosition === null ? menu.y : menuPosition.top) + 'px'
|
|
4988
|
+
},
|
|
4989
|
+
onKeyDown: moveMenuFocus,
|
|
4990
|
+
onContextMenu: function (event) {
|
|
4991
|
+
event.preventDefault();
|
|
4992
|
+
}
|
|
4993
|
+
}, menuItems);
|
|
4994
|
+
}
|
|
4995
|
+
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
4996
|
type: 'search',
|
|
4338
4997
|
value: query,
|
|
4339
4998
|
placeholder: t('search.placeholder'),
|
|
@@ -4420,7 +5079,7 @@ window.__ModuleLoader__.load({
|
|
|
4420
5079
|
}, t('labels.all'))
|
|
4421
5080
|
: null), h('div', { className: 'dsm-list-scroll' }, listContent), h('div', { className: 'dsm-foot' }, h('span', null, countLabel), visibleNotes.length === 0 || props.inputActions === undefined
|
|
4422
5081
|
? 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);
|
|
5082
|
+
: 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
5083
|
}
|
|
4425
5084
|
// ---------------------------------------------------------------------
|
|
4426
5085
|
// Chip title and header toggle
|
|
@@ -4445,6 +5104,374 @@ window.__ModuleLoader__.load({
|
|
|
4445
5104
|
}
|
|
4446
5105
|
}, h(IconNote, { size: 16 }));
|
|
4447
5106
|
}
|
|
5107
|
+
/**
|
|
5108
|
+
* "Save this conversation as a memo" header button.
|
|
5109
|
+
*
|
|
5110
|
+
* The chat snapshot hook only exists inside a Session-scope slot, so this
|
|
5111
|
+
* wrapper renders nothing on hosts that do not supply it and defers the real
|
|
5112
|
+
* button — whose hook calls must be unconditional — to a child component.
|
|
5113
|
+
*/
|
|
5114
|
+
function NotesChatMemo(props) {
|
|
5115
|
+
if (typeof props.useChat !== 'function')
|
|
5116
|
+
return null;
|
|
5117
|
+
return h(NotesChatMemoButton, props);
|
|
5118
|
+
}
|
|
5119
|
+
function NotesChatMemoButton(props) {
|
|
5120
|
+
var t = translateFor(props);
|
|
5121
|
+
var notes = props.notes;
|
|
5122
|
+
var openRequest = props.openRequest;
|
|
5123
|
+
var useChat = props.useChat;
|
|
5124
|
+
// Subscribe to the whole (identity-stable) snapshot; the transcript is
|
|
5125
|
+
// reduced only when the user picks a menu item, so this never allocates per
|
|
5126
|
+
// render and stays a safe useSyncExternalStore consumer.
|
|
5127
|
+
var chat = useChat(function (snapshot) {
|
|
5128
|
+
return snapshot;
|
|
5129
|
+
});
|
|
5130
|
+
var feedbackState = React.useState('');
|
|
5131
|
+
var feedback = feedbackState[0];
|
|
5132
|
+
var setFeedback = feedbackState[1];
|
|
5133
|
+
var menuState = React.useState(false);
|
|
5134
|
+
var menuOpen = menuState[0];
|
|
5135
|
+
var setMenuOpen = menuState[1];
|
|
5136
|
+
var anchorState = React.useState(null);
|
|
5137
|
+
var anchor = anchorState[0];
|
|
5138
|
+
var setAnchor = anchorState[1];
|
|
5139
|
+
var rootRef = React.useRef(null);
|
|
5140
|
+
var buttonRef = React.useRef(null);
|
|
5141
|
+
React.useEffect(function () {
|
|
5142
|
+
if (feedback === '')
|
|
5143
|
+
return undefined;
|
|
5144
|
+
var timer = setTimeout(function () {
|
|
5145
|
+
setFeedback('');
|
|
5146
|
+
}, feedback === 'saved' ? COPY_FEEDBACK_MS : COPY_FAILED_MS);
|
|
5147
|
+
return function () {
|
|
5148
|
+
clearTimeout(timer);
|
|
5149
|
+
};
|
|
5150
|
+
}, [feedback]);
|
|
5151
|
+
// The header clips its own children, so the menu is fixed-positioned under
|
|
5152
|
+
// the button and dismissed like a native menu: outside press, Escape, or
|
|
5153
|
+
// any scroll/resize that would leave it floating in the wrong place.
|
|
5154
|
+
React.useEffect(function () {
|
|
5155
|
+
if (!menuOpen)
|
|
5156
|
+
return undefined;
|
|
5157
|
+
function close() {
|
|
5158
|
+
setMenuOpen(false);
|
|
5159
|
+
}
|
|
5160
|
+
function onPointerDown(event) {
|
|
5161
|
+
var root = rootRef.current;
|
|
5162
|
+
if (root !== null && typeof root.contains === 'function' && root.contains(event.target))
|
|
5163
|
+
return;
|
|
5164
|
+
close();
|
|
5165
|
+
}
|
|
5166
|
+
function onKeyDown(event) {
|
|
5167
|
+
if (event.key === 'Escape')
|
|
5168
|
+
close();
|
|
5169
|
+
}
|
|
5170
|
+
document.addEventListener('pointerdown', onPointerDown, true);
|
|
5171
|
+
document.addEventListener('keydown', onKeyDown);
|
|
5172
|
+
window.addEventListener('resize', close);
|
|
5173
|
+
window.addEventListener('scroll', close, true);
|
|
5174
|
+
return function () {
|
|
5175
|
+
document.removeEventListener('pointerdown', onPointerDown, true);
|
|
5176
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
5177
|
+
window.removeEventListener('resize', close);
|
|
5178
|
+
window.removeEventListener('scroll', close, true);
|
|
5179
|
+
};
|
|
5180
|
+
}, [menuOpen]);
|
|
5181
|
+
function visibleNodes() {
|
|
5182
|
+
var list = [];
|
|
5183
|
+
if (chat === null || chat === undefined)
|
|
5184
|
+
return list;
|
|
5185
|
+
var order = Array.isArray(chat.order) ? chat.order : [];
|
|
5186
|
+
var nodes = chat.nodes;
|
|
5187
|
+
if (nodes === null || nodes === undefined || typeof nodes.get !== 'function')
|
|
5188
|
+
return list;
|
|
5189
|
+
for (var index = 0; index < order.length; index += 1) {
|
|
5190
|
+
var node = nodes.get(order[index]);
|
|
5191
|
+
if (node !== null && node !== undefined)
|
|
5192
|
+
list.push(node);
|
|
5193
|
+
}
|
|
5194
|
+
return list;
|
|
5195
|
+
}
|
|
5196
|
+
function openMenu() {
|
|
5197
|
+
var button = buttonRef.current;
|
|
5198
|
+
if (button !== null && typeof button.getBoundingClientRect === 'function') {
|
|
5199
|
+
var rect = button.getBoundingClientRect();
|
|
5200
|
+
var width = 226;
|
|
5201
|
+
var viewport = typeof window === 'undefined' ? width : window.innerWidth;
|
|
5202
|
+
var left = Math.max(8, Math.min(rect.right - width, viewport - width - 8));
|
|
5203
|
+
setAnchor({ left: left, top: rect.bottom + 6 });
|
|
5204
|
+
}
|
|
5205
|
+
setMenuOpen(true);
|
|
5206
|
+
}
|
|
5207
|
+
function save(mode) {
|
|
5208
|
+
setMenuOpen(false);
|
|
5209
|
+
var draft = chatMemoDraft(visibleNodes(), {
|
|
5210
|
+
you: t('chat.you'),
|
|
5211
|
+
assistant: t('chat.assistant'),
|
|
5212
|
+
fallbackTitle: t('chat.fallbackTitle')
|
|
5213
|
+
}, mode);
|
|
5214
|
+
if (draft === null) {
|
|
5215
|
+
setFeedback(mode === 'prompts' ? 'empty-prompts' : 'empty');
|
|
5216
|
+
return;
|
|
5217
|
+
}
|
|
5218
|
+
var note = notes.create();
|
|
5219
|
+
if (note === undefined) {
|
|
5220
|
+
setFeedback('limit');
|
|
5221
|
+
return;
|
|
5222
|
+
}
|
|
5223
|
+
notes.update(note.id, { title: draft.title, body: draft.body });
|
|
5224
|
+
notes.flush();
|
|
5225
|
+
openRequest.set(note.id);
|
|
5226
|
+
if (typeof props.reveal === 'function')
|
|
5227
|
+
props.reveal();
|
|
5228
|
+
setFeedback('saved');
|
|
5229
|
+
}
|
|
5230
|
+
var saved = feedback === 'saved';
|
|
5231
|
+
var failed = feedback !== '' && !saved;
|
|
5232
|
+
var label = saved
|
|
5233
|
+
? t('chat.saved')
|
|
5234
|
+
: feedback === 'limit'
|
|
5235
|
+
? t('chat.limit')
|
|
5236
|
+
: feedback === 'empty-prompts'
|
|
5237
|
+
? t('chat.emptyPrompts')
|
|
5238
|
+
: feedback === 'empty'
|
|
5239
|
+
? t('chat.empty')
|
|
5240
|
+
: t('chat.create');
|
|
5241
|
+
var menuStyle = anchor === null ? undefined : { left: anchor.left + 'px', top: anchor.top + 'px' };
|
|
5242
|
+
return h('div', { className: 'dsm-chat-memo', ref: rootRef }, h('button', {
|
|
5243
|
+
type: 'button',
|
|
5244
|
+
className: 'dsm-toggle',
|
|
5245
|
+
ref: buttonRef,
|
|
5246
|
+
'data-side-memo-chat-memo': 'true',
|
|
5247
|
+
'data-state': saved ? 'saved' : failed ? 'failed' : 'idle',
|
|
5248
|
+
'data-active': menuOpen ? 'true' : 'false',
|
|
5249
|
+
'aria-haspopup': 'menu',
|
|
5250
|
+
'aria-expanded': menuOpen ? 'true' : 'false',
|
|
5251
|
+
title: label,
|
|
5252
|
+
'aria-label': saved || failed ? label : t('chat.createAria'),
|
|
5253
|
+
onClick: function () {
|
|
5254
|
+
if (menuOpen)
|
|
5255
|
+
setMenuOpen(false);
|
|
5256
|
+
else
|
|
5257
|
+
openMenu();
|
|
5258
|
+
}
|
|
5259
|
+
}, saved ? h(IconCheck, { size: 16 }) : h(IconChatMemo, { size: 16 })), menuOpen
|
|
5260
|
+
? h('div', {
|
|
5261
|
+
className: 'dsm-chat-memo-menu',
|
|
5262
|
+
role: 'menu',
|
|
5263
|
+
'aria-label': t('chat.menu.label'),
|
|
5264
|
+
style: menuStyle
|
|
5265
|
+
}, h('button', {
|
|
5266
|
+
type: 'button',
|
|
5267
|
+
role: 'menuitem',
|
|
5268
|
+
className: 'dsm-chat-memo-item',
|
|
5269
|
+
onClick: function () {
|
|
5270
|
+
save('chat');
|
|
5271
|
+
}
|
|
5272
|
+
}, h('span', null, t('chat.menu.chat')), h('span', { className: 'dsm-chat-memo-note' }, t('chat.menu.chatNote'))), h('button', {
|
|
5273
|
+
type: 'button',
|
|
5274
|
+
role: 'menuitem',
|
|
5275
|
+
className: 'dsm-chat-memo-item',
|
|
5276
|
+
onClick: function () {
|
|
5277
|
+
save('prompts');
|
|
5278
|
+
}
|
|
5279
|
+
}, h('span', null, t('chat.menu.prompts')), h('span', { className: 'dsm-chat-memo-note' }, t('chat.menu.promptsNote'))))
|
|
5280
|
+
: null, h('span', { className: 'dsm-sr-only', role: 'status', 'aria-live': 'polite' }, saved ? t('chat.saved') : ''));
|
|
5281
|
+
}
|
|
5282
|
+
/**
|
|
5283
|
+
* Quick "save this prompt as a memo" affordance.
|
|
5284
|
+
*
|
|
5285
|
+
* DSH renders a user message's action row itself and exposes no slot for it,
|
|
5286
|
+
* so the button is fixed-positioned next to that row and follows the pointer
|
|
5287
|
+
* from message to message. The prompt text is read from the
|
|
5288
|
+
* `conversation.chat.user.content` slot anchor, and the button lives in the
|
|
5289
|
+
* click-through `shell.overlay` layer so the message list's overflow and
|
|
5290
|
+
* hover scopes cannot clip it.
|
|
5291
|
+
*/
|
|
5292
|
+
function NotesPromptMemo(props) {
|
|
5293
|
+
var t = translateFor(props);
|
|
5294
|
+
var labels = {
|
|
5295
|
+
you: t('chat.you'),
|
|
5296
|
+
assistant: t('chat.assistant'),
|
|
5297
|
+
fallbackTitle: t('chat.fallbackTitle')
|
|
5298
|
+
};
|
|
5299
|
+
var notes = props.notes;
|
|
5300
|
+
var openRequest = props.openRequest;
|
|
5301
|
+
var labelsRef = React.useRef(labels);
|
|
5302
|
+
labelsRef.current = labels;
|
|
5303
|
+
var targetState = React.useState(null);
|
|
5304
|
+
var target = targetState[0];
|
|
5305
|
+
var setTarget = targetState[1];
|
|
5306
|
+
var targetRef = React.useRef(target);
|
|
5307
|
+
targetRef.current = target;
|
|
5308
|
+
var feedbackState = React.useState('');
|
|
5309
|
+
var feedback = feedbackState[0];
|
|
5310
|
+
var setFeedback = feedbackState[1];
|
|
5311
|
+
var feedbackRef = React.useRef(feedback);
|
|
5312
|
+
feedbackRef.current = feedback;
|
|
5313
|
+
var hideTimer = React.useRef(null);
|
|
5314
|
+
function clearHide() {
|
|
5315
|
+
var timer = hideTimer.current;
|
|
5316
|
+
if (timer === null)
|
|
5317
|
+
return;
|
|
5318
|
+
clearTimeout(timer);
|
|
5319
|
+
hideTimer.current = null;
|
|
5320
|
+
}
|
|
5321
|
+
function hide() {
|
|
5322
|
+
clearHide();
|
|
5323
|
+
setTarget(null);
|
|
5324
|
+
}
|
|
5325
|
+
function scheduleHide() {
|
|
5326
|
+
clearHide();
|
|
5327
|
+
hideTimer.current = window.setTimeout(function () {
|
|
5328
|
+
hideTimer.current = null;
|
|
5329
|
+
setTarget(null);
|
|
5330
|
+
}, 140);
|
|
5331
|
+
}
|
|
5332
|
+
React.useEffect(function () {
|
|
5333
|
+
if (feedback === '')
|
|
5334
|
+
return undefined;
|
|
5335
|
+
var timer = setTimeout(function () {
|
|
5336
|
+
setFeedback('');
|
|
5337
|
+
}, feedback === 'saved' ? COPY_FEEDBACK_MS : COPY_FAILED_MS);
|
|
5338
|
+
return function () {
|
|
5339
|
+
clearTimeout(timer);
|
|
5340
|
+
};
|
|
5341
|
+
}, [feedback]);
|
|
5342
|
+
React.useEffect(function () {
|
|
5343
|
+
if (typeof document === 'undefined')
|
|
5344
|
+
return undefined;
|
|
5345
|
+
function elementOf(value) {
|
|
5346
|
+
return value !== null && typeof value === 'object' && 'closest' in value ? value : null;
|
|
5347
|
+
}
|
|
5348
|
+
function locate(item) {
|
|
5349
|
+
var row = item.querySelector('[data-clock="start"]');
|
|
5350
|
+
if (row === null)
|
|
5351
|
+
return null;
|
|
5352
|
+
var copy = row.querySelector('button');
|
|
5353
|
+
if (copy === null)
|
|
5354
|
+
return null;
|
|
5355
|
+
var copyBox = copy.getBoundingClientRect();
|
|
5356
|
+
if (copyBox.width === 0 && copyBox.height === 0)
|
|
5357
|
+
return null;
|
|
5358
|
+
var first = row.children.length === 0 ? null : row.children[0];
|
|
5359
|
+
var time = first !== null && first.tagName === 'SPAN' ? first : null;
|
|
5360
|
+
var left = Math.round(copyBox.right + 5);
|
|
5361
|
+
if (time !== null) {
|
|
5362
|
+
var timeBox = time.getBoundingClientRect();
|
|
5363
|
+
if (timeBox.right + 26 <= copyBox.left)
|
|
5364
|
+
left = Math.round(timeBox.right + 4);
|
|
5365
|
+
else
|
|
5366
|
+
left = Math.round((timeBox.right + copyBox.left) / 2 - 11);
|
|
5367
|
+
}
|
|
5368
|
+
var content = item.querySelector('[data-slot="conversation.chat.user.content"]');
|
|
5369
|
+
var text = content === null ? '' : content.innerText || content.textContent || '';
|
|
5370
|
+
if (promptMemoDraft(text, labelsRef.current) === null)
|
|
5371
|
+
return null;
|
|
5372
|
+
return {
|
|
5373
|
+
text: text,
|
|
5374
|
+
left: left,
|
|
5375
|
+
top: Math.round(copyBox.top + copyBox.height / 2 - 11)
|
|
5376
|
+
};
|
|
5377
|
+
}
|
|
5378
|
+
function onPointerOver(event) {
|
|
5379
|
+
var node = elementOf(event.target);
|
|
5380
|
+
if (node === null)
|
|
5381
|
+
return;
|
|
5382
|
+
if (node.closest('[data-side-memo-prompt-memo]') !== null) {
|
|
5383
|
+
clearHide();
|
|
5384
|
+
return;
|
|
5385
|
+
}
|
|
5386
|
+
var item = node.closest('[data-chat-flow-kind="user"]');
|
|
5387
|
+
if (item === null)
|
|
5388
|
+
return;
|
|
5389
|
+
var next = locate(item);
|
|
5390
|
+
if (next === null)
|
|
5391
|
+
return;
|
|
5392
|
+
var current = targetRef.current;
|
|
5393
|
+
if (current !== null && current.text === next.text && current.left === next.left && current.top === next.top)
|
|
5394
|
+
return;
|
|
5395
|
+
clearHide();
|
|
5396
|
+
if (feedbackRef.current !== '')
|
|
5397
|
+
setFeedback('');
|
|
5398
|
+
setTarget(next);
|
|
5399
|
+
}
|
|
5400
|
+
function onPointerOut(event) {
|
|
5401
|
+
var node = elementOf(event.target);
|
|
5402
|
+
if (node === null)
|
|
5403
|
+
return;
|
|
5404
|
+
var item = node.closest('[data-chat-flow-kind="user"]');
|
|
5405
|
+
if (item === null)
|
|
5406
|
+
return;
|
|
5407
|
+
var related = elementOf(event.relatedTarget);
|
|
5408
|
+
if (related !== null && (related.closest('[data-chat-flow-kind="user"]') === item || related.closest('[data-side-memo-prompt-memo]') !== null))
|
|
5409
|
+
return;
|
|
5410
|
+
scheduleHide();
|
|
5411
|
+
}
|
|
5412
|
+
function onPointerDown(event) {
|
|
5413
|
+
var node = elementOf(event.target);
|
|
5414
|
+
if (node !== null && node.closest('[data-side-memo-prompt-memo]') !== null)
|
|
5415
|
+
return;
|
|
5416
|
+
hide();
|
|
5417
|
+
}
|
|
5418
|
+
function onKeyDown(event) {
|
|
5419
|
+
if (event.key === 'Escape')
|
|
5420
|
+
hide();
|
|
5421
|
+
}
|
|
5422
|
+
document.addEventListener('pointerover', onPointerOver, true);
|
|
5423
|
+
document.addEventListener('pointerout', onPointerOut, true);
|
|
5424
|
+
document.addEventListener('pointerdown', onPointerDown, true);
|
|
5425
|
+
document.addEventListener('keydown', onKeyDown);
|
|
5426
|
+
window.addEventListener('scroll', hide, true);
|
|
5427
|
+
window.addEventListener('resize', hide);
|
|
5428
|
+
return function () {
|
|
5429
|
+
clearHide();
|
|
5430
|
+
document.removeEventListener('pointerover', onPointerOver, true);
|
|
5431
|
+
document.removeEventListener('pointerout', onPointerOut, true);
|
|
5432
|
+
document.removeEventListener('pointerdown', onPointerDown, true);
|
|
5433
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
5434
|
+
window.removeEventListener('scroll', hide, true);
|
|
5435
|
+
window.removeEventListener('resize', hide);
|
|
5436
|
+
};
|
|
5437
|
+
}, []);
|
|
5438
|
+
function save() {
|
|
5439
|
+
if (target === null)
|
|
5440
|
+
return;
|
|
5441
|
+
var draft = promptMemoDraft(target.text, labels);
|
|
5442
|
+
if (draft === null)
|
|
5443
|
+
return;
|
|
5444
|
+
var note = notes.create();
|
|
5445
|
+
if (note === undefined) {
|
|
5446
|
+
setFeedback('limit');
|
|
5447
|
+
return;
|
|
5448
|
+
}
|
|
5449
|
+
notes.update(note.id, { title: draft.title, body: draft.body });
|
|
5450
|
+
notes.flush();
|
|
5451
|
+
openRequest.set(note.id);
|
|
5452
|
+
if (typeof props.reveal === 'function')
|
|
5453
|
+
props.reveal();
|
|
5454
|
+
setFeedback('saved');
|
|
5455
|
+
}
|
|
5456
|
+
var saved = feedback === 'saved';
|
|
5457
|
+
var failed = feedback !== '' && !saved;
|
|
5458
|
+
var status = saved ? t('chat.saved') : feedback === 'limit' ? t('chat.limit') : '';
|
|
5459
|
+
if (target === null) {
|
|
5460
|
+
return h('span', { className: 'dsm-sr-only', role: 'status', 'aria-live': 'polite' }, status);
|
|
5461
|
+
}
|
|
5462
|
+
return h(React.Fragment, null, h('button', {
|
|
5463
|
+
type: 'button',
|
|
5464
|
+
className: 'dsm-prompt-memo',
|
|
5465
|
+
'data-side-memo-prompt-memo': 'true',
|
|
5466
|
+
'data-state': saved ? 'saved' : failed ? 'failed' : 'idle',
|
|
5467
|
+
style: { left: target.left + 'px', top: target.top + 'px' },
|
|
5468
|
+
title: saved ? t('chat.saved') : t('prompt.create'),
|
|
5469
|
+
'aria-label': t('prompt.createAria'),
|
|
5470
|
+
onClick: save,
|
|
5471
|
+
onPointerEnter: clearHide,
|
|
5472
|
+
onPointerLeave: scheduleHide
|
|
5473
|
+
}, saved ? h(IconCheck, { size: 13 }) : h(IconChatMemo, { size: 13 })), h('span', { className: 'dsm-sr-only', role: 'status', 'aria-live': 'polite' }, status));
|
|
5474
|
+
}
|
|
4448
5475
|
/**
|
|
4449
5476
|
* The header toggle for the phases without the normal Session chrome.
|
|
4450
5477
|
*
|
|
@@ -4594,7 +5621,7 @@ window.__ModuleLoader__.load({
|
|
|
4594
5621
|
onClick: function () {
|
|
4595
5622
|
drawer.set(false);
|
|
4596
5623
|
}
|
|
4597
|
-
}, h(IconX, { size: 16 }))), h('div', { className: 'dsm-drawer-body' }, h(NotesBody, { t: t, notes: props.notes }))));
|
|
5624
|
+
}, h(IconX, { size: 16 }))), h('div', { className: 'dsm-drawer-body' }, h(NotesBody, { t: t, notes: props.notes, openRequest: props.openRequest }))));
|
|
4598
5625
|
}
|
|
4599
5626
|
// ---------------------------------------------------------------------
|
|
4600
5627
|
// Plugin body
|
|
@@ -4620,6 +5647,7 @@ window.__ModuleLoader__.load({
|
|
|
4620
5647
|
fallbackT = ctx.locale.bind(NS);
|
|
4621
5648
|
var notes = createNotesStore();
|
|
4622
5649
|
var drawer = createDrawerStore();
|
|
5650
|
+
var openRequest = createOpenRequestStore();
|
|
4623
5651
|
var mountedBinding = createMountedBinding(function () {
|
|
4624
5652
|
return ctx.sidebarRight;
|
|
4625
5653
|
});
|
|
@@ -4719,6 +5747,22 @@ window.__ModuleLoader__.load({
|
|
|
4719
5747
|
}, 'could not toggle the notes panel in the right sidebar');
|
|
4720
5748
|
drawer.set(false);
|
|
4721
5749
|
}
|
|
5750
|
+
// Unlike toggleNotes, this never collapses an already-open panel: the
|
|
5751
|
+
// chat-memo button saved a memo and has to show it, not hide it.
|
|
5752
|
+
function revealNotes() {
|
|
5753
|
+
var mounted = readMountedSession(ctx.sidebarRight);
|
|
5754
|
+
if (mounted === undefined) {
|
|
5755
|
+
drawer.set(true);
|
|
5756
|
+
return;
|
|
5757
|
+
}
|
|
5758
|
+
retrySidebarCommand(function () {
|
|
5759
|
+
var right = ctx.sidebarRight;
|
|
5760
|
+
right.openTab(KIND);
|
|
5761
|
+
if (!right.isExpanded())
|
|
5762
|
+
right.toggleExpanded();
|
|
5763
|
+
}, 'could not open the notes panel in the right sidebar');
|
|
5764
|
+
drawer.set(false);
|
|
5765
|
+
}
|
|
4722
5766
|
ctx.effect(function () {
|
|
4723
5767
|
return ctx.slots.inject('conversation.session.header.utilities', function () {
|
|
4724
5768
|
return ctx.slots.register({
|
|
@@ -4732,6 +5776,37 @@ window.__ModuleLoader__.load({
|
|
|
4732
5776
|
}, NotesToggle);
|
|
4733
5777
|
});
|
|
4734
5778
|
}, 'side-memo: header toggle');
|
|
5779
|
+
// Turn the visible conversation into a memo in one click. The slot is
|
|
5780
|
+
// Session-scope, which is what supplies the standard `useChat` hook.
|
|
5781
|
+
ctx.effect(function () {
|
|
5782
|
+
return ctx.slots.inject('conversation.session.header.utilities', function () {
|
|
5783
|
+
return ctx.slots.register({
|
|
5784
|
+
name: 'conversation.session.header.utilities',
|
|
5785
|
+
id: 'side-memo-chat-memo',
|
|
5786
|
+
order: 31,
|
|
5787
|
+
locale: NS,
|
|
5788
|
+
inject: function () {
|
|
5789
|
+
return { reveal: revealNotes, notes: notes, openRequest: openRequest };
|
|
5790
|
+
}
|
|
5791
|
+
}, NotesChatMemo);
|
|
5792
|
+
});
|
|
5793
|
+
}, 'side-memo: chat memo');
|
|
5794
|
+
// One click next to a prompt turns that prompt into a memo. User messages
|
|
5795
|
+
// have no action slot, so the button rides the click-through overlay layer
|
|
5796
|
+
// and locates the hovered prompt in the message list.
|
|
5797
|
+
ctx.effect(function () {
|
|
5798
|
+
return ctx.slots.inject('shell.overlay', function () {
|
|
5799
|
+
return ctx.slots.register({
|
|
5800
|
+
name: 'shell.overlay',
|
|
5801
|
+
id: 'side-memo.prompt-memo',
|
|
5802
|
+
order: 45,
|
|
5803
|
+
locale: NS,
|
|
5804
|
+
inject: function () {
|
|
5805
|
+
return { notes: notes, openRequest: openRequest, reveal: revealNotes };
|
|
5806
|
+
}
|
|
5807
|
+
}, NotesPromptMemo);
|
|
5808
|
+
});
|
|
5809
|
+
}, 'side-memo: prompt memo');
|
|
4735
5810
|
// The resident header's leading seat is the one place that exists before a
|
|
4736
5811
|
// Session and while the new-Session (blank) chrome is up, so the toggle is
|
|
4737
5812
|
// there exactly for those phases; once the Session is established the
|
|
@@ -4760,7 +5835,7 @@ window.__ModuleLoader__.load({
|
|
|
4760
5835
|
order: 40,
|
|
4761
5836
|
locale: NS,
|
|
4762
5837
|
inject: function () {
|
|
4763
|
-
return { notes: notes, drawer: drawer };
|
|
5838
|
+
return { notes: notes, drawer: drawer, openRequest: openRequest };
|
|
4764
5839
|
}
|
|
4765
5840
|
}, NotesDrawer);
|
|
4766
5841
|
});
|
|
@@ -4798,7 +5873,7 @@ window.__ModuleLoader__.load({
|
|
|
4798
5873
|
locale: NS,
|
|
4799
5874
|
// Slot inject is a factory called as inject(bindingKey, actions).
|
|
4800
5875
|
inject: function () {
|
|
4801
|
-
return { notes: notes };
|
|
5876
|
+
return { notes: notes, openRequest: openRequest };
|
|
4802
5877
|
},
|
|
4803
5878
|
children: {
|
|
4804
5879
|
'sidebar.right.tab.notes.actions': {
|
|
@@ -4887,6 +5962,7 @@ window.__ModuleLoader__.load({
|
|
|
4887
5962
|
noteImageIds: noteImageIds,
|
|
4888
5963
|
normalizeImageRecord: normalizeImageRecord,
|
|
4889
5964
|
sortNotes: sortNotes,
|
|
5965
|
+
placeContextMenu: placeContextMenu,
|
|
4890
5966
|
ageLabel: ageLabel,
|
|
4891
5967
|
crc32: crc32,
|
|
4892
5968
|
encodeText: encodeText,
|
|
@@ -4904,7 +5980,12 @@ window.__ModuleLoader__.load({
|
|
|
4904
5980
|
importFavorite: importFavorite,
|
|
4905
5981
|
parseLabelCell: parseLabelCell,
|
|
4906
5982
|
normalizedHeader: normalizedHeader,
|
|
4907
|
-
stripXmlPrefixes: stripXmlPrefixes
|
|
5983
|
+
stripXmlPrefixes: stripXmlPrefixes,
|
|
5984
|
+
// Chat → memo helpers (also used by the Node unit suite).
|
|
5985
|
+
chatMemoEntries: chatMemoEntries,
|
|
5986
|
+
chatMemoDraft: chatMemoDraft,
|
|
5987
|
+
createOpenRequestStore: createOpenRequestStore,
|
|
5988
|
+
promptMemoDraft: promptMemoDraft
|
|
4908
5989
|
};
|
|
4909
5990
|
}
|
|
4910
5991
|
return module.exports;
|