dsh-edit-turn 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -1
- package/lib/client.js +1221 -70
- package/lib/index.js +116 -4
- package/lib/patch.js +27 -9
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -22,6 +22,9 @@ window.__ModuleLoader__.load({
|
|
|
22
22
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
23
23
|
|
|
24
24
|
const react = require('react')
|
|
25
|
+
// DOM injection has to land before the browser paints, or the host's own
|
|
26
|
+
// buttons are visible for a frame and ours pop in after them.
|
|
27
|
+
const useBrowserLayoutEffect = typeof react.useLayoutEffect === 'function' ? react.useLayoutEffect : react.useEffect
|
|
25
28
|
const jsxRuntime = require('react/jsx-runtime')
|
|
26
29
|
const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
27
30
|
const { jsx, jsxs, Fragment } = jsxRuntime
|
|
@@ -42,7 +45,7 @@ window.__ModuleLoader__.load({
|
|
|
42
45
|
const RERUN_PREFIX = '/dsh-rerun-turn'
|
|
43
46
|
|
|
44
47
|
/** Keep in sync with package.json and lib/index.js. */
|
|
45
|
-
const PLUGIN_VERSION = '0.
|
|
48
|
+
const PLUGIN_VERSION = '0.5.0'
|
|
46
49
|
|
|
47
50
|
// --- copy -----------------------------------------------------------------
|
|
48
51
|
|
|
@@ -65,6 +68,37 @@ window.__ModuleLoader__.load({
|
|
|
65
68
|
'editor.rerun': '重跑',
|
|
66
69
|
'editor.save-reply': '保存',
|
|
67
70
|
'editor.block': '编辑哪一块',
|
|
71
|
+
'editor.block.all': '全部',
|
|
72
|
+
'config.style': '编辑器样式',
|
|
73
|
+
'config.placement': '编辑器位置',
|
|
74
|
+
'config.height': '编辑器高度',
|
|
75
|
+
'height.max': '一轮最大',
|
|
76
|
+
'height.content': '随内容',
|
|
77
|
+
'height.fill': '全部铺满',
|
|
78
|
+
'config.summary.height.max': '一轮最大',
|
|
79
|
+
'config.summary.height.content': '随内容',
|
|
80
|
+
'config.summary.height.fill': '全部铺满',
|
|
81
|
+
'config.hint.height.max': '取这一轮所有思维链/正文里的最大高度,切换块时框不变。',
|
|
82
|
+
'config.hint.height.content': '框高跟着当前块的内容走,切换块时框会变。',
|
|
83
|
+
'config.hint.height.fill': '普通块按一轮最大;选「全部」时自动铺满对话区到输入框之间的整段高度(刚好触碰上下边界),只有一个滚动条。',
|
|
84
|
+
'placement.overlay': '浮层',
|
|
85
|
+
'placement.inline': '页面内联',
|
|
86
|
+
'placement.legacy': '整行旧版',
|
|
87
|
+
'config.summary.legacy': '整行旧版',
|
|
88
|
+
'config.hint.legacy': '整条 assistant 行套框、正文保持旧的内部滚动;同样向下盖住 action bar,功能键在左下。',
|
|
89
|
+
'config.summary.overlay': '浮层',
|
|
90
|
+
'config.summary.inline': '页面内联',
|
|
91
|
+
'config.hint.overlay': '悬浮在视口内,长内容在编辑框内滚动。',
|
|
92
|
+
'config.hint.inline': '按页面坐标铺在消息上,跟随页面滚动,高度按内容展开,不再截到视口。',
|
|
93
|
+
'config.summary.frame': '整行描边',
|
|
94
|
+
'config.summary.bubble': '贴合气泡',
|
|
95
|
+
'config.summary.center': '居中面板',
|
|
96
|
+
'config.hint.frame': '一圈描边套住整条消息行,正文输入框落在消息文字上,按钮在右上角。',
|
|
97
|
+
'config.hint.bubble': '编辑框精确等于消息气泡的大小。',
|
|
98
|
+
'config.hint.center': '大号居中面板,输入框随内容增长。',
|
|
99
|
+
'style.frame': '整行',
|
|
100
|
+
'style.bubble': '气泡',
|
|
101
|
+
'style.center': '居中',
|
|
68
102
|
'editor.block.text': '正文',
|
|
69
103
|
'editor.block.reasoning': '思维链',
|
|
70
104
|
'editor.placeholder-block': '只改这一块;工具调用、其余内容和后续对话保持不变。',
|
|
@@ -120,6 +154,37 @@ window.__ModuleLoader__.load({
|
|
|
120
154
|
'editor.rerun': 'Re-run',
|
|
121
155
|
'editor.save-reply': 'Save',
|
|
122
156
|
'editor.block': 'Block',
|
|
157
|
+
'editor.block.all': 'All blocks',
|
|
158
|
+
'config.style': 'Editor style',
|
|
159
|
+
'config.placement': 'Editor placement',
|
|
160
|
+
'config.height': 'Editor height',
|
|
161
|
+
'height.max': 'Turn max',
|
|
162
|
+
'height.content': 'Content',
|
|
163
|
+
'height.fill': 'Fill when all',
|
|
164
|
+
'config.summary.height.max': 'Turn max',
|
|
165
|
+
'config.summary.height.content': 'Content',
|
|
166
|
+
'config.summary.height.fill': 'Fill when all',
|
|
167
|
+
'config.hint.height.max': 'Uses the tallest reasoning/text block of the turn, so the box does not resize when switching.',
|
|
168
|
+
'config.hint.height.content': 'The box follows the current block, so it resizes when switching.',
|
|
169
|
+
'config.hint.height.fill': 'Single blocks use the turn max; the all-blocks page fills the whole band between the conversation top and the composer, with one scrollbar.',
|
|
170
|
+
'placement.overlay': 'Overlay',
|
|
171
|
+
'placement.inline': 'In-page',
|
|
172
|
+
'placement.legacy': 'Legacy row',
|
|
173
|
+
'config.summary.legacy': 'Legacy row',
|
|
174
|
+
'config.hint.legacy': 'Frames the whole assistant row and keeps the old inner scroll; still covers the action bar, with the controls at the bottom left.',
|
|
175
|
+
'config.summary.overlay': 'Overlay',
|
|
176
|
+
'config.summary.inline': 'In-page',
|
|
177
|
+
'config.hint.overlay': 'Floats inside the viewport; long content scrolls inside the editor.',
|
|
178
|
+
'config.hint.inline': 'Lays over the message in page coordinates and scrolls with the page; it grows with the content instead of being clipped to the viewport.',
|
|
179
|
+
'config.summary.frame': 'Row frame',
|
|
180
|
+
'config.summary.bubble': 'Exact bubble',
|
|
181
|
+
'config.summary.center': 'Centered panel',
|
|
182
|
+
'config.hint.frame': 'Outline the whole message row; the textarea sits on the message text and the actions move to the top-right.',
|
|
183
|
+
'config.hint.bubble': 'The editor is exactly the message bubble.',
|
|
184
|
+
'config.hint.center': 'A large centred panel whose textarea grows with the content.',
|
|
185
|
+
'style.frame': 'Frame',
|
|
186
|
+
'style.bubble': 'Bubble',
|
|
187
|
+
'style.center': 'Center',
|
|
123
188
|
'editor.block.text': 'Answer',
|
|
124
189
|
'editor.block.reasoning': 'Reasoning',
|
|
125
190
|
'editor.placeholder-block': 'Only this block changes; tool calls, the rest of the reply and later turns stay.',
|
|
@@ -222,6 +287,9 @@ window.__ModuleLoader__.load({
|
|
|
222
287
|
// positioned button inside the bubble used to land on the last line, which
|
|
223
288
|
// is how the rewritten prompt got covered up.
|
|
224
289
|
'.dshet-revision.dshet-revision-action{padding-inline-end:40px}',
|
|
290
|
+
'.dshet-revision-text{display:block}',
|
|
291
|
+
'.dshet-revision-files{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}',
|
|
292
|
+
'.dshet-revision-file{display:inline-flex;align-items:center;gap:6px;max-width:100%;padding:2px 6px 2px 2px;border:1px solid var(--dshet-line);border-radius:8px;background:var(--dshet-chip);font-size:12px;line-height:18px}',
|
|
225
293
|
'.dshet-revision .dshet-action-host{position:absolute;inset-inline-end:6px;top:6px;opacity:0;transition:opacity .12s ease}',
|
|
226
294
|
'.dshet-revision:hover .dshet-action-host,.dshet-revision:focus-within .dshet-action-host{opacity:1}',
|
|
227
295
|
// A sibling plugin can append its own button (dsh-delete-turn's bin) into
|
|
@@ -260,11 +328,20 @@ window.__ModuleLoader__.load({
|
|
|
260
328
|
// composition and its clicks a piece at a time. The layer covers the
|
|
261
329
|
// viewport but only the box itself takes pointer events, and the box is
|
|
262
330
|
// positioned under its row by `placeEditor`.
|
|
331
|
+
'.dshet-layer[data-dshet-inline="1"]{position:absolute;left:0;top:0;right:auto;bottom:auto;width:100%;height:0}',
|
|
263
332
|
'.dshet-layer{position:fixed;left:0;top:0;right:0;bottom:0;pointer-events:none;z-index:40}',
|
|
264
333
|
'.dshet-layer .dshet-editor{position:absolute;pointer-events:auto}',
|
|
265
|
-
'.dshet-editor{display:flex;flex-direction:column;max-height:calc(100vh -
|
|
334
|
+
'.dshet-editor{display:flex;flex-direction:column;max-height:calc(100vh - 24px);overflow-y:auto;overscroll-behavior:contain;margin:0;border:1px solid var(--dshet-line);border-radius:16px;background:var(--dshet-panel);backdrop-filter:blur(18px) saturate(1.2);box-shadow:var(--dshet-shadow);color:var(--dshet-ink);transition:border-color .12s}',
|
|
335
|
+
'.dshet-editor[data-dshet-drop="1"]{outline:2px dashed #4a7dff;outline-offset:-4px}',
|
|
266
336
|
'.dshet-editor:focus-within{border-color:var(--dsw-alias-button-primary-fill,var(--dshet-accent))}',
|
|
267
|
-
'.dshet-editor
|
|
337
|
+
'.dshet-editor[data-dshet-inplace="1"]{overflow:hidden;border:1.5px solid var(--dsw-alias-button-primary-fill,#4a7dff);border-radius:12px;background:var(--dshet-panel);backdrop-filter:none;box-shadow:none}',
|
|
338
|
+
'.dshet-editor[data-dshet-inplace="1"] textarea{position:absolute;min-height:0;max-height:none;padding:6px 10px;border:0;border-radius:8px;background:transparent;overflow-y:auto}',
|
|
339
|
+
'.dshet-editor[data-dshet-inplace="1"] .dshet-footer{position:absolute;top:auto;left:0;right:0;bottom:0;height:46px;align-items:center;padding:6px 10px;box-sizing:border-box;background:var(--dshet-panel);border-radius:0 0 12px 12px;gap:6px}',
|
|
340
|
+
'.dshet-editor[data-dshet-inplace="1"] .dshet-btn{padding:2px 8px;font-size:12px;line-height:18px;border-radius:8px}',
|
|
341
|
+
'.dshet-editor[data-dshet-inplace="1"] .dshet-pick{position:static;top:auto;left:auto;padding:0;background:transparent;border-radius:0}',
|
|
342
|
+
'.dshet-editor[data-dshet-inplace="1"] .dshet-note,.dshet-editor[data-dshet-inplace="1"] .dshet-warn,.dshet-editor[data-dshet-inplace="1"] .dshet-error{position:absolute;left:8px;bottom:6px;max-width:60%;margin:0;background:var(--dshet-panel);border-radius:8px;padding:2px 6px}',
|
|
343
|
+
|
|
344
|
+
'.dshet-editor textarea{display:block;width:100%;min-height:47px;max-height:min(46vh,420px);overflow-y:auto;resize:none;box-sizing:border-box;padding:11px 14px 0;border:0;background:transparent;color:inherit;font:inherit;font-size:var(--dsh-content-font-size,14px);line-height:calc(22px + var(--dsh-content-font-delta,0px));outline:none}',
|
|
268
345
|
'.dshet-editor textarea::placeholder{color:var(--dshet-ink-dim)}',
|
|
269
346
|
'.dshet-editor textarea:disabled{opacity:.65}',
|
|
270
347
|
// The blocks this message carries: one chip each, drawn from the same
|
|
@@ -280,8 +357,9 @@ window.__ModuleLoader__.load({
|
|
|
280
357
|
'.dshet-chip-remove:hover:not(:disabled){background:var(--dshet-hover);color:var(--dsw-alias-state-error-primary,#c93a31)}',
|
|
281
358
|
'.dshet-chip-remove:focus-visible{outline:2px solid var(--dsw-alias-button-primary-fill,var(--dshet-accent));outline-offset:1px}',
|
|
282
359
|
'.dshet-chip-remove:disabled{cursor:default;opacity:.5}',
|
|
283
|
-
'.dshet-attach-row{display:flex;align-items:center;
|
|
284
|
-
'.dshet-attach{
|
|
360
|
+
'.dshet-attach-row{position:absolute;left:12px;bottom:10px;z-index:2;display:flex;align-items:center;margin:0}',
|
|
361
|
+
'.dshet-attach{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:1px 6px;box-sizing:border-box;border:0;border-radius:999px;background:#f5f6f7;color:#0f1115;font:inherit;line-height:1;cursor:pointer;outline:none;box-shadow:none;-webkit-appearance:none;transition:background-color .12s,color .12s}',
|
|
362
|
+
'.dshet-attach svg{width:14px;height:14px;display:block;stroke:currentColor;fill:none;stroke-width:1.3;}',
|
|
285
363
|
'.dshet-attach:hover:not(:disabled){background:var(--dshet-hover);color:var(--dshet-ink)}',
|
|
286
364
|
'.dshet-attach:focus-visible{outline:2px solid var(--dsw-alias-button-primary-fill,var(--dshet-accent));outline-offset:2px}',
|
|
287
365
|
'.dshet-attach:disabled{cursor:default;opacity:.5}',
|
|
@@ -290,15 +368,44 @@ window.__ModuleLoader__.load({
|
|
|
290
368
|
'.dshet-note{color:var(--dshet-ink-dim)}',
|
|
291
369
|
'.dshet-warn{color:var(--dsw-alias-state-warning-primary,#a2650a)}',
|
|
292
370
|
'.dshet-error{color:var(--dsw-alias-state-error-primary,#c93a31)}',
|
|
293
|
-
'.dshet-footer{position:
|
|
371
|
+
'.dshet-footer{position:absolute;left:0;right:0;bottom:0;top:auto;z-index:3;display:flex;align-items:center;justify-content:space-between;gap:6px;height:46px;padding:6px 10px;box-sizing:border-box;background:var(--dshet-panel);border-radius:0 0 16px 16px}',
|
|
372
|
+
'.dshet-footer-actions{display:flex;align-items:center;gap:6px}',
|
|
373
|
+
'.dshet-all-nav{position:sticky;top:0;z-index:2;display:flex;flex-wrap:nowrap;align-items:center;gap:6px;min-height:28px;padding:4px 0 6px;background:var(--dshet-panel);overflow-x:auto;scrollbar-width:none}',
|
|
374
|
+
'.dshet-all-nav button{flex:none;border:0;border-radius:999px;background:var(--dshet-hover);color:var(--dshet-ink-dim);font:inherit;font-size:12px;line-height:18px;padding:3px 10px;cursor:pointer;white-space:nowrap}',
|
|
375
|
+
'.dshet-all-nav button:hover{color:var(--dshet-ink)}',
|
|
376
|
+
'.dshet-all{position:absolute;left:8px;right:8px;top:6px;bottom:52px;overflow-y:auto;display:flex;flex-direction:column;padding:2px 4px 4px}',
|
|
377
|
+
'.dshet-all-block{display:flex;flex-direction:column;padding:6px 0 8px}',
|
|
378
|
+
'.dshet-all-block:first-child{padding-top:2px}',
|
|
379
|
+
'.dshet-all-head{display:flex;width:100%;box-sizing:border-box;align-items:center;gap:8px;font-size:11px;color:var(--dshet-ink-dim);padding:0 2px 4px}',
|
|
380
|
+
'.dshet-all-head>span:first-child{background:var(--dshet-hover);border-radius:6px;padding:1px 6px;white-space:nowrap}',
|
|
381
|
+
'.dshet-all-line{flex:1;height:1px;background:var(--dshet-line);opacity:.55}',
|
|
382
|
+
'.dshet-editor[data-dshet-inplace="1"] .dshet-all .dshet-all-area{position:static;left:auto;top:auto;width:100%;height:auto;min-height:32px;max-height:none;padding:0 2px;border-radius:0;overflow:hidden}',
|
|
383
|
+
'.dshet-all-area{display:block;width:100%;min-height:32px;max-height:none;box-sizing:border-box;border:0;background:transparent;color:inherit;font:inherit;font-size:var(--dsh-content-font-size,14px);line-height:calc(22px + var(--dsh-content-font-delta,0px));resize:none;outline:none;overflow:hidden;scrollbar-width:none;padding:0 2px}',
|
|
384
|
+
'.dshet-editor .dshet-footer .dshet-btn{height:34px;box-sizing:border-box;padding:0 14px}',
|
|
294
385
|
'.dshet-btn{padding:6px 16px;border:1px solid var(--dshet-line);border-radius:999px;background:var(--dshet-field);color:var(--dshet-ink);font:inherit;font-size:13px;line-height:20px;cursor:pointer;transition:background-color .12s,border-color .12s}',
|
|
295
386
|
'.dshet-btn:hover:not(:disabled){background:var(--dshet-hover)}',
|
|
296
387
|
'.dshet-btn:focus-visible{outline:2px solid var(--dsw-alias-button-primary-fill,var(--dshet-accent));outline-offset:2px}',
|
|
297
388
|
'.dshet-btn:disabled{cursor:default;opacity:.5}',
|
|
389
|
+
'.dshet-btn-save{width:28px;height:28px;padding:0;border:0;border-radius:50%;background:transparent;color:#81858c}',
|
|
390
|
+
'.dshet-btn-save:hover:not(:disabled){background:var(--dshet-hover)}',
|
|
391
|
+
'.dshet-btn-save svg{width:15px;height:15px;display:block;stroke:currentColor;fill:none;stroke-width:1.2;stroke-linecap:round;stroke-linejoin:round}',
|
|
392
|
+
'.dshet-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}',
|
|
298
393
|
'.dshet-btn-primary{border-color:var(--dsw-alias-button-primary-fill,var(--dshet-accent));background:var(--dsw-alias-button-primary-fill,var(--dshet-accent));color:var(--dsw-alias-label-primary-foreground,var(--dshet-on-accent))}',
|
|
299
394
|
'.dshet-btn-primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-fill,var(--dshet-accent));filter:brightness(1.08)}',
|
|
300
|
-
|
|
395
|
+
'.dshet-btn.dshet-btn-save{width:28px;height:28px;padding:0;border:0;border-radius:50%;background:transparent;color:#81858c;filter:none}',
|
|
396
|
+
'.dshet-btn.dshet-btn-save:hover:not(:disabled){background:var(--dshet-hover);filter:none}',
|
|
397
|
+
'.dshet-editor .dshet-footer .dshet-btn.dshet-btn-icon{width:28px;height:28px;padding:0;border:0;border-radius:50%;background:transparent;color:#81858c;filter:none}',
|
|
398
|
+
'.dshet-editor .dshet-footer .dshet-btn.dshet-btn-icon:hover:not(:disabled){background:var(--dshet-hover);color:#81858c;filter:none}',
|
|
399
|
+
'.dshet-btn-icon svg{width:15px;height:15px;display:block;fill:none;stroke:currentColor;stroke-width:1.2;stroke-linecap:round;stroke-linejoin:round}',
|
|
400
|
+
'.dshet-editor[data-dshet-inplace="1"] .dshet-btn.dshet-btn-save{width:28px;height:28px;padding:0;border:0;border-radius:50%;background:transparent;color:#81858c;filter:none}',
|
|
401
|
+
'.dshet-editor[data-dshet-inplace="1"] .dshet-btn.dshet-btn-save:hover:not(:disabled){background:var(--dshet-hover);filter:none}',
|
|
402
|
+
'.dshet-pick{display:flex;align-items:center;gap:4px;padding:0;font-size:12px;color:var(--dshet-ink-dim)}',
|
|
403
|
+
'.dshet-config{display:flex;flex-direction:column;gap:10px}',
|
|
404
|
+
'.dshet-config-row{display:flex;align-items:center;justify-content:space-between;gap:12px}',
|
|
405
|
+
'.dshet-config-hint{margin:0;color:var(--dsw-alias-label-tertiary,#8a8f98);font-size:13px;line-height:20px}',
|
|
301
406
|
'.dshet-select{padding:3px 8px;border:1px solid var(--dshet-line);border-radius:8px;background:var(--dshet-field);color:var(--dshet-ink);font:inherit;font-size:12px;line-height:18px}',
|
|
407
|
+
'.dshet-pick .dshet-select{height:28px;padding:0 6px;border:0;border-radius:8px;background:transparent;line-height:1;cursor:pointer;outline:none}',
|
|
408
|
+
'.dshet-pick .dshet-select:hover{background:var(--dshet-hover)}',
|
|
302
409
|
'[data-dshet-patch]{white-space:pre-wrap}',
|
|
303
410
|
'.dshet-patch-note{margin:6px 0;padding:6px 10px;border:1px dashed var(--dshet-line);border-radius:10px;background:var(--dshet-panel);color:var(--dshet-ink-dim);font-size:12px;line-height:18px}',
|
|
304
411
|
'.dshet-patch-note summary{cursor:pointer}',
|
|
@@ -364,6 +471,9 @@ window.__ModuleLoader__.load({
|
|
|
364
471
|
this.editorElement = null
|
|
365
472
|
this.editorBox = null
|
|
366
473
|
this.editorRow = null
|
|
474
|
+
this.editorAnchor = null
|
|
475
|
+
this.editorMode = 'reply'
|
|
476
|
+
this.editorStyle = 'frame'
|
|
367
477
|
this.editorFocus = false
|
|
368
478
|
this.editorDraft = ''
|
|
369
479
|
// The block list being edited: what the carrier will be rebuilt from.
|
|
@@ -505,6 +615,12 @@ window.__ModuleLoader__.load({
|
|
|
505
615
|
// replies are indexed by it too.
|
|
506
616
|
if (typeof reply.messageId === 'string' && reply.messageId !== '') repliesByMessage.set(reply.messageId, reply)
|
|
507
617
|
}
|
|
618
|
+
let preferredStyle = data.config && (data.config.editorStyle === 'bubble' || data.config.editorStyle === 'center')
|
|
619
|
+
? data.config.editorStyle
|
|
620
|
+
: 'frame'
|
|
621
|
+
const savedStyle = readStoredEditorStyle()
|
|
622
|
+
if (savedStyle !== null) preferredStyle = savedStyle
|
|
623
|
+
this.editorStyle = preferredStyle
|
|
508
624
|
this.publish({
|
|
509
625
|
hidden,
|
|
510
626
|
revisions,
|
|
@@ -514,6 +630,9 @@ window.__ModuleLoader__.load({
|
|
|
514
630
|
patches,
|
|
515
631
|
patchBySeq,
|
|
516
632
|
replyMode: data.mode === 'patch' ? 'patch' : 'replay',
|
|
633
|
+
editorStyle: data.config && (data.config.editorStyle === 'bubble' || data.config.editorStyle === 'center')
|
|
634
|
+
? data.config.editorStyle
|
|
635
|
+
: 'frame',
|
|
517
636
|
surfaceReady: true,
|
|
518
637
|
loaded: true,
|
|
519
638
|
loadError: false,
|
|
@@ -552,7 +671,11 @@ window.__ModuleLoader__.load({
|
|
|
552
671
|
const current = same && typeof this.editorBlockIndex === 'number'
|
|
553
672
|
? replyBlocks.find((block) => block.index === this.editorBlockIndex)
|
|
554
673
|
: undefined
|
|
555
|
-
|
|
674
|
+
// A fresh open always lands on the answer text: a turn's block list is
|
|
675
|
+
// mostly reasoning now, and opening on a chain-of-thought is not what
|
|
676
|
+
// the pencil means. The picker is how the user goes to a thought.
|
|
677
|
+
const answerBlock = [...replyBlocks].reverse().find((block) => block.kind === 'text')
|
|
678
|
+
const chosen = (same ? current : undefined) ?? answerBlock ?? requested ?? replyBlocks[0]
|
|
556
679
|
const normalized = replyBlocks.length > 0
|
|
557
680
|
? {
|
|
558
681
|
...target,
|
|
@@ -563,9 +686,16 @@ window.__ModuleLoader__.load({
|
|
|
563
686
|
}
|
|
564
687
|
: target
|
|
565
688
|
this.editor = normalized
|
|
689
|
+
this.editorMode = normalized.mode === 'prompt' ? 'prompt' : 'reply'
|
|
690
|
+
this.editorAnchor = target.anchor !== undefined && target.anchor !== null && target.anchor.isConnected === true
|
|
691
|
+
? target.anchor
|
|
692
|
+
: null
|
|
566
693
|
this.editorBlocks = replyBlocks
|
|
567
694
|
this.editorBlockIndex = chosen === undefined ? null : chosen.index
|
|
568
695
|
this.editorBlockKind = chosen === undefined ? null : chosen.kind
|
|
696
|
+
this.editorTargetSeq = chosen !== undefined && typeof chosen.seq === 'number' ? chosen.seq : null
|
|
697
|
+
this.editorAll = false
|
|
698
|
+
this.editorAllDrafts = new Map()
|
|
569
699
|
if (!same) {
|
|
570
700
|
this.editorDraft = chosen === undefined ? target.text : chosen.text
|
|
571
701
|
this.editorParts = blocksToParts(normalized, this.sessionId)
|
|
@@ -592,6 +722,8 @@ window.__ModuleLoader__.load({
|
|
|
592
722
|
blocks: replyBlocks,
|
|
593
723
|
blockIndex: this.editorBlockIndex,
|
|
594
724
|
blockKind: this.editorBlockKind,
|
|
725
|
+
targetSeq: this.editorTargetSeq,
|
|
726
|
+
allBlocks: false,
|
|
595
727
|
confirming: false,
|
|
596
728
|
failure: null,
|
|
597
729
|
})
|
|
@@ -625,17 +757,31 @@ window.__ModuleLoader__.load({
|
|
|
625
757
|
// Pick another block of the same reply. The editor is rebuilt by the
|
|
626
758
|
// block index in its marker; the draft becomes that block's current
|
|
627
759
|
// (already projected) text.
|
|
628
|
-
selectBlock(
|
|
760
|
+
selectBlock(value) {
|
|
629
761
|
if (this.view.pending) return
|
|
630
762
|
const target = this.editor
|
|
631
763
|
if (target === null || !Array.isArray(target.blocks)) return
|
|
632
|
-
const
|
|
764
|
+
const raw = String(value)
|
|
765
|
+
if (raw === 'all') {
|
|
766
|
+
this.editorAll = true
|
|
767
|
+
this.editorAllDrafts = new Map(target.blocks.map((block) => [`${typeof block.seq === 'number' ? block.seq : ''}:${block.index}`, block.text]))
|
|
768
|
+
this.editorElement = null
|
|
769
|
+
this.publish({ allBlocks: true, blockIndex: null, blockKind: null, targetSeq: null, failure: null })
|
|
770
|
+
return
|
|
771
|
+
}
|
|
772
|
+
this.editorAll = false
|
|
773
|
+
const split = raw.includes(':') ? raw.split(':') : ['', raw]
|
|
774
|
+
const seq = split[0] === '' ? null : Number(split[0])
|
|
775
|
+
const index = Number(split[1])
|
|
776
|
+
const block = target.blocks.find((entry) =>
|
|
777
|
+
entry.index === index && (seq === null || entry.seq === undefined || entry.seq === seq))
|
|
633
778
|
if (block === undefined || block.kind === undefined) return
|
|
634
779
|
this.editorBlockIndex = block.index
|
|
635
780
|
this.editorBlockKind = block.kind
|
|
781
|
+
this.editorTargetSeq = typeof block.seq === 'number' ? block.seq : this.editorTargetSeq
|
|
636
782
|
this.editorDraft = block.text
|
|
637
783
|
this.editorElement = null
|
|
638
|
-
this.publish({ draft: block.text, blockIndex: block.index, blockKind: block.kind, failure: null })
|
|
784
|
+
this.publish({ draft: block.text, blockIndex: block.index, blockKind: block.kind, targetSeq: this.editorTargetSeq, failure: null })
|
|
639
785
|
}
|
|
640
786
|
|
|
641
787
|
close() {
|
|
@@ -644,6 +790,8 @@ window.__ModuleLoader__.load({
|
|
|
644
790
|
this.editorElement = null
|
|
645
791
|
this.editorBox = null
|
|
646
792
|
this.editorRow = null
|
|
793
|
+
this.editorAnchor = null
|
|
794
|
+
this.editorMode = 'reply'
|
|
647
795
|
this.editorFocus = false
|
|
648
796
|
this.editor = null
|
|
649
797
|
this.editorDraft = ''
|
|
@@ -653,6 +801,9 @@ window.__ModuleLoader__.load({
|
|
|
653
801
|
this.editorBlocks = []
|
|
654
802
|
this.editorBlockIndex = null
|
|
655
803
|
this.editorBlockKind = null
|
|
804
|
+
this.editorTargetSeq = null
|
|
805
|
+
this.editorAll = false
|
|
806
|
+
this.editorAllDrafts = new Map()
|
|
656
807
|
this.publish({ editing: null, draft: '', parts: [], blocks: [], blockIndex: null, blockKind: null, confirming: false, failure: null })
|
|
657
808
|
}
|
|
658
809
|
|
|
@@ -660,6 +811,13 @@ window.__ModuleLoader__.load({
|
|
|
660
811
|
this.editorDraft = value
|
|
661
812
|
}
|
|
662
813
|
|
|
814
|
+
setEditorStyle(style) {
|
|
815
|
+
if (!EDITOR_STYLES.includes(style)) return
|
|
816
|
+
setEditorStylePreference(style)
|
|
817
|
+
this.editorStyle = style
|
|
818
|
+
this.publish({ editorStyle: style })
|
|
819
|
+
}
|
|
820
|
+
|
|
663
821
|
/**
|
|
664
822
|
* One chip the user removed leaves the block list, and nothing else.
|
|
665
823
|
*
|
|
@@ -725,6 +883,56 @@ window.__ModuleLoader__.load({
|
|
|
725
883
|
}
|
|
726
884
|
}
|
|
727
885
|
|
|
886
|
+
/** The all-blocks save: one request carrying every edited block. */
|
|
887
|
+
async confirmAll(target) {
|
|
888
|
+
const blocks = Array.isArray(target.blocks) ? target.blocks : []
|
|
889
|
+
const drafts = this.editorAllDrafts instanceof Map ? this.editorAllDrafts : new Map()
|
|
890
|
+
const targets = []
|
|
891
|
+
for (const block of blocks) {
|
|
892
|
+
const key = `${typeof block.seq === 'number' ? block.seq : ''}:${block.index}`
|
|
893
|
+
const text = drafts.get(key)
|
|
894
|
+
if (typeof text !== 'string' || text.trim() === '') {
|
|
895
|
+
this.publish({ failure: 'empty' })
|
|
896
|
+
return
|
|
897
|
+
}
|
|
898
|
+
if (text !== block.text) {
|
|
899
|
+
targets.push({ seq: typeof block.seq === 'number' ? block.seq : target.seq, blockIndex: block.index, kind: block.kind, text })
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
if (targets.length === 0) {
|
|
903
|
+
this.publish({ failure: null })
|
|
904
|
+
this.editor = null
|
|
905
|
+
this.publish({ editing: null })
|
|
906
|
+
return
|
|
907
|
+
}
|
|
908
|
+
this.publish({ pending: true, failure: null })
|
|
909
|
+
try {
|
|
910
|
+
const res = await fetch(`${ROUTE_PREFIX}/apply`, {
|
|
911
|
+
method: 'POST',
|
|
912
|
+
headers: { 'content-type': 'application/json' },
|
|
913
|
+
body: JSON.stringify({ sessionId: this.sessionId, targets }),
|
|
914
|
+
})
|
|
915
|
+
const data = await res.json().catch(() => ({}))
|
|
916
|
+
if (!res.ok || !data.ok) {
|
|
917
|
+
this.publish({ pending: false, failure: data && data.code ? String(data.code) : 'generic' })
|
|
918
|
+
return
|
|
919
|
+
}
|
|
920
|
+
this.editor = null
|
|
921
|
+
this.editorDraft = ''
|
|
922
|
+
this.editorAll = false
|
|
923
|
+
this.editorAllDrafts = new Map()
|
|
924
|
+
this.editorElement = null
|
|
925
|
+
this.editorBox = null
|
|
926
|
+
this.editorRow = null
|
|
927
|
+
this.editorFocus = false
|
|
928
|
+
this.confirming = false
|
|
929
|
+
this.publish({ pending: false, editing: null, confirming: false, failure: null })
|
|
930
|
+
this.load()
|
|
931
|
+
} catch {
|
|
932
|
+
this.publish({ pending: false, failure: 'generic' })
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
728
936
|
review() {
|
|
729
937
|
this.pendingRerun = false
|
|
730
938
|
this.runReview()
|
|
@@ -760,6 +968,9 @@ window.__ModuleLoader__.load({
|
|
|
760
968
|
if (target === null || this.view.pending) return
|
|
761
969
|
const rerunIntent = this.pendingRerun
|
|
762
970
|
this.pendingRerun = false
|
|
971
|
+
if (this.editorAll === true) {
|
|
972
|
+
return this.confirmAll(target)
|
|
973
|
+
}
|
|
763
974
|
const text = this.editorDraft
|
|
764
975
|
if (text.trim() === '') {
|
|
765
976
|
this.publish({ failure: 'empty' })
|
|
@@ -786,7 +997,11 @@ window.__ModuleLoader__.load({
|
|
|
786
997
|
// The in-place reply path names one block; the host validates that
|
|
787
998
|
// it is still that kind at that index before writing the record.
|
|
788
999
|
...(target.patch === true && typeof this.editorBlockIndex === 'number'
|
|
789
|
-
? {
|
|
1000
|
+
? {
|
|
1001
|
+
blockIndex: this.editorBlockIndex,
|
|
1002
|
+
kind: this.editorBlockKind,
|
|
1003
|
+
...(this.editorTargetSeq === null ? {} : { targetSeq: this.editorTargetSeq }),
|
|
1004
|
+
}
|
|
790
1005
|
: {}),
|
|
791
1006
|
}),
|
|
792
1007
|
})
|
|
@@ -1197,9 +1412,9 @@ window.__ModuleLoader__.load({
|
|
|
1197
1412
|
* at all and reads exactly like a dead pencil.
|
|
1198
1413
|
*/
|
|
1199
1414
|
function wireAction(button) {
|
|
1200
|
-
pressable(button, () => {
|
|
1415
|
+
pressable(button, (event) => {
|
|
1201
1416
|
const activate = button[ACTIVATE]
|
|
1202
|
-
if (typeof activate === 'function') activate()
|
|
1417
|
+
if (typeof activate === 'function') activate(event)
|
|
1203
1418
|
})
|
|
1204
1419
|
}
|
|
1205
1420
|
|
|
@@ -1261,7 +1476,8 @@ window.__ModuleLoader__.load({
|
|
|
1261
1476
|
}
|
|
1262
1477
|
const entry = view.editable.get(headRevision(view.revisions.get(revisionSeq), view.revisions))
|
|
1263
1478
|
const text = entry && typeof entry.text === 'string' ? entry.text : ''
|
|
1264
|
-
|
|
1479
|
+
const extraBlocks = entry && Array.isArray(entry.blocks) ? entry.blocks.filter((block) => block.type !== 'text') : []
|
|
1480
|
+
if (text === '' && extraBlocks.length === 0) {
|
|
1265
1481
|
if (existing !== null) existing.remove()
|
|
1266
1482
|
releaseRowCopy(row)
|
|
1267
1483
|
return node.kind !== 'user'
|
|
@@ -1274,7 +1490,22 @@ window.__ModuleLoader__.load({
|
|
|
1274
1490
|
}
|
|
1275
1491
|
}
|
|
1276
1492
|
placeRevisionBubble(row, bubble)
|
|
1277
|
-
|
|
1493
|
+
// A rewritten prompt may still carry images/files. The carrier keeps
|
|
1494
|
+
// those blocks, the model reads them, and the editor lists them - the
|
|
1495
|
+
// standing-in bubble must show them too, or an edited image message looks
|
|
1496
|
+
// like the images were deleted.
|
|
1497
|
+
const signature = text + '|' + extraBlocks.map((block) =>
|
|
1498
|
+
`${String(block.index)}:${String(block.type)}:${String(block.name ?? '')}:${String(block.bytes ?? '')}`).join(',')
|
|
1499
|
+
if (bubble.dataset.dshetRevisionContent !== signature) {
|
|
1500
|
+
bubble.textContent = ''
|
|
1501
|
+
const textNode = document.createElement('span')
|
|
1502
|
+
textNode.className = 'dshet-revision-text'
|
|
1503
|
+
textNode.textContent = text
|
|
1504
|
+
bubble.appendChild(textNode)
|
|
1505
|
+
const files = revisionAttachments(entry, controller)
|
|
1506
|
+
if (files !== null) bubble.appendChild(files)
|
|
1507
|
+
bubble.dataset.dshetRevisionContent = signature
|
|
1508
|
+
}
|
|
1278
1509
|
// The edit entry goes where every other action of this row lives: the
|
|
1279
1510
|
// platform's own bar, which the collapse keeps standing, at its right end
|
|
1280
1511
|
// behind the clock and the copy button - the row the user reads as "this
|
|
@@ -1307,6 +1538,40 @@ window.__ModuleLoader__.load({
|
|
|
1307
1538
|
return true
|
|
1308
1539
|
}
|
|
1309
1540
|
|
|
1541
|
+
/**
|
|
1542
|
+
* The non-text blocks of a rewritten prompt, drawn under its text.
|
|
1543
|
+
*
|
|
1544
|
+
* These are the same blocks the editor lists as chips; the bytes come from
|
|
1545
|
+
* this plugin's attachment route, so a thumbnail works without a host API.
|
|
1546
|
+
*/
|
|
1547
|
+
function revisionAttachments(entry, controller) {
|
|
1548
|
+
if (!entry || !Array.isArray(entry.blocks)) return null
|
|
1549
|
+
const blocks = entry.blocks.filter((block) => block.type !== 'text')
|
|
1550
|
+
if (blocks.length === 0) return null
|
|
1551
|
+
const strip = document.createElement('div')
|
|
1552
|
+
strip.className = 'dshet-revision-files'
|
|
1553
|
+
for (const block of blocks) {
|
|
1554
|
+
const item = document.createElement('div')
|
|
1555
|
+
item.className = 'dshet-revision-file'
|
|
1556
|
+
if (block.preview === true && typeof block.index === 'number') {
|
|
1557
|
+
const image = document.createElement('img')
|
|
1558
|
+
image.className = 'dshet-thumb'
|
|
1559
|
+
image.alt = ''
|
|
1560
|
+
image.src = previewUrl(controller.sessionId, entry.seq, block.index)
|
|
1561
|
+
image.addEventListener('error', () => {
|
|
1562
|
+
if (typeof image.remove === 'function') image.remove()
|
|
1563
|
+
})
|
|
1564
|
+
item.appendChild(image)
|
|
1565
|
+
}
|
|
1566
|
+
const label = document.createElement('span')
|
|
1567
|
+
label.className = 'dshet-chip-label'
|
|
1568
|
+
label.textContent = typeof block.name === 'string' && block.name !== '' ? block.name : block.type
|
|
1569
|
+
item.appendChild(label)
|
|
1570
|
+
strip.appendChild(item)
|
|
1571
|
+
}
|
|
1572
|
+
return strip
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1310
1575
|
// A copy of a live user bubble element, classes and all.
|
|
1311
1576
|
// The key of the row a bubble stands in for. It is what makes a stray
|
|
1312
1577
|
// bubble recognisable after the host's DOM has shifted around it: an
|
|
@@ -1815,7 +2080,14 @@ window.__ModuleLoader__.load({
|
|
|
1815
2080
|
} else if (button.dataset !== undefined && button.dataset.dshetFallback !== undefined) {
|
|
1816
2081
|
delete button.dataset.dshetFallback
|
|
1817
2082
|
}
|
|
1818
|
-
button[ACTIVATE] = () => controller.open(
|
|
2083
|
+
button[ACTIVATE] = (event) => controller.open({
|
|
2084
|
+
...entry.target,
|
|
2085
|
+
// The editor belongs beside the pencil the user just pressed, not at the
|
|
2086
|
+
// top of a row that may be taller than the screen.
|
|
2087
|
+
anchor: event !== undefined && event !== null && event.currentTarget !== undefined
|
|
2088
|
+
? event.currentTarget
|
|
2089
|
+
: button,
|
|
2090
|
+
})
|
|
1819
2091
|
liveHosts.add(host)
|
|
1820
2092
|
if (button.getAttribute('aria-label') !== label) {
|
|
1821
2093
|
button.setAttribute('aria-label', label)
|
|
@@ -1945,6 +2217,30 @@ window.__ModuleLoader__.load({
|
|
|
1945
2217
|
// Put the box under the row it belongs to: same width, just below its lower
|
|
1946
2218
|
// edge. Called on render and whenever the page scrolls or resizes, since
|
|
1947
2219
|
// the layer is fixed to the viewport.
|
|
2220
|
+
// Some host skins mark their editor chrome `!important`; only an inline
|
|
2221
|
+
// `!important` outranks that. The stub DOM has no `setProperty`, so it falls
|
|
2222
|
+
// back to a plain assignment there.
|
|
2223
|
+
function forceStyle(element, property, value) {
|
|
2224
|
+
if (element === null || element === undefined || element.style === undefined) return
|
|
2225
|
+
if (typeof element.style.setProperty === 'function') {
|
|
2226
|
+
element.style.setProperty(property, value, 'important')
|
|
2227
|
+
return
|
|
2228
|
+
}
|
|
2229
|
+
element.style[property] = value
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
function pageBackground() {
|
|
2233
|
+
try {
|
|
2234
|
+
if (typeof getComputedStyle === 'function' && typeof document !== 'undefined') {
|
|
2235
|
+
const value = getComputedStyle(document.body).backgroundColor
|
|
2236
|
+
if (value && value !== 'transparent' && !/rgba?\(0, 0, 0, 0\)/.test(value)) return value
|
|
2237
|
+
}
|
|
2238
|
+
} catch {
|
|
2239
|
+
/* fall through */
|
|
2240
|
+
}
|
|
2241
|
+
return '#ffffff'
|
|
2242
|
+
}
|
|
2243
|
+
|
|
1948
2244
|
function viewportSize() {
|
|
1949
2245
|
const win = typeof window === 'undefined' ? undefined : window
|
|
1950
2246
|
const doc = typeof document === 'undefined' ? undefined : document
|
|
@@ -1961,35 +2257,429 @@ window.__ModuleLoader__.load({
|
|
|
1961
2257
|
// past the bottom edge where nothing could reach it. Flip above the row when
|
|
1962
2258
|
// there is room, otherwise pin the box to the bottom of the viewport and let
|
|
1963
2259
|
// its own scrollbar carry the content; the footer stays sticky either way.
|
|
1964
|
-
|
|
2260
|
+
/**
|
|
2261
|
+
* The bubble the editor stands in for.
|
|
2262
|
+
*
|
|
2263
|
+
* A prompt is a right-aligned input bubble; a reply is the output content
|
|
2264
|
+
* block. The editor takes that element's width and left edge and overlays its
|
|
2265
|
+
* position, so it reads as the message being edited instead of a floating
|
|
2266
|
+
* panel. Class hashes change between builds, so the suffixes are matched.
|
|
2267
|
+
*/
|
|
2268
|
+
function bubbleFor(row, mode, blockKind = null) {
|
|
2269
|
+
let selectors
|
|
2270
|
+
if (mode === 'reply' && blockKind === 'reasoning') {
|
|
2271
|
+
selectors = ['details', '[class*="_reasoning"]', '[class*="_thinking"]', '[class*="reasoning"]']
|
|
2272
|
+
} else if (mode === 'reply') {
|
|
2273
|
+
selectors = ['[class*="_markdown"]', '[class*="_bubble"]', '[class*="messageBody"]']
|
|
2274
|
+
} else {
|
|
2275
|
+
selectors = ['[class*="_bubble"]', '[class*="userStack"]', '[class*="_markdown"]']
|
|
2276
|
+
}
|
|
2277
|
+
for (const selector of selectors) {
|
|
2278
|
+
let nodes = []
|
|
2279
|
+
try {
|
|
2280
|
+
nodes = [...row.querySelectorAll(selector)]
|
|
2281
|
+
} catch {
|
|
2282
|
+
nodes = []
|
|
2283
|
+
}
|
|
2284
|
+
let best = null
|
|
2285
|
+
for (const element of nodes) {
|
|
2286
|
+
if (typeof element.closest === 'function' && element.closest('.dshet-editor') !== null) continue
|
|
2287
|
+
const rect = element.getBoundingClientRect()
|
|
2288
|
+
if (rect.width < 80 || rect.height <= 0) continue
|
|
2289
|
+
if (best === null || rect.width * rect.height > best.rect.width * best.rect.height) best = { element, rect }
|
|
2290
|
+
}
|
|
2291
|
+
if (best !== null) return best
|
|
2292
|
+
}
|
|
2293
|
+
const rect = row.getBoundingClientRect()
|
|
2294
|
+
return { element: row, rect }
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
/**
|
|
2298
|
+
* Which row an editor should attach to when several cover the edited seq.
|
|
2299
|
+
*
|
|
2300
|
+
* A reply's seq appears in both the assistant-step row and the turn-tail
|
|
2301
|
+
* row that carries its action strip; the tail also covers the reply, so a
|
|
2302
|
+
* last-write-wins loop handed the editor the tail's little action bar and
|
|
2303
|
+
* the box came out message-sized instead of bubble-sized. The message row
|
|
2304
|
+
* always wins.
|
|
2305
|
+
*/
|
|
2306
|
+
function editorRowRank(row, mode) {
|
|
2307
|
+
const kind = row.getAttribute('data-chat-flow-kind')
|
|
2308
|
+
if (mode === 'prompt') {
|
|
2309
|
+
if (kind === 'user') return 4
|
|
2310
|
+
if (kind === 'revision' || kind === 'user-revision') return 3
|
|
2311
|
+
if (kind === 'turn-tail' || kind === 'turn-process') return 1
|
|
2312
|
+
return 2
|
|
2313
|
+
}
|
|
2314
|
+
if (kind === 'assistant-step') return 4
|
|
2315
|
+
if (kind === 'turn-tail' || kind === 'turn-process') return 1
|
|
2316
|
+
return 2
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
/**
|
|
2320
|
+
* The vertical band the conversation owns: below its top, above the composer.
|
|
2321
|
+
*
|
|
2322
|
+
* This runs on every scroll frame, so the lookups are cached briefly: the
|
|
2323
|
+
* composer element and the conversation top do not move between frames, and
|
|
2324
|
+
* a global `[class*=...]` scan per frame is what made scrolling feel heavy.
|
|
2325
|
+
*/
|
|
2326
|
+
let conversationBoundsCache = null
|
|
2327
|
+
let conversationBoundsAt = 0
|
|
2328
|
+
let composerElementCache = null
|
|
2329
|
+
function conversationBounds(row = null) {
|
|
2330
|
+
const now = Date.now()
|
|
2331
|
+
if (conversationBoundsCache !== null && now - conversationBoundsAt < 250) return conversationBoundsCache
|
|
2332
|
+
const viewportHeight = typeof window !== 'undefined' && typeof window.innerHeight === 'number' ? window.innerHeight : 0
|
|
2333
|
+
const bounds = { top: 0, bottom: viewportHeight, composerLeft: 0, composerWidth: 0 }
|
|
2334
|
+
try {
|
|
2335
|
+
const conversation = document.querySelector('[data-slot="main.conversation"]')
|
|
2336
|
+
if (conversation !== null && typeof conversation.getBoundingClientRect === 'function') {
|
|
2337
|
+
bounds.top = conversation.getBoundingClientRect().top
|
|
2338
|
+
}
|
|
2339
|
+
} catch {}
|
|
2340
|
+
// The real scroll area is the nearest scrollable ancestor of the edited
|
|
2341
|
+
// row; the slot wrapper can start above the header, which let the box
|
|
2342
|
+
// cover the tab bar.
|
|
2343
|
+
try {
|
|
2344
|
+
let node = row !== null && row !== undefined ? row.parentElement : null
|
|
2345
|
+
while (node !== null && node !== undefined && node !== document.body) {
|
|
2346
|
+
const style = getComputedStyle(node)
|
|
2347
|
+
if ((style.overflowY === 'auto' || style.overflowY === 'scroll') && node.getBoundingClientRect().height > 120) {
|
|
2348
|
+
bounds.top = Math.max(bounds.top, node.getBoundingClientRect().top)
|
|
2349
|
+
break
|
|
2350
|
+
}
|
|
2351
|
+
node = node.parentElement
|
|
2352
|
+
}
|
|
2353
|
+
} catch {}
|
|
2354
|
+
// The composer stack has several wrappers with "composer" in the class;
|
|
2355
|
+
// the boundary is the LOWEST one that still sits above the viewport edge,
|
|
2356
|
+
// otherwise the box runs under the input box.
|
|
2357
|
+
try {
|
|
2358
|
+
// The composer CARD is the rounded surface that owns the input, not the
|
|
2359
|
+
// outer seat (which is wider and made the editor overhang). Walk up from
|
|
2360
|
+
// the input element to the first rounded, painted ancestor.
|
|
2361
|
+
let card = document.querySelector('textarea[placeholder], [contenteditable="true"]')
|
|
2362
|
+
while (card !== null && card !== undefined && card !== document.body) {
|
|
2363
|
+
const style = getComputedStyle(card)
|
|
2364
|
+
const radius = Number.parseFloat(style.borderTopLeftRadius)
|
|
2365
|
+
if (radius > 6 && style.backgroundColor !== 'rgba(0, 0, 0, 0)') break
|
|
2366
|
+
card = card.parentElement
|
|
2367
|
+
}
|
|
2368
|
+
let lowest = 0
|
|
2369
|
+
let lowestRect = null
|
|
2370
|
+
if (card !== null && card !== undefined && card !== document.body && typeof card.getBoundingClientRect === 'function') {
|
|
2371
|
+
const rect = card.getBoundingClientRect()
|
|
2372
|
+
if (rect.top > 0 && (viewportHeight === 0 || rect.top < viewportHeight)) {
|
|
2373
|
+
lowest = rect.top
|
|
2374
|
+
lowestRect = rect
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
if (lowestRect === null) {
|
|
2378
|
+
const candidates = document.querySelectorAll('[data-slot="conversation.input"], [class*="composerSeat"], [class*="composer"]')
|
|
2379
|
+
for (const candidate of candidates) {
|
|
2380
|
+
if (typeof candidate.getBoundingClientRect !== 'function') continue
|
|
2381
|
+
const rect = candidate.getBoundingClientRect()
|
|
2382
|
+
if (rect.top > lowest && (viewportHeight === 0 || rect.top < viewportHeight - 40)) {
|
|
2383
|
+
lowest = rect.top
|
|
2384
|
+
lowestRect = rect
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
bounds.bottom = lowest > 0 ? lowest : Math.min(viewportHeight, viewportHeight - 120)
|
|
2389
|
+
if (lowestRect !== null) {
|
|
2390
|
+
bounds.composerLeft = lowestRect.left
|
|
2391
|
+
bounds.composerWidth = lowestRect.width
|
|
2392
|
+
}
|
|
2393
|
+
} catch {
|
|
2394
|
+
bounds.bottom = viewportHeight > 0 ? viewportHeight - 120 : viewportHeight
|
|
2395
|
+
}
|
|
2396
|
+
conversationBoundsCache = bounds
|
|
2397
|
+
conversationBoundsAt = now
|
|
2398
|
+
return bounds
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
function placeEditor(row, box, mode = 'reply', style = 'frame', blockKind = null, blocks = null) {
|
|
1965
2402
|
const layer = ensureEditorLayer()
|
|
1966
2403
|
if (box.parentElement !== layer) layer.appendChild(box)
|
|
1967
|
-
const
|
|
2404
|
+
const placement = readStoredEditorPlacement() ?? 'overlay'
|
|
2405
|
+
const legacy = placement === 'legacy'
|
|
2406
|
+
const inline = placement === 'inline'
|
|
2407
|
+
// Every fixed mode is clamped to the conversation band by default; the
|
|
2408
|
+
// in-page mode keeps its page coordinates and page scroll.
|
|
2409
|
+
const clamped = !inline
|
|
2410
|
+
if (legacy) style = 'frame'
|
|
2411
|
+
if (inline) layer.dataset.dshetInline = '1'
|
|
2412
|
+
else delete layer.dataset.dshetInline
|
|
2413
|
+
const rowRect = row.getBoundingClientRect()
|
|
2414
|
+
const bubble = bubbleFor(row, mode, blockKind)
|
|
2415
|
+
const storedStyle = readStoredEditorStyle()
|
|
2416
|
+
if (storedStyle !== null) style = storedStyle
|
|
2417
|
+
const chipsInBox = typeof box.querySelector === 'function' ? box.querySelector('.dshet-chips') : null
|
|
2418
|
+
const minimumHeight = chipsInBox !== null && chipsInBox !== undefined ? 190 : 150
|
|
1968
2419
|
const viewport = viewportSize()
|
|
1969
2420
|
const margin = 8
|
|
1970
|
-
const
|
|
1971
|
-
|
|
1972
|
-
|
|
2421
|
+
const area = typeof box.querySelector === 'function' ? box.querySelector('textarea') : null
|
|
2422
|
+
const frame = typeof box.querySelector === 'function' ? box.querySelector('.dshet-frame') : null
|
|
2423
|
+
const allBox = typeof box.querySelector === 'function' ? box.querySelector('.dshet-all') : null
|
|
2424
|
+
const heightMode = readStoredEditorHeight() ?? 'fill'
|
|
2425
|
+
if (frame !== null && frame !== undefined) frame.style.display = style === 'frame' ? '' : 'none'
|
|
2426
|
+
box.style.boxShadow = 'none'
|
|
2427
|
+
const footerEl = typeof box.querySelector === 'function' ? box.querySelector('.dshet-footer') : null
|
|
2428
|
+
if (footerEl !== null && footerEl !== undefined && !(style === 'center' && !inline)) {
|
|
2429
|
+
footerEl.style.position = ''
|
|
2430
|
+
footerEl.style.height = ''
|
|
2431
|
+
footerEl.style.marginTop = ''
|
|
2432
|
+
footerEl.style.borderRadius = ''
|
|
2433
|
+
}
|
|
2434
|
+
if (style === 'center' && !inline) {
|
|
2435
|
+
// A large centred panel, the first shape that shipped. The textarea
|
|
2436
|
+
// grows with its content; the original message stays where it is.
|
|
2437
|
+
delete box.dataset.dshetInplace
|
|
2438
|
+
if (area !== null && area !== undefined) {
|
|
2439
|
+
area.style.position = ''
|
|
2440
|
+
area.style.left = ''
|
|
2441
|
+
area.style.top = ''
|
|
2442
|
+
area.style.width = ''
|
|
2443
|
+
area.style.height = ''
|
|
2444
|
+
area.style.maxHeight = ''
|
|
2445
|
+
area.style.background = ''
|
|
2446
|
+
area.style.paddingBottom = '16px'
|
|
2447
|
+
fitEditorHeight(area)
|
|
2448
|
+
const centerFooter = typeof box.querySelector === 'function' ? box.querySelector('.dshet-footer') : null
|
|
2449
|
+
if (centerFooter !== null && centerFooter !== undefined) {
|
|
2450
|
+
centerFooter.style.position = 'static'
|
|
2451
|
+
centerFooter.style.height = '46px'
|
|
2452
|
+
centerFooter.style.marginTop = '0'
|
|
2453
|
+
centerFooter.style.borderRadius = '0 0 16px 16px'
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
const width = viewport.width > 0 ? Math.max(320, Math.min(1400, viewport.width - margin * 2)) : Math.max(320, rowRect.width)
|
|
2457
|
+
box.style.boxSizing = 'border-box'
|
|
2458
|
+
box.style.width = `${Math.round(width)}px`
|
|
2459
|
+
box.style.left = `${Math.round(viewport.width > 0 ? (viewport.width - width) / 2 : rowRect.left)}px`
|
|
2460
|
+
const bounds = conversationBounds(row)
|
|
2461
|
+
const topLimit = bounds.top + margin
|
|
2462
|
+
const bottomLimit = (bounds.bottom > 0 ? bounds.bottom : viewport.height) - margin
|
|
2463
|
+
const usable = bottomLimit > topLimit ? bottomLimit - topLimit : 0
|
|
2464
|
+
box.style.height = ''
|
|
2465
|
+
box.style.maxHeight = usable > 0
|
|
2466
|
+
? `${Math.max(180, usable)}px`
|
|
2467
|
+
: viewport.height > 0
|
|
2468
|
+
? `${Math.max(180, viewport.height - margin * 2)}px`
|
|
2469
|
+
: ''
|
|
2470
|
+
const measured = box.getBoundingClientRect()
|
|
2471
|
+
const height = (measured && measured.height) || box.offsetHeight || 0
|
|
2472
|
+
const top = usable > 0 && height > 0
|
|
2473
|
+
? Math.round(Math.max(topLimit, Math.min(topLimit + (usable - height) / 2, bottomLimit - height)))
|
|
2474
|
+
: viewport.height > 0 && height > 0
|
|
2475
|
+
? Math.round(Math.max(margin, Math.min((viewport.height - height) / 2, viewport.height - height - margin)))
|
|
2476
|
+
: Math.round((rowRect.bottom || 0) + 6)
|
|
2477
|
+
box.style.top = `${top}px`
|
|
2478
|
+
return
|
|
2479
|
+
}
|
|
2480
|
+
// `bubble` and `frame` both paint over the message, so the textarea gets
|
|
2481
|
+
// an opaque surface: the recorded text underneath must not show through
|
|
2482
|
+
// the draft. `frame` outlines the whole row; `bubble` outlines just the
|
|
2483
|
+
// message block.
|
|
2484
|
+
box.dataset.dshetInplace = '1'
|
|
2485
|
+
// A full opaque backdrop: the frame covers the whole row, so the host's
|
|
2486
|
+
// status line and action bar underneath must not show through either.
|
|
2487
|
+
const opaque = pageBackground()
|
|
2488
|
+
box.style.background = opaque
|
|
2489
|
+
if (area !== null && area !== undefined) {
|
|
2490
|
+
area.style.background = opaque
|
|
2491
|
+
}
|
|
2492
|
+
// A reply edit is scoped to the selected block (answer text or reasoning),
|
|
2493
|
+
// not to the whole assistant row: the other blocks stay out of the frame.
|
|
2494
|
+
const base = legacy ? rowRect : mode === 'reply' ? bubble.rect : style === 'frame' ? rowRect : bubble.rect
|
|
2495
|
+
let width = base.width
|
|
2496
|
+
let alignedLeft = null
|
|
2497
|
+
const fillAllView = allBox !== null && allBox !== undefined && heightMode !== 'content'
|
|
2498
|
+
if (fillAllView) {
|
|
2499
|
+
// The all-blocks page lines up with the composer card: same left edge,
|
|
2500
|
+
// same width, so the two boxes read as one column.
|
|
2501
|
+
const aligned = conversationBounds(row)
|
|
2502
|
+
if (aligned.composerWidth > 0) {
|
|
2503
|
+
width = aligned.composerWidth
|
|
2504
|
+
alignedLeft = aligned.composerLeft
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
if (!fillAllView && mode === 'reply' && typeof rowRect.width === 'number' && rowRect.width > width) {
|
|
2508
|
+
// A reply block is narrower than the conversation row; the editor gets
|
|
2509
|
+
// the full row width so the text is not squeezed into the bubble.
|
|
2510
|
+
width = rowRect.width
|
|
2511
|
+
}
|
|
2512
|
+
let left = alignedLeft === null ? base.left : alignedLeft
|
|
2513
|
+
if (inline) {
|
|
2514
|
+
const pageX = typeof window !== 'undefined' && typeof window.scrollX === 'number' ? window.scrollX : 0
|
|
2515
|
+
left += pageX
|
|
2516
|
+
if (viewport.width > 0) width = Math.min(width, viewport.width - margin * 2)
|
|
2517
|
+
} else if (viewport.width > 0) {
|
|
2518
|
+
width = Math.min(width, viewport.width - margin * 2)
|
|
2519
|
+
left = Math.max(margin, Math.min(left, viewport.width - width - margin))
|
|
2520
|
+
}
|
|
2521
|
+
box.style.boxSizing = 'border-box'
|
|
1973
2522
|
box.style.width = `${Math.round(width)}px`
|
|
1974
|
-
const measured = box.getBoundingClientRect()
|
|
1975
|
-
const height = (measured && measured.height) || box.offsetHeight || 0
|
|
1976
|
-
let top = rect.bottom + 6
|
|
1977
|
-
if (viewport.height > 0 && height > 0) {
|
|
1978
|
-
const maxTop = Math.max(margin, viewport.height - height - margin)
|
|
1979
|
-
if (top + height > viewport.height - margin) {
|
|
1980
|
-
const above = rect.top - height - 6
|
|
1981
|
-
top = above >= margin ? above : maxTop
|
|
1982
|
-
}
|
|
1983
|
-
// A row taller than the screen has its bottom above the viewport after
|
|
1984
|
-
// the user scrolls to its action bar; without this clamp the box follows
|
|
1985
|
-
// it off-screen and the buttons become unreachable again.
|
|
1986
|
-
top = Math.max(margin, Math.min(top, maxTop))
|
|
1987
|
-
}
|
|
1988
|
-
let left = rect.left
|
|
1989
|
-
if (viewport.width > 0 && left + width > viewport.width - margin) left = viewport.width - width - margin
|
|
1990
|
-
if (left < margin) left = margin
|
|
1991
2523
|
box.style.left = `${Math.round(left)}px`
|
|
2524
|
+
// The host's action strip sits under the reported bubble. Extend the
|
|
2525
|
+
// editor down to the row's bottom so that strip is covered, and the
|
|
2526
|
+
// footer controls end up where those host buttons used to be.
|
|
2527
|
+
const baseBottom = typeof base.bottom === 'number' ? base.bottom : base.top + base.height
|
|
2528
|
+
const rowBottom = typeof rowRect.bottom === 'number' ? rowRect.bottom : rowRect.top + rowRect.height
|
|
2529
|
+
// The action strip can be a sibling of the row (the turn-tail row), not
|
|
2530
|
+
// its child. Find the nearest one from our own injected action buttons
|
|
2531
|
+
// and include its bottom in the cover.
|
|
2532
|
+
let actionBottom = 0
|
|
2533
|
+
if (legacy) try {
|
|
2534
|
+
const actions = document.querySelectorAll('.dshet-action')
|
|
2535
|
+
for (const action of actions) {
|
|
2536
|
+
const bar = action.parentElement
|
|
2537
|
+
if (bar === null || bar === undefined || typeof bar.getBoundingClientRect !== 'function') continue
|
|
2538
|
+
const rect = bar.getBoundingClientRect()
|
|
2539
|
+
if (rect.top >= rowRect.top - 8 && rect.top <= rowRect.bottom + 160) {
|
|
2540
|
+
actionBottom = Math.max(actionBottom, rect.bottom)
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
} catch {}
|
|
2544
|
+
// The action strip is rendered as its own turn-tail row under the
|
|
2545
|
+
// message row. Use that row's bottom; if the host renders it in a shape
|
|
2546
|
+
// we cannot identify, reserve one action-band height so the buttons can
|
|
2547
|
+
// never peek out underneath the opaque editor.
|
|
2548
|
+
let tailBottom = 0
|
|
2549
|
+
if (legacy) try {
|
|
2550
|
+
const tails = document.querySelectorAll('[data-chat-flow-kind="turn-tail"]')
|
|
2551
|
+
for (const tail of tails) {
|
|
2552
|
+
if (typeof tail.getBoundingClientRect !== 'function') continue
|
|
2553
|
+
const rect = tail.getBoundingClientRect()
|
|
2554
|
+
if (rect.top >= rowRect.top - 8 && rect.top <= rowRect.bottom + 200) {
|
|
2555
|
+
tailBottom = Math.max(tailBottom, rect.bottom)
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
} catch {}
|
|
2559
|
+
const coverBottom = Math.max(rowBottom, actionBottom, tailBottom)
|
|
2560
|
+
const coveredHeight = Math.max(base.height, coverBottom - base.top + 48)
|
|
2561
|
+
// Non-legacy modes hug the content: the box ends right after the last
|
|
2562
|
+
// line plus the control band, instead of stretching to the row bottom.
|
|
2563
|
+
const FOOTER_BAND = 46
|
|
2564
|
+
const boxRect0 = typeof box.getBoundingClientRect === 'function' ? box.getBoundingClientRect() : { top: base.top }
|
|
2565
|
+
const chipStrip0 = typeof box.querySelector === 'function' ? box.querySelector('.dshet-chips') : null
|
|
2566
|
+
const chipBottom0 = chipStrip0 === null || chipStrip0 === undefined || typeof chipStrip0.getBoundingClientRect !== 'function'
|
|
2567
|
+
? 0
|
|
2568
|
+
: Math.max(0, chipStrip0.getBoundingClientRect().bottom - boxRect0.top + 8)
|
|
2569
|
+
const headerOffset = Math.max(38, chipBottom0, bubble.rect.top - base.top)
|
|
2570
|
+
let contentHeight = Math.round(base.height)
|
|
2571
|
+
if (area !== null && area !== undefined && (allBox === null || allBox === undefined)) {
|
|
2572
|
+
try {
|
|
2573
|
+
const previousValue = area.value
|
|
2574
|
+
const previousHeight = area.style.height
|
|
2575
|
+
const measure = (text) => {
|
|
2576
|
+
area.value = text
|
|
2577
|
+
area.style.height = 'auto'
|
|
2578
|
+
const measured = area.scrollHeight
|
|
2579
|
+
return typeof measured === 'number' && measured > 0 ? Math.round(measured) : 0
|
|
2580
|
+
}
|
|
2581
|
+
const own = measure(previousValue)
|
|
2582
|
+
if (own > 0) contentHeight = own
|
|
2583
|
+
// "Turn max" pins the box to the tallest block of the turn, so
|
|
2584
|
+
// switching between a short answer and a long thought does not resize
|
|
2585
|
+
// the frame under the user's hands. The measurement is cached: doing
|
|
2586
|
+
// it for every block on every scroll frame is what made the editor
|
|
2587
|
+
// drag while the conversation moved.
|
|
2588
|
+
if (heightMode !== 'content' && Array.isArray(blocks)) {
|
|
2589
|
+
const key = `${Math.round(area.clientWidth || 0)}|${blocks.map((block) => `${block.seq ?? ''}:${block.index}:${typeof block.text === 'string' ? block.text.length : 0}`).join(',')}`
|
|
2590
|
+
if (maxHeightCache.key === key) {
|
|
2591
|
+
contentHeight = Math.max(contentHeight, maxHeightCache.value)
|
|
2592
|
+
} else {
|
|
2593
|
+
let tallest = 0
|
|
2594
|
+
for (const block of blocks) {
|
|
2595
|
+
if (block === null || block === undefined || typeof block.text !== 'string') continue
|
|
2596
|
+
const measured = measure(block.text)
|
|
2597
|
+
if (measured > tallest) tallest = measured
|
|
2598
|
+
}
|
|
2599
|
+
maxHeightCache = { key, value: tallest }
|
|
2600
|
+
if (tallest > contentHeight) contentHeight = tallest
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
area.value = previousValue
|
|
2604
|
+
area.style.height = previousHeight
|
|
2605
|
+
} catch {}
|
|
2606
|
+
}
|
|
2607
|
+
// Every mode hugs the measured content; `coveredHeight` is only a
|
|
2608
|
+
// fallback for a host whose textarea cannot be measured.
|
|
2609
|
+
let height = Math.max(minimumHeight, contentHeight + headerOffset + FOOTER_BAND)
|
|
2610
|
+
let clampedTop = null
|
|
2611
|
+
if (clamped) {
|
|
2612
|
+
const bounds = conversationBounds(row)
|
|
2613
|
+
const bottomLimit = (bounds.bottom > 0 ? bounds.bottom : viewport.height) - margin
|
|
2614
|
+
const topLimit = bounds.top + margin
|
|
2615
|
+
if (bottomLimit > topLimit) {
|
|
2616
|
+
// All-blocks mode shows one page of blocks behind ONE scrollbar: it
|
|
2617
|
+
// fills the WHOLE band (not just the message's own position), so the
|
|
2618
|
+
// top touches the conversation edge and the bottom the composer.
|
|
2619
|
+
const fillAll = allBox !== null && allBox !== undefined && heightMode !== 'content'
|
|
2620
|
+
clampedTop = fillAll ? topLimit : Math.max(base.top, topLimit)
|
|
2621
|
+
height = fillAll
|
|
2622
|
+
? Math.max(minimumHeight, bottomLimit - clampedTop)
|
|
2623
|
+
: Math.min(height, Math.max(minimumHeight, bottomLimit - clampedTop))
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
if (!inline && !clamped && viewport.height > 0) height = Math.min(height, Math.max(minimumHeight, viewport.height - margin * 2))
|
|
2627
|
+
height = Math.max(minimumHeight, height)
|
|
2628
|
+
box.style.height = `${height}px`
|
|
2629
|
+
box.style.maxHeight = ''
|
|
2630
|
+
let top = clampedTop === null ? base.top : clampedTop
|
|
2631
|
+
if (inline) {
|
|
2632
|
+
const pageY = typeof window !== 'undefined' && typeof window.scrollY === 'number' ? window.scrollY : 0
|
|
2633
|
+
top += pageY
|
|
2634
|
+
} else if (viewport.height > 0) {
|
|
2635
|
+
top = Math.max(margin, Math.min(top, viewport.height - height - margin))
|
|
2636
|
+
}
|
|
1992
2637
|
box.style.top = `${Math.round(top)}px`
|
|
2638
|
+
if (allBox !== null && allBox !== undefined) {
|
|
2639
|
+
allBox.style.position = 'absolute'
|
|
2640
|
+
allBox.style.left = '8px'
|
|
2641
|
+
allBox.style.right = '8px'
|
|
2642
|
+
// The all-blocks page has its own sticky outline, so it does not need
|
|
2643
|
+
// the 38px control header the single-block editor reserves.
|
|
2644
|
+
allBox.style.top = '6px'
|
|
2645
|
+
allBox.style.bottom = `${FOOTER_BAND + 8}px`
|
|
2646
|
+
allBox.style.overflowY = 'auto'
|
|
2647
|
+
// The container is now laid out; re-measure each block so the document
|
|
2648
|
+
// is exactly as tall as its text and the ONE scrollbar appears when it
|
|
2649
|
+
// overflows the band.
|
|
2650
|
+
if (typeof allBox.querySelectorAll === 'function') {
|
|
2651
|
+
for (const blockArea of allBox.querySelectorAll('.dshet-all-area')) {
|
|
2652
|
+
blockArea.style.height = 'auto'
|
|
2653
|
+
const measured = blockArea.scrollHeight
|
|
2654
|
+
if (typeof measured === 'number' && measured > 0) blockArea.style.height = `${measured}px`
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
} else if (area !== null && area !== undefined) {
|
|
2658
|
+
const offsetLeft = Math.max(0, bubble.rect.left - base.left)
|
|
2659
|
+
// Reserve a header band for the controls; when the prompt carries image
|
|
2660
|
+
// chips they sit above the text, so the text starts below them too.
|
|
2661
|
+
const offsetTop = headerOffset
|
|
2662
|
+
const available = Math.max(40, height - offsetTop - 8)
|
|
2663
|
+
area.style.position = 'absolute'
|
|
2664
|
+
area.style.boxSizing = 'border-box'
|
|
2665
|
+
area.style.left = `${Math.round(offsetLeft)}px`
|
|
2666
|
+
area.style.top = `${Math.round(offsetTop)}px`
|
|
2667
|
+
area.style.width = `${Math.round(mode === 'reply' ? Math.max(80, width - offsetLeft - 8) : Math.max(80, Math.min(bubble.rect.width, width - offsetLeft)))}px`
|
|
2668
|
+
// Never let the textarea paint outside the box: when the measured
|
|
2669
|
+
// content is taller than the box (viewport-capped overlay), it scrolls
|
|
2670
|
+
// inside instead of spilling past the border.
|
|
2671
|
+
area.style.height = `${Math.round(Math.max(32, Math.min(contentHeight, available)))}px`
|
|
2672
|
+
area.style.maxHeight = 'none'
|
|
2673
|
+
area.style.padding = ''
|
|
2674
|
+
area.style.paddingBottom = '8px'
|
|
2675
|
+
}
|
|
2676
|
+
box.style.borderRadius = ''
|
|
2677
|
+
try {
|
|
2678
|
+
const skin = getComputedStyle(bubble.element)
|
|
2679
|
+
if (skin.borderRadius && skin.borderRadius !== '0px') box.style.borderRadius = skin.borderRadius
|
|
2680
|
+
} catch {
|
|
2681
|
+
/* a detached node keeps the stylesheet skin */
|
|
2682
|
+
}
|
|
1993
2683
|
}
|
|
1994
2684
|
|
|
1995
2685
|
// Grow the textarea to its content, the way a normal input box behaves.
|
|
@@ -1997,7 +2687,10 @@ window.__ModuleLoader__.load({
|
|
|
1997
2687
|
// in half an empty box. The stylesheet owns the limits (`min-height`,
|
|
1998
2688
|
// `max-height`); the element only ever gets the measured height, and the
|
|
1999
2689
|
// `+2` is the top and bottom border `scrollHeight` does not count.
|
|
2690
|
+
let maxHeightCache = { key: '', value: 0 }
|
|
2691
|
+
|
|
2000
2692
|
function fitEditorHeight(area) {
|
|
2693
|
+
if (typeof area.closest === 'function' && area.closest('[data-dshet-inplace="1"]') !== null) return
|
|
2001
2694
|
area.style.height = 'auto'
|
|
2002
2695
|
const height = area.scrollHeight
|
|
2003
2696
|
if (typeof height === 'number' && height > 0) area.style.height = `${height + 2}px`
|
|
@@ -2019,7 +2712,7 @@ window.__ModuleLoader__.load({
|
|
|
2019
2712
|
pressed = true
|
|
2020
2713
|
event.preventDefault()
|
|
2021
2714
|
event.stopPropagation()
|
|
2022
|
-
action()
|
|
2715
|
+
action(event)
|
|
2023
2716
|
}
|
|
2024
2717
|
button.onclick = (event) => {
|
|
2025
2718
|
event.preventDefault()
|
|
@@ -2028,7 +2721,7 @@ window.__ModuleLoader__.load({
|
|
|
2028
2721
|
pressed = false
|
|
2029
2722
|
return
|
|
2030
2723
|
}
|
|
2031
|
-
action()
|
|
2724
|
+
action(event)
|
|
2032
2725
|
}
|
|
2033
2726
|
}
|
|
2034
2727
|
|
|
@@ -2149,6 +2842,7 @@ window.__ModuleLoader__.load({
|
|
|
2149
2842
|
view.parts.map((part) => part.key).join('.'),
|
|
2150
2843
|
// The selected reply block: switching blocks has to rebuild the box.
|
|
2151
2844
|
view.blockIndex === null || view.blockIndex === undefined ? '' : String(view.blockIndex),
|
|
2845
|
+
view.targetSeq === null || view.targetSeq === undefined ? '' : String(view.targetSeq),
|
|
2152
2846
|
].join('|')
|
|
2153
2847
|
}
|
|
2154
2848
|
|
|
@@ -2224,6 +2918,16 @@ window.__ModuleLoader__.load({
|
|
|
2224
2918
|
return strip
|
|
2225
2919
|
}
|
|
2226
2920
|
|
|
2921
|
+
/** The FileList of a paste/drop payload, without assuming the event shape. */
|
|
2922
|
+
function filesFromData(data) {
|
|
2923
|
+
if (data === null || data === undefined) return []
|
|
2924
|
+
try {
|
|
2925
|
+
return data.files === null || data.files === undefined ? [] : Array.prototype.slice.call(data.files)
|
|
2926
|
+
} catch {
|
|
2927
|
+
return []
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2227
2931
|
/**
|
|
2228
2932
|
* The add/replace entry: a picker that puts a block on the list.
|
|
2229
2933
|
*
|
|
@@ -2252,7 +2956,10 @@ window.__ModuleLoader__.load({
|
|
|
2252
2956
|
const add = document.createElement('button')
|
|
2253
2957
|
add.type = 'button'
|
|
2254
2958
|
add.className = 'dshet-attach'
|
|
2255
|
-
add.
|
|
2959
|
+
add.innerHTML = '<svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" stroke-width="1.3"><path d="M8 2V14" stroke="currentColor"></path><path d="M2 8H14" stroke="currentColor"></path></svg>'
|
|
2960
|
+
add.setAttribute('aria-haspopup', 'listbox')
|
|
2961
|
+
add.title = t('editor.attach.add')
|
|
2962
|
+
add.setAttribute('aria-label', t('editor.attach.add'))
|
|
2256
2963
|
add.disabled = view.pending
|
|
2257
2964
|
pressable(add, () => {
|
|
2258
2965
|
if (typeof picker.click === 'function') picker.click()
|
|
@@ -2270,7 +2977,7 @@ window.__ModuleLoader__.load({
|
|
|
2270
2977
|
? controller.editorBox
|
|
2271
2978
|
: null
|
|
2272
2979
|
if (existing && existing.dataset.dshetFor === marker) {
|
|
2273
|
-
placeEditor(row, existing)
|
|
2980
|
+
placeEditor(row, existing, controller.editorMode, controller.editorStyle, controller.editorBlockKind, controller.editorBlocks)
|
|
2274
2981
|
return
|
|
2275
2982
|
}
|
|
2276
2983
|
|
|
@@ -2292,30 +2999,64 @@ window.__ModuleLoader__.load({
|
|
|
2292
2999
|
box.className = 'dshet-editor'
|
|
2293
3000
|
box.dataset[EDITOR_ATTR] = '1'
|
|
2294
3001
|
box.dataset.dshetFor = marker
|
|
3002
|
+
// The frame is a child, not the box's own border: host skins mark the
|
|
3003
|
+
// editor chrome `!important` and win the cascade against the box itself,
|
|
3004
|
+
// but they cannot name this child. It draws the blue outline of the
|
|
3005
|
+
// reference UI on top of whatever border the skin puts on the box.
|
|
3006
|
+
const frame = document.createElement('span')
|
|
3007
|
+
frame.className = 'dshet-frame'
|
|
3008
|
+
frame.style.position = 'absolute'
|
|
3009
|
+
frame.style.inset = '-1px'
|
|
3010
|
+
frame.style.border = '2px solid #4a7dff'
|
|
3011
|
+
frame.style.borderRadius = 'inherit'
|
|
3012
|
+
frame.style.boxSizing = 'border-box'
|
|
3013
|
+
frame.style.pointerEvents = 'none'
|
|
3014
|
+
frame.style.zIndex = '3'
|
|
3015
|
+
box.appendChild(frame)
|
|
2295
3016
|
|
|
2296
3017
|
// A reply on the patch path may carry several editable blocks (answer
|
|
2297
3018
|
// text and reasoning). The picker chooses which one this save targets;
|
|
2298
3019
|
// every option carries the block's CURRENT projected text, so switching
|
|
2299
3020
|
// never shows the recorded original over a landed patch.
|
|
2300
|
-
if (target.mode === 'reply' && target.patch === true && Array.isArray(target.blocks) && target.blocks.length
|
|
3021
|
+
if (target.mode === 'reply' && target.patch === true && Array.isArray(target.blocks) && target.blocks.length >= 1) {
|
|
2301
3022
|
const picker = document.createElement('label')
|
|
2302
3023
|
picker.className = 'dshet-pick'
|
|
2303
3024
|
const caption = document.createElement('span')
|
|
3025
|
+
caption.className = 'dshet-sr-only'
|
|
2304
3026
|
caption.textContent = t('editor.block')
|
|
2305
3027
|
const select = document.createElement('select')
|
|
2306
3028
|
select.className = 'dshet-select'
|
|
2307
3029
|
select.disabled = view.pending || view.confirming
|
|
2308
3030
|
select.setAttribute('aria-label', t('editor.block'))
|
|
3031
|
+
// A single reply can carry several answer texts and several reasoning
|
|
3032
|
+
// blocks; number the labels so each one is addressable in the picker.
|
|
3033
|
+
const totals = {}
|
|
3034
|
+
for (const block of target.blocks) totals[block.kind] = (totals[block.kind] ?? 0) + 1
|
|
3035
|
+
const seen = {}
|
|
2309
3036
|
for (const block of target.blocks) {
|
|
2310
3037
|
const option = document.createElement('option')
|
|
2311
|
-
option.value = String(block.index)
|
|
3038
|
+
option.value = typeof block.seq === 'number' ? `${block.seq}:${block.index}` : String(block.index)
|
|
2312
3039
|
const kind = block.kind === 'reasoning' ? 'editor.block.reasoning' : 'editor.block.text'
|
|
2313
|
-
|
|
3040
|
+
seen[block.kind] = (seen[block.kind] ?? 0) + 1
|
|
3041
|
+
option.textContent = totals[block.kind] > 1 ? `${t(kind)} ${seen[block.kind]}` : t(kind)
|
|
2314
3042
|
option.selected = block.index === view.blockIndex
|
|
2315
3043
|
select.appendChild(option)
|
|
2316
3044
|
}
|
|
2317
|
-
|
|
2318
|
-
|
|
3045
|
+
// "All blocks" is always offered for a multi-block turn, not only once
|
|
3046
|
+
// it is already active - otherwise there is no way to select it.
|
|
3047
|
+
if (target.blocks.length > 1) {
|
|
3048
|
+
const allOption = document.createElement('option')
|
|
3049
|
+
allOption.value = 'all'
|
|
3050
|
+
allOption.textContent = t('editor.block.all')
|
|
3051
|
+
select.appendChild(allOption)
|
|
3052
|
+
}
|
|
3053
|
+
if (view.allBlocks === true) {
|
|
3054
|
+
select.value = 'all'
|
|
3055
|
+
} else if (view.blockIndex !== null && view.blockIndex !== undefined) {
|
|
3056
|
+
const current = target.blocks.find((block) => block.index === view.blockIndex && (view.targetSeq === null || view.targetSeq === undefined || block.seq === undefined || block.seq === view.targetSeq))
|
|
3057
|
+
select.value = current !== undefined && typeof current.seq === 'number' ? `${current.seq}:${current.index}` : String(view.blockIndex)
|
|
3058
|
+
}
|
|
3059
|
+
select.addEventListener('change', () => controller.selectBlock(select.value))
|
|
2319
3060
|
picker.appendChild(caption)
|
|
2320
3061
|
picker.appendChild(select)
|
|
2321
3062
|
box.appendChild(picker)
|
|
@@ -2337,13 +3078,13 @@ window.__ModuleLoader__.load({
|
|
|
2337
3078
|
fitEditorHeight(area)
|
|
2338
3079
|
const row = controller.editorRow
|
|
2339
3080
|
const box = controller.editorBox
|
|
2340
|
-
if (row !== null && box !== null && box.isConnected === true) placeEditor(row, box)
|
|
3081
|
+
if (row !== null && box !== null && box.isConnected === true) placeEditor(row, box, controller.editorMode, controller.editorStyle, controller.editorBlockKind, controller.editorBlocks)
|
|
2341
3082
|
})
|
|
2342
3083
|
area.addEventListener('compositionend', () => {
|
|
2343
3084
|
fitEditorHeight(area)
|
|
2344
3085
|
const row = controller.editorRow
|
|
2345
3086
|
const box = controller.editorBox
|
|
2346
|
-
if (row !== null && box !== null && box.isConnected === true) placeEditor(row, box)
|
|
3087
|
+
if (row !== null && box !== null && box.isConnected === true) placeEditor(row, box, controller.editorMode, controller.editorStyle, controller.editorBlockKind, controller.editorBlocks)
|
|
2347
3088
|
})
|
|
2348
3089
|
area.addEventListener('focus', () => {
|
|
2349
3090
|
controller.editorFocus = true
|
|
@@ -2370,6 +3111,100 @@ window.__ModuleLoader__.load({
|
|
|
2370
3111
|
})
|
|
2371
3112
|
box.appendChild(area)
|
|
2372
3113
|
|
|
3114
|
+
// "All blocks": every reasoning and text block of the turn on one page,
|
|
3115
|
+
// each in its own textarea under a light separator.
|
|
3116
|
+
if (view.allBlocks === true && Array.isArray(target.blocks)) {
|
|
3117
|
+
const allBox = document.createElement('div')
|
|
3118
|
+
allBox.className = 'dshet-all'
|
|
3119
|
+
// A sticky outline over the page: one chip per block that scrolls the
|
|
3120
|
+
// container to it, so the document keeps its single scrollbar and still
|
|
3121
|
+
// has navigation.
|
|
3122
|
+
const nav = document.createElement('div')
|
|
3123
|
+
nav.className = 'dshet-all-nav'
|
|
3124
|
+
const totals = {}
|
|
3125
|
+
for (const block of target.blocks) totals[block.kind] = (totals[block.kind] ?? 0) + 1
|
|
3126
|
+
const seen = {}
|
|
3127
|
+
for (const block of target.blocks) {
|
|
3128
|
+
const key = `${typeof block.seq === 'number' ? block.seq : ''}:${block.index}`
|
|
3129
|
+
const section = document.createElement('div')
|
|
3130
|
+
section.className = 'dshet-all-block'
|
|
3131
|
+
const head = document.createElement('div')
|
|
3132
|
+
head.className = 'dshet-all-head'
|
|
3133
|
+
const label = document.createElement('span')
|
|
3134
|
+
seen[block.kind] = (seen[block.kind] ?? 0) + 1
|
|
3135
|
+
const kindKey = block.kind === 'reasoning' ? 'editor.block.reasoning' : 'editor.block.text'
|
|
3136
|
+
const labelText = totals[block.kind] > 1 ? `${t(kindKey)} ${seen[block.kind]}` : t(kindKey)
|
|
3137
|
+
label.textContent = labelText
|
|
3138
|
+
const navButton = document.createElement('button')
|
|
3139
|
+
navButton.type = 'button'
|
|
3140
|
+
navButton.textContent = labelText
|
|
3141
|
+
navButton.addEventListener('click', () => {
|
|
3142
|
+
if (typeof section.scrollIntoView === 'function') section.scrollIntoView({ block: 'start' })
|
|
3143
|
+
else allBox.scrollTop = section.offsetTop ?? 0
|
|
3144
|
+
})
|
|
3145
|
+
nav.appendChild(navButton)
|
|
3146
|
+
const line = document.createElement('span')
|
|
3147
|
+
line.className = 'dshet-all-line'
|
|
3148
|
+
head.appendChild(label)
|
|
3149
|
+
head.appendChild(line)
|
|
3150
|
+
const blockArea = document.createElement('textarea')
|
|
3151
|
+
blockArea.className = 'dshet-all-area'
|
|
3152
|
+
blockArea.rows = 1
|
|
3153
|
+
blockArea.spellcheck = false
|
|
3154
|
+
blockArea.style.overflow = 'hidden'
|
|
3155
|
+
blockArea.style.position = 'static'
|
|
3156
|
+
blockArea.style.left = 'auto'
|
|
3157
|
+
blockArea.style.top = 'auto'
|
|
3158
|
+
blockArea.value = controller.editorAllDrafts instanceof Map ? (controller.editorAllDrafts.get(key) ?? block.text) : block.text
|
|
3159
|
+
blockArea.disabled = view.pending || view.confirming
|
|
3160
|
+
const grow = () => {
|
|
3161
|
+
blockArea.style.height = 'auto'
|
|
3162
|
+
const measured = blockArea.scrollHeight
|
|
3163
|
+
if (typeof measured === 'number' && measured > 0) blockArea.style.height = `${measured}px`
|
|
3164
|
+
}
|
|
3165
|
+
blockArea.addEventListener('input', () => {
|
|
3166
|
+
if (controller.editorAllDrafts instanceof Map) controller.editorAllDrafts.set(key, blockArea.value)
|
|
3167
|
+
grow()
|
|
3168
|
+
})
|
|
3169
|
+
section.appendChild(head)
|
|
3170
|
+
section.appendChild(blockArea)
|
|
3171
|
+
allBox.appendChild(section)
|
|
3172
|
+
// Only now is the textarea in the document, so `scrollHeight` is the
|
|
3173
|
+
// real content height; measuring before the append returned 0 and the
|
|
3174
|
+
// block stayed 32px tall with its text clipped.
|
|
3175
|
+
grow()
|
|
3176
|
+
}
|
|
3177
|
+
allBox.insertBefore(nav, allBox.firstChild)
|
|
3178
|
+
box.appendChild(allBox)
|
|
3179
|
+
area.style.display = 'none'
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
// Input parity with the composer: a screenshot pasted from the clipboard
|
|
3183
|
+
// and files dropped onto the box join the same chip list as the picker.
|
|
3184
|
+
// Reply edits cannot carry blocks, so the handlers exist only for prompts
|
|
3185
|
+
// whose host proved it can keep them.
|
|
3186
|
+
if (target.mode === 'prompt' && view.attachmentsOk === true) {
|
|
3187
|
+
area.addEventListener('paste', (event) => {
|
|
3188
|
+
const files = filesFromData(event.clipboardData)
|
|
3189
|
+
if (files.length === 0) return
|
|
3190
|
+
event.preventDefault()
|
|
3191
|
+
controller.attachFiles(files)
|
|
3192
|
+
})
|
|
3193
|
+
box.addEventListener('dragover', (event) => {
|
|
3194
|
+
event.preventDefault()
|
|
3195
|
+
box.dataset.dshetDrop = '1'
|
|
3196
|
+
})
|
|
3197
|
+
box.addEventListener('dragleave', () => {
|
|
3198
|
+
delete box.dataset.dshetDrop
|
|
3199
|
+
})
|
|
3200
|
+
box.addEventListener('drop', (event) => {
|
|
3201
|
+
event.preventDefault()
|
|
3202
|
+
delete box.dataset.dshetDrop
|
|
3203
|
+
const files = filesFromData(event.dataTransfer)
|
|
3204
|
+
if (files.length > 0) controller.attachFiles(files)
|
|
3205
|
+
})
|
|
3206
|
+
}
|
|
3207
|
+
|
|
2373
3208
|
// The blocks this message carries, each one a chip the user can remove,
|
|
2374
3209
|
// and the entry that adds another. Drawn only when the host answered that
|
|
2375
3210
|
// it can admit what a chip promises - otherwise the warning below states
|
|
@@ -2409,12 +3244,20 @@ window.__ModuleLoader__.load({
|
|
|
2409
3244
|
|
|
2410
3245
|
const footer = document.createElement('div')
|
|
2411
3246
|
footer.className = 'dshet-footer'
|
|
3247
|
+
const footerActions = document.createElement('div')
|
|
3248
|
+
footerActions.className = 'dshet-footer-actions'
|
|
2412
3249
|
|
|
2413
3250
|
const cancel = document.createElement('button')
|
|
2414
3251
|
cancel.type = 'button'
|
|
2415
|
-
cancel.className = 'dshet-btn'
|
|
2416
|
-
|
|
2417
|
-
cancel.
|
|
3252
|
+
cancel.className = 'dshet-btn dshet-btn-icon dshet-btn-cancel'
|
|
3253
|
+
const cancelLabel = t('editor.cancel')
|
|
3254
|
+
cancel.innerHTML = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3.5 3.5l9 9" stroke="currentColor"/><path d="M12.5 3.5l-9 9" stroke="currentColor"/></svg>'
|
|
3255
|
+
const cancelText = document.createElement('span')
|
|
3256
|
+
cancelText.className = 'dshet-sr-only'
|
|
3257
|
+
cancelText.textContent = cancelLabel
|
|
3258
|
+
cancel.appendChild(cancelText)
|
|
3259
|
+
cancel.title = `${cancelLabel} (Esc)`
|
|
3260
|
+
cancel.setAttribute('aria-label', cancelLabel)
|
|
2418
3261
|
cancel.disabled = view.pending
|
|
2419
3262
|
pressable(cancel, () => {
|
|
2420
3263
|
if (view.confirming) controller.back()
|
|
@@ -2423,20 +3266,29 @@ window.__ModuleLoader__.load({
|
|
|
2423
3266
|
|
|
2424
3267
|
const submit = document.createElement('button')
|
|
2425
3268
|
submit.type = 'button'
|
|
2426
|
-
submit.className = 'dshet-btn dshet-btn-primary'
|
|
2427
|
-
|
|
3269
|
+
submit.className = 'dshet-btn dshet-btn-primary dshet-btn-save'
|
|
3270
|
+
const submitLabel = view.pending
|
|
2428
3271
|
? t('editor.pending')
|
|
2429
3272
|
: view.confirming
|
|
2430
3273
|
? t('editor.review')
|
|
2431
3274
|
: texts.save
|
|
2432
|
-
|
|
3275
|
+
// The official composer uses a bare double-check for confirm, not a black
|
|
3276
|
+
// pill. The label stays in the accessible name (and in the test-visible
|
|
3277
|
+
// textContent) through a visually hidden span.
|
|
3278
|
+
submit.innerHTML = '<svg width="15" height="15" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12.5l4.5 4.5L18 6.5"/><path d="M10 12.5l4.5 4.5L24 6.5"/></svg>'
|
|
3279
|
+
const submitText = document.createElement('span')
|
|
3280
|
+
submitText.className = 'dshet-sr-only'
|
|
3281
|
+
submitText.textContent = submitLabel
|
|
3282
|
+
submit.appendChild(submitText)
|
|
3283
|
+
submit.title = `${submitLabel} (Enter)`
|
|
3284
|
+
submit.setAttribute('aria-label', submitLabel)
|
|
2433
3285
|
submit.disabled = view.pending
|
|
2434
3286
|
pressable(submit, () => {
|
|
2435
3287
|
if (view.confirming) controller.confirm()
|
|
2436
3288
|
else controller.review()
|
|
2437
3289
|
})
|
|
2438
3290
|
|
|
2439
|
-
|
|
3291
|
+
footerActions.appendChild(cancel)
|
|
2440
3292
|
// The re-run button belongs to the sibling (dsh-rerun-turn): it appears
|
|
2441
3293
|
// only when that plugin is mounted, only for a prompt edit, and only
|
|
2442
3294
|
// outside the confirmation step - the intent is carried through it. Save
|
|
@@ -2445,20 +3297,49 @@ window.__ModuleLoader__.load({
|
|
|
2445
3297
|
if (target.mode === 'prompt' && view.siblingRerun === 'present' && view.confirming !== true) {
|
|
2446
3298
|
const rerun = document.createElement('button')
|
|
2447
3299
|
rerun.type = 'button'
|
|
2448
|
-
rerun.className = 'dshet-btn dshet-rerun'
|
|
2449
|
-
|
|
3300
|
+
rerun.className = 'dshet-btn dshet-btn-icon dshet-rerun'
|
|
3301
|
+
const rerunLabel = t('editor.rerun')
|
|
3302
|
+
rerun.innerHTML = '<svg width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true" stroke-width="1.2"><path d="M13.65 2.6v2.9h-2.9" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path><path d="M13.3 8a5.3 5.3 0 1 1-1.55-3.75l1.9 1.25" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"></path></svg>'
|
|
3303
|
+
const rerunText = document.createElement('span')
|
|
3304
|
+
rerunText.className = 'dshet-sr-only'
|
|
3305
|
+
rerunText.textContent = rerunLabel
|
|
3306
|
+
rerun.appendChild(rerunText)
|
|
3307
|
+
rerun.title = rerunLabel
|
|
3308
|
+
rerun.setAttribute('aria-label', rerunLabel)
|
|
2450
3309
|
rerun.disabled = view.pending
|
|
2451
3310
|
pressable(rerun, () => controller.reviewRerun())
|
|
2452
|
-
|
|
3311
|
+
footerActions.appendChild(rerun)
|
|
3312
|
+
}
|
|
3313
|
+
footerActions.appendChild(submit)
|
|
3314
|
+
footer.appendChild(footerActions)
|
|
3315
|
+
const attachRow = typeof box.querySelector === 'function' ? box.querySelector('.dshet-attach-row') : null
|
|
3316
|
+
if (attachRow !== null && attachRow !== undefined) {
|
|
3317
|
+
attachRow.style.position = 'static'
|
|
3318
|
+
attachRow.style.left = ''
|
|
3319
|
+
attachRow.style.bottom = ''
|
|
3320
|
+
attachRow.style.margin = '0'
|
|
3321
|
+
footer.insertBefore(attachRow, footer.firstChild)
|
|
3322
|
+
}
|
|
3323
|
+
// The block picker shares the control band and sits at its far right.
|
|
3324
|
+
const pickerEl = typeof box.querySelector === 'function' ? box.querySelector('.dshet-pick') : null
|
|
3325
|
+
if (pickerEl !== null && pickerEl !== undefined) {
|
|
3326
|
+
pickerEl.style.position = 'static'
|
|
3327
|
+
pickerEl.style.left = ''
|
|
3328
|
+
pickerEl.style.top = ''
|
|
3329
|
+
pickerEl.style.marginLeft = 'auto'
|
|
3330
|
+
pickerEl.style.marginRight = '0'
|
|
3331
|
+
pickerEl.style.display = 'flex'
|
|
3332
|
+
pickerEl.style.flex = '0 0 auto'
|
|
3333
|
+
pickerEl.style.visibility = ''
|
|
3334
|
+
footer.appendChild(pickerEl)
|
|
2453
3335
|
}
|
|
2454
|
-
footer.appendChild(submit)
|
|
2455
3336
|
box.appendChild(footer)
|
|
2456
3337
|
|
|
2457
3338
|
controller.editorBox = box
|
|
2458
3339
|
controller.editorRow = row
|
|
2459
3340
|
controller.editorElement = area
|
|
3341
|
+
placeEditor(row, box, controller.editorMode, controller.editorStyle, controller.editorBlockKind, controller.editorBlocks)
|
|
2460
3342
|
fitEditorHeight(area)
|
|
2461
|
-
placeEditor(row, box)
|
|
2462
3343
|
if (!view.confirming && !view.pending && (fresh || wasFocused)) {
|
|
2463
3344
|
try {
|
|
2464
3345
|
area.focus()
|
|
@@ -2802,6 +3683,8 @@ window.__ModuleLoader__.load({
|
|
|
2802
3683
|
const animate = controller.consumeAnimate()
|
|
2803
3684
|
const rows = document.querySelectorAll('[data-chat-flow-key]')
|
|
2804
3685
|
syncEditableSet(controller, rows.length)
|
|
3686
|
+
let editorCandidate = null
|
|
3687
|
+
let editorRank = -1
|
|
2805
3688
|
for (const row of rows) {
|
|
2806
3689
|
if (!(row instanceof HTMLElement)) continue
|
|
2807
3690
|
const key = row.getAttribute('data-chat-flow-key')
|
|
@@ -2854,7 +3737,20 @@ window.__ModuleLoader__.load({
|
|
|
2854
3737
|
(seqsFor(node).includes(editing.seq) ||
|
|
2855
3738
|
seqsFor(node).some((seq) => headRevision(seq, view.revisions) === editing.seq))
|
|
2856
3739
|
if (coversEditing && (!hidden || row.style.display !== 'none')) {
|
|
2857
|
-
|
|
3740
|
+
const rank = editorRowRank(row, editing.mode)
|
|
3741
|
+
if (rank > editorRank) {
|
|
3742
|
+
editorRank = rank
|
|
3743
|
+
editorCandidate = row
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
if (view.editing !== null) {
|
|
3748
|
+
if (editorCandidate !== null) {
|
|
3749
|
+
renderEditor(editorCandidate, controller, view, t)
|
|
3750
|
+
} else {
|
|
3751
|
+
clearEditor()
|
|
3752
|
+
controller.editorBox = null
|
|
3753
|
+
controller.editorRow = null
|
|
2858
3754
|
}
|
|
2859
3755
|
}
|
|
2860
3756
|
if (view.editing === null) {
|
|
@@ -2932,6 +3828,7 @@ window.__ModuleLoader__.load({
|
|
|
2932
3828
|
event.preventDefault()
|
|
2933
3829
|
event.stopPropagation()
|
|
2934
3830
|
controller.open({
|
|
3831
|
+
anchor: event.currentTarget,
|
|
2935
3832
|
seq: entry.seq,
|
|
2936
3833
|
mode: 'reply',
|
|
2937
3834
|
turn: entry.turn,
|
|
@@ -2982,23 +3879,51 @@ window.__ModuleLoader__.load({
|
|
|
2982
3879
|
&& target.closest('.dshet-editor') !== null
|
|
2983
3880
|
if (!inside) controller.editorFocus = false
|
|
2984
3881
|
}
|
|
3882
|
+
let placementScheduled = false
|
|
3883
|
+
let lastPlacementKey = ''
|
|
2985
3884
|
const keepPlaced = () => {
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
3885
|
+
if (placementScheduled) return
|
|
3886
|
+
placementScheduled = true
|
|
3887
|
+
const run = () => {
|
|
3888
|
+
placementScheduled = false
|
|
3889
|
+
const row = controller.editorRow
|
|
3890
|
+
const box = controller.editorBox
|
|
3891
|
+
if (row === null || box === null || box.isConnected !== true) return
|
|
3892
|
+
const rect = row.getBoundingClientRect()
|
|
3893
|
+
const viewportWidth = typeof window !== 'undefined' && typeof window.innerWidth === 'number' ? window.innerWidth : 0
|
|
3894
|
+
const viewportHeight = typeof window !== 'undefined' && typeof window.innerHeight === 'number' ? window.innerHeight : 0
|
|
3895
|
+
const key = `${Math.round(rect.top)}:${Math.round(rect.height)}:${Math.round(typeof window !== 'undefined' && typeof window.scrollY === 'number' ? window.scrollY : 0)}:${viewportWidth}x${viewportHeight}:${controller.editorBlockKind}:${controller.editorAll === true ? 'all' : ''}`
|
|
3896
|
+
if (key === lastPlacementKey) return
|
|
3897
|
+
lastPlacementKey = key
|
|
3898
|
+
placeEditor(row, box, controller.editorMode, controller.editorStyle, controller.editorBlockKind, controller.editorBlocks)
|
|
3899
|
+
}
|
|
3900
|
+
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(run)
|
|
3901
|
+
else window.setTimeout(run, 16)
|
|
3902
|
+
}
|
|
3903
|
+
const onResize = () => {
|
|
3904
|
+
// A window resize (or a fullscreen toggle / zoom) moves the composer
|
|
3905
|
+
// and the conversation edges; the cached band must not survive it.
|
|
3906
|
+
conversationBoundsCache = null
|
|
3907
|
+
keepPlaced()
|
|
2989
3908
|
}
|
|
2990
3909
|
document.addEventListener('pointerdown', onPointerDown, true)
|
|
2991
3910
|
// Scroll events do not bubble; capture reaches the inner scroller too.
|
|
2992
3911
|
document.addEventListener('scroll', keepPlaced, { capture: true, passive: true })
|
|
2993
|
-
window.addEventListener('resize',
|
|
3912
|
+
window.addEventListener('resize', onResize)
|
|
3913
|
+
let resizeObserver = null
|
|
3914
|
+
if (typeof ResizeObserver === 'function') {
|
|
3915
|
+
resizeObserver = new ResizeObserver(onResize)
|
|
3916
|
+
try { resizeObserver.observe(document.body) } catch {}
|
|
3917
|
+
}
|
|
2994
3918
|
return () => {
|
|
2995
3919
|
document.removeEventListener('pointerdown', onPointerDown, true)
|
|
2996
3920
|
document.removeEventListener('scroll', keepPlaced, true)
|
|
2997
|
-
window.removeEventListener('resize',
|
|
3921
|
+
window.removeEventListener('resize', onResize)
|
|
3922
|
+
if (resizeObserver !== null) resizeObserver.disconnect()
|
|
2998
3923
|
}
|
|
2999
3924
|
}, [controller])
|
|
3000
3925
|
|
|
3001
|
-
|
|
3926
|
+
useBrowserLayoutEffect(() => {
|
|
3002
3927
|
if (snapshot === undefined) return undefined
|
|
3003
3928
|
let scheduled = false
|
|
3004
3929
|
const run = () => {
|
|
@@ -3010,7 +3935,33 @@ window.__ModuleLoader__.load({
|
|
|
3010
3935
|
applyDom(snapshot, live, controller, t)
|
|
3011
3936
|
}
|
|
3012
3937
|
run()
|
|
3013
|
-
const observer = new MutationObserver(() => {
|
|
3938
|
+
const observer = new MutationObserver((records) => {
|
|
3939
|
+
// Our own editor writes attributes and text while the user types;
|
|
3940
|
+
// feeding those back into a full document pass is what made typing
|
|
3941
|
+
// and streaming compete for the same frames.
|
|
3942
|
+
let relevant = false
|
|
3943
|
+
let lostOurAction = false
|
|
3944
|
+
for (const record of records) {
|
|
3945
|
+
const target = record.target
|
|
3946
|
+
if (target !== null && target !== undefined && typeof target.closest === 'function' && target.closest('.dshet-layer') !== null) continue
|
|
3947
|
+
relevant = true
|
|
3948
|
+
if (record.removedNodes && record.removedNodes.length > 0) {
|
|
3949
|
+
for (const node of record.removedNodes) {
|
|
3950
|
+
if (node === null || node === undefined) continue
|
|
3951
|
+
if (typeof node.closest === 'function' && node.closest('.dshet-action') !== null) lostOurAction = true
|
|
3952
|
+
else if (typeof node.querySelector === 'function' && node.querySelector('.dshet-action') !== null) lostOurAction = true
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
break
|
|
3956
|
+
}
|
|
3957
|
+
if (!relevant) return
|
|
3958
|
+
// The host rebuilt a row and our button went with it: put it back in
|
|
3959
|
+
// the same task instead of waiting a frame for the rAF pass.
|
|
3960
|
+
if (lostOurAction) {
|
|
3961
|
+
scheduled = false
|
|
3962
|
+
run()
|
|
3963
|
+
return
|
|
3964
|
+
}
|
|
3014
3965
|
if (scheduled) return
|
|
3015
3966
|
scheduled = true
|
|
3016
3967
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(run)
|
|
@@ -3117,6 +4068,192 @@ window.__ModuleLoader__.load({
|
|
|
3117
4068
|
editorLayer = null
|
|
3118
4069
|
}
|
|
3119
4070
|
|
|
4071
|
+
// --- editor-style preference ----------------------------------------------
|
|
4072
|
+
//
|
|
4073
|
+
// The style is a browser preference so the manager page and the editor read
|
|
4074
|
+
// one value with no host round-trip. The host config's editorStyle is the
|
|
4075
|
+
// default when nothing has been chosen here.
|
|
4076
|
+
const EDITOR_STYLE_KEY = 'dsh-edit-turn/editorStyle'
|
|
4077
|
+
const EDITOR_STYLES = ['frame', 'bubble', 'center']
|
|
4078
|
+
|
|
4079
|
+
function readStoredEditorStyle() {
|
|
4080
|
+
try {
|
|
4081
|
+
if (typeof localStorage === 'undefined') return null
|
|
4082
|
+
const value = localStorage.getItem(EDITOR_STYLE_KEY)
|
|
4083
|
+
return EDITOR_STYLES.includes(value) ? value : null
|
|
4084
|
+
} catch {
|
|
4085
|
+
return null
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
|
|
4089
|
+
let editorStylePreference = readStoredEditorStyle() ?? 'frame'
|
|
4090
|
+
const EDITOR_PLACEMENT_KEY = 'dsh-edit-turn/editorPlacement'
|
|
4091
|
+
const EDITOR_PLACEMENTS = ['overlay', 'inline', 'legacy']
|
|
4092
|
+
|
|
4093
|
+
function readStoredEditorPlacement() {
|
|
4094
|
+
try {
|
|
4095
|
+
if (typeof localStorage === 'undefined') return null
|
|
4096
|
+
const value = localStorage.getItem(EDITOR_PLACEMENT_KEY)
|
|
4097
|
+
return EDITOR_PLACEMENTS.includes(value) ? value : null
|
|
4098
|
+
} catch {
|
|
4099
|
+
return null
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
let editorPlacementPreference = readStoredEditorPlacement() ?? 'overlay'
|
|
4104
|
+
const EDITOR_HEIGHT_KEY = 'dsh-edit-turn/editorHeight'
|
|
4105
|
+
const EDITOR_HEIGHTS = ['max', 'content', 'fill']
|
|
4106
|
+
|
|
4107
|
+
function readStoredEditorHeight() {
|
|
4108
|
+
try {
|
|
4109
|
+
if (typeof localStorage === 'undefined') return null
|
|
4110
|
+
const value = localStorage.getItem(EDITOR_HEIGHT_KEY)
|
|
4111
|
+
return EDITOR_HEIGHTS.includes(value) ? value : null
|
|
4112
|
+
} catch {
|
|
4113
|
+
return null
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
|
|
4117
|
+
let editorHeightPreference = readStoredEditorHeight() ?? 'fill'
|
|
4118
|
+
const editorHeightListeners = new Set()
|
|
4119
|
+
|
|
4120
|
+
function subscribeEditorHeight(listener) {
|
|
4121
|
+
editorHeightListeners.add(listener)
|
|
4122
|
+
return () => editorHeightListeners.delete(listener)
|
|
4123
|
+
}
|
|
4124
|
+
|
|
4125
|
+
function setEditorHeightPreference(mode) {
|
|
4126
|
+
if (!EDITOR_HEIGHTS.includes(mode)) return
|
|
4127
|
+
editorHeightPreference = mode
|
|
4128
|
+
try {
|
|
4129
|
+
if (typeof localStorage !== 'undefined') localStorage.setItem(EDITOR_HEIGHT_KEY, mode)
|
|
4130
|
+
} catch {}
|
|
4131
|
+
for (const listener of [...editorHeightListeners]) {
|
|
4132
|
+
try { listener() } catch {}
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
|
|
4136
|
+
const editorPlacementListeners = new Set()
|
|
4137
|
+
|
|
4138
|
+
function subscribeEditorPlacement(listener) {
|
|
4139
|
+
editorPlacementListeners.add(listener)
|
|
4140
|
+
return () => editorPlacementListeners.delete(listener)
|
|
4141
|
+
}
|
|
4142
|
+
|
|
4143
|
+
function setEditorPlacementPreference(placement) {
|
|
4144
|
+
if (!EDITOR_PLACEMENTS.includes(placement)) return
|
|
4145
|
+
editorPlacementPreference = placement
|
|
4146
|
+
try {
|
|
4147
|
+
if (typeof localStorage !== 'undefined') localStorage.setItem(EDITOR_PLACEMENT_KEY, placement)
|
|
4148
|
+
} catch {
|
|
4149
|
+
/* private mode keeps the in-memory choice */
|
|
4150
|
+
}
|
|
4151
|
+
for (const listener of [...editorPlacementListeners]) {
|
|
4152
|
+
try {
|
|
4153
|
+
listener()
|
|
4154
|
+
} catch {}
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
|
|
4158
|
+
const editorStyleListeners = new Set()
|
|
4159
|
+
|
|
4160
|
+
function subscribeEditorStyle(listener) {
|
|
4161
|
+
editorStyleListeners.add(listener)
|
|
4162
|
+
return () => editorStyleListeners.delete(listener)
|
|
4163
|
+
}
|
|
4164
|
+
|
|
4165
|
+
function setEditorStylePreference(style) {
|
|
4166
|
+
if (!EDITOR_STYLES.includes(style)) return
|
|
4167
|
+
editorStylePreference = style
|
|
4168
|
+
try {
|
|
4169
|
+
if (typeof localStorage !== 'undefined') localStorage.setItem(EDITOR_STYLE_KEY, style)
|
|
4170
|
+
} catch {
|
|
4171
|
+
/* private mode keeps the in-memory choice */
|
|
4172
|
+
}
|
|
4173
|
+
for (const listener of [...editorStyleListeners]) {
|
|
4174
|
+
try {
|
|
4175
|
+
listener()
|
|
4176
|
+
} catch {}
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
|
|
4180
|
+
/**
|
|
4181
|
+
* The component row's configure page (`plugins.row.config`), the same slot
|
|
4182
|
+
* dsh-delete-turn uses. The manager's summary asks for a one-liner; the page
|
|
4183
|
+
* itself renders the three-way picker.
|
|
4184
|
+
*/
|
|
4185
|
+
function EditorStyleOption({ view, t }) {
|
|
4186
|
+
const tr = typeof t === 'function' ? t : (key) => key
|
|
4187
|
+
const [style, setStyle] = react.useState(editorStylePreference)
|
|
4188
|
+
const [placement, setPlacement] = react.useState(editorPlacementPreference)
|
|
4189
|
+
const [heightMode, setHeightMode] = react.useState(editorHeightPreference)
|
|
4190
|
+
react.useEffect(() => subscribeEditorStyle(() => setStyle(editorStylePreference)), [])
|
|
4191
|
+
react.useEffect(() => subscribeEditorPlacement(() => setPlacement(editorPlacementPreference)), [])
|
|
4192
|
+
react.useEffect(() => subscribeEditorHeight(() => setHeightMode(editorHeightPreference)), [])
|
|
4193
|
+
const summaryKey = style === 'bubble' ? 'config.summary.bubble' : style === 'center' ? 'config.summary.center' : 'config.summary.frame'
|
|
4194
|
+
if (view === 'summary') return `${tr(summaryKey)} · ${tr(placement === 'inline' ? 'config.summary.inline' : placement === 'legacy' ? 'config.summary.legacy' : 'config.summary.overlay')} · ${tr(heightMode === 'content' ? 'config.summary.height.content' : heightMode === 'fill' ? 'config.summary.height.fill' : 'config.summary.height.max')}`
|
|
4195
|
+
const hintKey = style === 'bubble' ? 'config.hint.bubble' : style === 'center' ? 'config.hint.center' : 'config.hint.frame'
|
|
4196
|
+
const placementHint = placement === 'inline' ? 'config.hint.inline' : placement === 'legacy' ? 'config.hint.legacy' : 'config.hint.overlay'
|
|
4197
|
+
const heightHint = heightMode === 'content' ? 'config.hint.height.content' : heightMode === 'fill' ? 'config.hint.height.fill' : 'config.hint.height.max'
|
|
4198
|
+
const choices = [
|
|
4199
|
+
['frame', 'style.frame'],
|
|
4200
|
+
['bubble', 'style.bubble'],
|
|
4201
|
+
['center', 'style.center'],
|
|
4202
|
+
]
|
|
4203
|
+
return jsxs('div', {
|
|
4204
|
+
className: 'dshet-config',
|
|
4205
|
+
children: [
|
|
4206
|
+
jsxs('label', {
|
|
4207
|
+
className: 'dshet-config-row',
|
|
4208
|
+
children: [
|
|
4209
|
+
jsx('span', { children: tr('config.style') }),
|
|
4210
|
+
jsx('select', {
|
|
4211
|
+
className: 'dshet-select',
|
|
4212
|
+
value: style,
|
|
4213
|
+
onChange: (event) => setEditorStylePreference(event.target.value),
|
|
4214
|
+
children: choices.map(([value, key]) => jsx('option', { value, children: tr(key) }, value)),
|
|
4215
|
+
}),
|
|
4216
|
+
],
|
|
4217
|
+
}),
|
|
4218
|
+
jsx('p', { className: 'dshet-config-hint', children: tr(hintKey) }),
|
|
4219
|
+
jsxs('label', {
|
|
4220
|
+
className: 'dshet-config-row',
|
|
4221
|
+
children: [
|
|
4222
|
+
jsx('span', { children: tr('config.placement') }),
|
|
4223
|
+
jsx('select', {
|
|
4224
|
+
className: 'dshet-select',
|
|
4225
|
+
value: placement,
|
|
4226
|
+
onChange: (event) => setEditorPlacementPreference(event.target.value),
|
|
4227
|
+
children: [
|
|
4228
|
+
jsx('option', { value: 'overlay', children: tr('placement.overlay') }),
|
|
4229
|
+
jsx('option', { value: 'inline', children: tr('placement.inline') }),
|
|
4230
|
+
jsx('option', { value: 'legacy', children: tr('placement.legacy') }),
|
|
4231
|
+
],
|
|
4232
|
+
}),
|
|
4233
|
+
],
|
|
4234
|
+
}),
|
|
4235
|
+
jsx('p', { className: 'dshet-config-hint', children: tr(placementHint) }),
|
|
4236
|
+
jsxs('label', {
|
|
4237
|
+
className: 'dshet-config-row',
|
|
4238
|
+
children: [
|
|
4239
|
+
jsx('span', { children: tr('config.height') }),
|
|
4240
|
+
jsx('select', {
|
|
4241
|
+
className: 'dshet-select',
|
|
4242
|
+
value: heightMode,
|
|
4243
|
+
onChange: (event) => setEditorHeightPreference(event.target.value),
|
|
4244
|
+
children: [
|
|
4245
|
+
jsx('option', { value: 'max', children: tr('height.max') }),
|
|
4246
|
+
jsx('option', { value: 'content', children: tr('height.content') }),
|
|
4247
|
+
jsx('option', { value: 'fill', children: tr('height.fill') }),
|
|
4248
|
+
],
|
|
4249
|
+
}),
|
|
4250
|
+
],
|
|
4251
|
+
}),
|
|
4252
|
+
jsx('p', { className: 'dshet-config-hint', children: tr(heightHint) }),
|
|
4253
|
+
],
|
|
4254
|
+
})
|
|
4255
|
+
}
|
|
4256
|
+
|
|
3120
4257
|
// --- plugin ---------------------------------------------------------------
|
|
3121
4258
|
|
|
3122
4259
|
function apply(ctx) {
|
|
@@ -3171,6 +4308,20 @@ window.__ModuleLoader__.load({
|
|
|
3171
4308
|
),
|
|
3172
4309
|
)
|
|
3173
4310
|
|
|
4311
|
+
// The row's configure arrow in the plugin manager. Without this the
|
|
4312
|
+
// component page is a read-only overview (the card cannot be opened); the
|
|
4313
|
+
// key is the manager's `<bundle package>#<row id>` contract.
|
|
4314
|
+
ctx.slots.inject('plugins.row.config', () =>
|
|
4315
|
+
ctx.slots.register(
|
|
4316
|
+
{
|
|
4317
|
+
name: 'plugins.row.config',
|
|
4318
|
+
key: 'dsh-edit-turn#dsh-edit-turn',
|
|
4319
|
+
locale: NS,
|
|
4320
|
+
},
|
|
4321
|
+
EditorStyleOption,
|
|
4322
|
+
),
|
|
4323
|
+
)
|
|
4324
|
+
|
|
3174
4325
|
ctx.slots.inject('conversation.input.overlay', () =>
|
|
3175
4326
|
ctx.slots.register(
|
|
3176
4327
|
{
|