dsh-plugin-session-console-sleepcat 0.17.4
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/LICENSE +21 -0
- package/README.md +76 -0
- package/lib/client.js +1344 -0
- package/lib/index.js +11 -0
- package/package.json +47 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,1344 @@
|
|
|
1
|
+
/* dsh-plugin-session-console — client half (v9).
|
|
2
|
+
*
|
|
3
|
+
* A small TOOLBAR living INSIDE the conversation level (top-right of the chat
|
|
4
|
+
* area), collapsed to a pill; clicking it expands a row of tools. The
|
|
5
|
+
* questions console is ONE tool; future tools append to the TOOLS list.
|
|
6
|
+
*
|
|
7
|
+
* - ⚙️ Settings is a real, extensible SETTINGS PANEL (own overlay entry), not
|
|
8
|
+
* an inline section: sections are data-friendly and ready to grow with
|
|
9
|
+
* future customization. Currently: auto-expand toggle, max page count, and
|
|
10
|
+
* "auto-collapse the work process after the model finishes answering"
|
|
11
|
+
* (DEFAULT ON — expand anytime by clicking 展开过程; the setting disables
|
|
12
|
+
* it). User toggles are remembered and exempt the turn from auto-collapse.
|
|
13
|
+
* - 📋 会话记录 tool: questions console panel (draggable, 📌 pin, Esc /
|
|
14
|
+
* outside click closes). Questions newest first; opening expands the window
|
|
15
|
+
* (settings.maxPages) so recent questions are visible and jumpable.
|
|
16
|
+
* Clicking a question or a second-level item materializes the message
|
|
17
|
+
* (expanding the window if needed), auto-expands a collapsed turn if the
|
|
18
|
+
* target row is hidden, then scrolls and highlights.
|
|
19
|
+
* Second-level detail: per-question "AI 提问 / 用户回答" timeline — ask
|
|
20
|
+
* items parse the ask_user tool args (questions with options) and pair
|
|
21
|
+
* them with the user's answer; hovering an item shows a detailed tooltip.
|
|
22
|
+
* - Per-question collapse: "收起过程/展开过程" injected next to the copy
|
|
23
|
+
* button on every user question row; hides everything between the question
|
|
24
|
+
* and the turn's final answer, plus reasoning inside the final answer.
|
|
25
|
+
*
|
|
26
|
+
* Verified against installed sources:
|
|
27
|
+
* - shell.overlay = { kind: 'list', scope: 'root' }, click-through until the
|
|
28
|
+
* entry opts into pointer events.
|
|
29
|
+
* - SessionFace = ISession & ObservableSnapshot<ConversationSnapshot>;
|
|
30
|
+
* loadOlder() pages one earlier window page; snapshot.hasMore and
|
|
31
|
+
* snapshot.nodes[0].seq reflect the window head.
|
|
32
|
+
* - Chat DOM: flow rows carry data-chat-anchor-key (node.key) and
|
|
33
|
+
* data-chat-flow-kind; user rows render MessageIconActions whose copy
|
|
34
|
+
* button has a localized aria-label (copy/复制); reasoning rows carry
|
|
35
|
+
* data-variant="think". Tool cards anchor on the tool/CALL event seq, so
|
|
36
|
+
* tool-result jumps match by callId first.
|
|
37
|
+
*/
|
|
38
|
+
window.__ModuleLoader__.load({
|
|
39
|
+
id: "dsh-plugin-session-console-sleepcat",
|
|
40
|
+
factory: (require) => {
|
|
41
|
+
var module = { exports: {} };
|
|
42
|
+
var exports = module.exports;
|
|
43
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
44
|
+
|
|
45
|
+
var react = require("react");
|
|
46
|
+
var jsxRuntime = require("react/jsx-runtime");
|
|
47
|
+
|
|
48
|
+
var inject = ["slots"];
|
|
49
|
+
|
|
50
|
+
// ------------------------------------------ shared UI state (module-local)
|
|
51
|
+
function loadSettings() {
|
|
52
|
+
try {
|
|
53
|
+
var raw = window.localStorage.getItem("dsh-sc.settings");
|
|
54
|
+
if (raw) {
|
|
55
|
+
var s = JSON.parse(raw);
|
|
56
|
+
return {
|
|
57
|
+
autoExpand: s.autoExpand !== false,
|
|
58
|
+
maxPages: typeof s.maxPages === "number" ? Math.max(1, Math.min(12, Math.round(s.maxPages))) : 6,
|
|
59
|
+
autoCollapse: s.autoCollapse !== false,
|
|
60
|
+
maxQuestions: typeof s.maxQuestions === "number" ? Math.max(10, Math.min(500, Math.round(s.maxQuestions))) : 50
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
} catch (e) { /* ignore */ }
|
|
64
|
+
return { autoExpand: true, maxPages: 6, autoCollapse: true, maxQuestions: 50 };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function loadPos() {
|
|
68
|
+
try {
|
|
69
|
+
var raw = window.localStorage.getItem("dsh-sc.pos");
|
|
70
|
+
if (raw) {
|
|
71
|
+
var p = JSON.parse(raw);
|
|
72
|
+
if (typeof p.x === "number" && typeof p.y === "number") return { x: p.x, y: p.y };
|
|
73
|
+
}
|
|
74
|
+
} catch (e) { /* ignore */ }
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
var ui = {
|
|
79
|
+
open: false, // questions panel
|
|
80
|
+
pinned: false, // questions panel pin
|
|
81
|
+
settingsOpen: false, // settings panel
|
|
82
|
+
railCollapsed: false, // tool rail collapsed to a thin strip
|
|
83
|
+
railLeft: 300, // tool rail left offset (follows the DSH sidebar)
|
|
84
|
+
railTop: 64, // tool rail top (conversation header bottom line)
|
|
85
|
+
railBottom: 600, // tool rail bottom (chat area bottom)
|
|
86
|
+
pos: loadPos(),
|
|
87
|
+
settings: loadSettings(),
|
|
88
|
+
listeners: new Set()
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
function setUi(patch) {
|
|
92
|
+
for (var k in patch) ui[k] = patch[k];
|
|
93
|
+
ui.listeners.forEach(function (fn) { fn(); });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function persistPos() {
|
|
97
|
+
try {
|
|
98
|
+
if (ui.pos !== null) window.localStorage.setItem("dsh-sc.pos", JSON.stringify(ui.pos));
|
|
99
|
+
} catch (e) { /* ignore */ }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function persistSettings() {
|
|
103
|
+
try {
|
|
104
|
+
window.localStorage.setItem("dsh-sc.settings", JSON.stringify({
|
|
105
|
+
autoExpand: ui.settings.autoExpand,
|
|
106
|
+
maxPages: ui.settings.maxPages,
|
|
107
|
+
autoCollapse: ui.settings.autoCollapse,
|
|
108
|
+
maxQuestions: ui.settings.maxQuestions
|
|
109
|
+
}));
|
|
110
|
+
} catch (e) { /* ignore */ }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** "M月D日 HH:mm" — no seconds. */
|
|
114
|
+
function formatTime(ms) {
|
|
115
|
+
if (typeof ms !== "number" || isNaN(ms)) return "";
|
|
116
|
+
var d = new Date(ms);
|
|
117
|
+
var hh = String(d.getHours());
|
|
118
|
+
var mm = String(d.getMinutes());
|
|
119
|
+
if (hh.length < 2) hh = "0" + hh;
|
|
120
|
+
if (mm.length < 2) mm = "0" + mm;
|
|
121
|
+
return (d.getMonth() + 1) + "月" + d.getDate() + "日 " + hh + ":" + mm;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Subscribe to the shared UI state. */
|
|
125
|
+
function useUi() {
|
|
126
|
+
var pair = react.useState(ui);
|
|
127
|
+
react.useEffect(function () {
|
|
128
|
+
function on() { pair[1](Object.assign({}, ui)); }
|
|
129
|
+
ui.listeners.add(on);
|
|
130
|
+
on();
|
|
131
|
+
return function () { ui.listeners.delete(on); };
|
|
132
|
+
}, []);
|
|
133
|
+
return pair[0];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------- store
|
|
137
|
+
function createStore() {
|
|
138
|
+
var runtime = require("@deepseek-ai/dsh-client-runtime/client");
|
|
139
|
+
return runtime.defineStore({
|
|
140
|
+
init: function () {
|
|
141
|
+
return {
|
|
142
|
+
currentId: undefined,
|
|
143
|
+
snapshot: undefined,
|
|
144
|
+
session: undefined,
|
|
145
|
+
error: undefined
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
actions: {
|
|
149
|
+
setSession: function (d, currentId, snapshot, session) {
|
|
150
|
+
d.currentId = currentId;
|
|
151
|
+
d.snapshot = snapshot;
|
|
152
|
+
d.session = session;
|
|
153
|
+
d.error = undefined;
|
|
154
|
+
},
|
|
155
|
+
setError: function (d, error) { d.error = String(error); }
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// --------------------------------------------------------- text helpers
|
|
161
|
+
function blockText(block) {
|
|
162
|
+
if (block == null) return "";
|
|
163
|
+
if (typeof block.text === "string") return block.text;
|
|
164
|
+
var t = block.type;
|
|
165
|
+
if (t === "image") return "[image]";
|
|
166
|
+
if (t === "tool-call" || t === "tool") return "[tool call]";
|
|
167
|
+
return "[" + String(t) + "]";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function contentText(content) {
|
|
171
|
+
if (!content || !content.length) return "";
|
|
172
|
+
var out = [];
|
|
173
|
+
for (var i = 0; i < content.length; i++) {
|
|
174
|
+
var t = blockText(content[i]);
|
|
175
|
+
if (t) out.push(t);
|
|
176
|
+
}
|
|
177
|
+
return out.join(" ");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function preview(text) {
|
|
181
|
+
var line = String(text || "").replace(/\s+/g, " ").trim();
|
|
182
|
+
if (line.length > 140) line = line.slice(0, 140) + "…";
|
|
183
|
+
return line;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Parse ask_user tool call args into a readable question list. */
|
|
187
|
+
function optionLabel(o) {
|
|
188
|
+
if (typeof o === "string") return o;
|
|
189
|
+
if (o && typeof o.label === "string") return o.label;
|
|
190
|
+
return "";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function optionDetail(o) {
|
|
194
|
+
if (typeof o === "string") return o;
|
|
195
|
+
if (o) {
|
|
196
|
+
var label = typeof o.label === "string" ? o.label : "";
|
|
197
|
+
var desc = typeof o.description === "string" && o.description !== "" ? o.description : "";
|
|
198
|
+
return desc ? label + "(" + desc + ")" : label;
|
|
199
|
+
}
|
|
200
|
+
return "";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function parseAskQuestions(argsRaw) {
|
|
204
|
+
var out = [];
|
|
205
|
+
if (typeof argsRaw !== "string" || argsRaw === "") return out;
|
|
206
|
+
try {
|
|
207
|
+
var parsed = JSON.parse(argsRaw);
|
|
208
|
+
var qs = parsed && Array.isArray(parsed.questions) ? parsed.questions
|
|
209
|
+
: (Array.isArray(parsed) ? parsed : []);
|
|
210
|
+
for (var i = 0; i < qs.length; i++) {
|
|
211
|
+
var q = qs[i];
|
|
212
|
+
if (q && typeof q.question === "string" && q.question !== "") {
|
|
213
|
+
out.push({
|
|
214
|
+
question: q.question,
|
|
215
|
+
options: Array.isArray(q.options) ? q.options : []
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch (e) { /* not JSON — leave empty */ }
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Parse the ask_user tool RESULT (raw JSON) into the user's readable
|
|
224
|
+
* answer: selected option labels + custom text. */
|
|
225
|
+
function parseAskAnswers(content) {
|
|
226
|
+
var text = contentText(content).trim();
|
|
227
|
+
var start = text.indexOf("{");
|
|
228
|
+
var end = text.lastIndexOf("}");
|
|
229
|
+
if (start >= 0 && end > start) {
|
|
230
|
+
try {
|
|
231
|
+
var parsed = JSON.parse(text.slice(start, end + 1));
|
|
232
|
+
var answers = parsed && Array.isArray(parsed.answers) ? parsed.answers : [];
|
|
233
|
+
var parts = [];
|
|
234
|
+
for (var i = 0; i < answers.length; i++) {
|
|
235
|
+
var a = answers[i];
|
|
236
|
+
var bits = [];
|
|
237
|
+
if (Array.isArray(a.selected)) {
|
|
238
|
+
for (var s = 0; s < a.selected.length; s++) bits.push(String(a.selected[s]));
|
|
239
|
+
}
|
|
240
|
+
if (typeof a.custom === "string" && a.custom !== "") bits.push(a.custom);
|
|
241
|
+
if (bits.length > 0) parts.push(bits.join(";"));
|
|
242
|
+
}
|
|
243
|
+
if (parts.length > 0) return parts.join(" / ");
|
|
244
|
+
} catch (e) { /* fall through */ }
|
|
245
|
+
}
|
|
246
|
+
return text.replace(/^"|"$/g, "").trim();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function questionsPreview(questions) {
|
|
250
|
+
var parts = [];
|
|
251
|
+
for (var i = 0; i < questions.length; i++) parts.push(questions[i].question);
|
|
252
|
+
return preview(parts.join(" / "));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ------------------------------------------------------ sessions wiring
|
|
256
|
+
function wireSessions(sessions, actions) {
|
|
257
|
+
var unsub = null;
|
|
258
|
+
function release() { if (unsub) { unsub(); unsub = null; } }
|
|
259
|
+
function follow() {
|
|
260
|
+
var list = sessions.list.getSnapshot();
|
|
261
|
+
var id = list.current;
|
|
262
|
+
release();
|
|
263
|
+
if (id !== undefined) {
|
|
264
|
+
var binding = sessions.binding(id);
|
|
265
|
+
if (binding !== undefined) {
|
|
266
|
+
latestSnapshot = binding.session.getSnapshot();
|
|
267
|
+
actions.setSession(id, latestSnapshot, binding.session);
|
|
268
|
+
unsub = binding.session.subscribe(function () {
|
|
269
|
+
latestSnapshot = binding.session.getSnapshot();
|
|
270
|
+
actions.setSession(id, latestSnapshot, binding.session);
|
|
271
|
+
});
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
actions.setSession(undefined, undefined, undefined);
|
|
276
|
+
}
|
|
277
|
+
var offList = sessions.list.subscribe(follow);
|
|
278
|
+
follow();
|
|
279
|
+
return function () { offList(); release(); };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ------------------------------------------------------------- jumping
|
|
283
|
+
function legacyNodesOf(snapshot) {
|
|
284
|
+
if (!snapshot) return [];
|
|
285
|
+
return snapshot.nodes ||
|
|
286
|
+
(snapshot.chat && snapshot.chat.legacy && snapshot.chat.legacy.nodes) || [];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function legacyNodeBySeq(snapshot, seq) {
|
|
290
|
+
var nodes = legacyNodesOf(snapshot);
|
|
291
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
292
|
+
if (nodes[i].seq === seq) return nodes[i];
|
|
293
|
+
}
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Map a legacy node to its chat row key. Tool cards anchor on the
|
|
298
|
+
* tool/CALL event seq, so tool-result jumps match by callId first, then
|
|
299
|
+
* fall back to the nearest preceding tool-call anchor. */
|
|
300
|
+
function chatKeyForNode(snapshot, node) {
|
|
301
|
+
if (!snapshot || !snapshot.chat || !snapshot.chat.nodes) return undefined;
|
|
302
|
+
var nodes = snapshot.chat.nodes.values();
|
|
303
|
+
if (node && node.kind === "tool-result" && node.callId) {
|
|
304
|
+
var best = undefined;
|
|
305
|
+
var bestSeq = -Infinity;
|
|
306
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
307
|
+
var n = nodes[i];
|
|
308
|
+
if (n.kind !== "tool-call" && n.kind !== "tool") continue;
|
|
309
|
+
var root = n.data && n.data.root;
|
|
310
|
+
if (root && root.callId === node.callId) return n.key;
|
|
311
|
+
if (typeof n.anchorSeq === "number" && n.anchorSeq < node.seq && n.anchorSeq > bestSeq) {
|
|
312
|
+
best = n.key;
|
|
313
|
+
bestSeq = n.anchorSeq;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return best;
|
|
317
|
+
}
|
|
318
|
+
var target = node ? node.seq : undefined;
|
|
319
|
+
for (var j = 0; j < nodes.length; j++) {
|
|
320
|
+
if (nodes[j].anchorSeq === target) return nodes[j].key;
|
|
321
|
+
}
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function rowElementForNode(snapshot, node) {
|
|
326
|
+
var key = chatKeyForNode(snapshot, node);
|
|
327
|
+
if (key === undefined) return null;
|
|
328
|
+
var escaped = window.CSS && typeof window.CSS.escape === "function" ? window.CSS.escape(key) : key;
|
|
329
|
+
return document.querySelector('[data-chat-anchor-key="' + escaped + '"]');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function rowElementForSeq(snapshot, seq) {
|
|
333
|
+
return rowElementForNode(snapshot, legacyNodeBySeq(snapshot, seq));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** If the row is hidden because its turn is collapsed, expand that turn
|
|
337
|
+
* first so the jump lands on a visible row. Walks back to the owning
|
|
338
|
+
* QUESTION row (skipping steering answers and any tool rows in between),
|
|
339
|
+
* since the collapse state is keyed by the question seq. */
|
|
340
|
+
function ensureRowVisible(snapshot, el) {
|
|
341
|
+
if (el === null || el.style.display !== "none") return;
|
|
342
|
+
var keySeq = {};
|
|
343
|
+
if (snapshot && snapshot.chat && snapshot.chat.nodes) {
|
|
344
|
+
var nodes = snapshot.chat.nodes.values();
|
|
345
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
346
|
+
var n = nodes[i];
|
|
347
|
+
if (typeof n.anchorSeq === "number" && typeof n.key === "string") keySeq[n.key] = n.anchorSeq;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
var cursor = el.previousElementSibling;
|
|
351
|
+
while (cursor !== null) {
|
|
352
|
+
var kind = cursor.getAttribute ? cursor.getAttribute("data-chat-flow-kind") : null;
|
|
353
|
+
if (kind === "user") {
|
|
354
|
+
var key = cursor.getAttribute ? cursor.getAttribute("data-chat-anchor-key") : null;
|
|
355
|
+
var seq = key !== null ? keySeq[key] : undefined;
|
|
356
|
+
if (typeof seq === "number" && collapsedUserSeqs.has(seq)) {
|
|
357
|
+
collapsedUserSeqs.delete(seq);
|
|
358
|
+
applyBetween(cursor, turnEndRow(cursor), false);
|
|
359
|
+
var btns = document.querySelectorAll('[data-sc-seq="' + seq + '"]');
|
|
360
|
+
for (var b = 0; b < btns.length; b++) btns[b].textContent = "收起过程";
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
cursor = cursor.previousElementSibling;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function jumpToSeq(snapshot, seq) {
|
|
369
|
+
var el = rowElementForSeq(snapshot, seq);
|
|
370
|
+
if (el === null) return false;
|
|
371
|
+
ensureRowVisible(snapshot, el);
|
|
372
|
+
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
373
|
+
window.setTimeout(function () {
|
|
374
|
+
var prev = el.style.outline;
|
|
375
|
+
el.style.outline = "2px solid #2563eb";
|
|
376
|
+
el.style.outlineOffset = "-2px";
|
|
377
|
+
window.setTimeout(function () { el.style.outline = prev; }, 1600);
|
|
378
|
+
}, 350);
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function delay(ms) {
|
|
383
|
+
return new Promise(function (resolve) { window.setTimeout(resolve, ms); });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Jump to a message, expanding the window (bounded) until it materializes. */
|
|
387
|
+
async function jumpWithExpand(session, seq, maxPages, onNotice) {
|
|
388
|
+
if (!session) { onNotice("会话不可用"); return; }
|
|
389
|
+
var snap = session.getSnapshot();
|
|
390
|
+
if (rowElementForSeq(snap, seq) !== null) {
|
|
391
|
+
if (!jumpToSeq(snap, seq)) onNotice("对话区尚未渲染该位置");
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
var found = false;
|
|
395
|
+
for (var i = 0; i < maxPages; i++) {
|
|
396
|
+
var s = session.getSnapshot();
|
|
397
|
+
if (!s || !s.hasMore) break;
|
|
398
|
+
var head = s.nodes && s.nodes.length > 0 ? s.nodes[0].seq : Infinity;
|
|
399
|
+
if (head <= seq) break;
|
|
400
|
+
try {
|
|
401
|
+
await session.loadOlder();
|
|
402
|
+
} catch (e) {
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
for (var w = 0; w < 10; w++) {
|
|
406
|
+
await delay(100);
|
|
407
|
+
var s2 = session.getSnapshot();
|
|
408
|
+
if (rowElementForSeq(s2, seq) !== null) { found = true; break; }
|
|
409
|
+
var head2 = s2.nodes && s2.nodes.length > 0 ? s2.nodes[0].seq : Infinity;
|
|
410
|
+
if (head2 <= seq) break;
|
|
411
|
+
}
|
|
412
|
+
if (found) break;
|
|
413
|
+
}
|
|
414
|
+
var finalSnap = session.getSnapshot();
|
|
415
|
+
if (rowElementForSeq(finalSnap, seq) !== null && jumpToSeq(finalSnap, seq)) {
|
|
416
|
+
onNotice("已加载更早内容并跳转");
|
|
417
|
+
} else {
|
|
418
|
+
onNotice("该消息不在当前加载窗口内,无法跳转");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// --------------------------------------- per-question collapse (DOM side)
|
|
423
|
+
/** User question seqs whose work process is currently collapsed. */
|
|
424
|
+
var collapsedUserSeqs = new Set();
|
|
425
|
+
/** User question seqs the user toggled manually — exempt from auto-collapse. */
|
|
426
|
+
var manualSeqs = new Set();
|
|
427
|
+
/** Latest session snapshot (module-level mirror for rail-level actions). */
|
|
428
|
+
var latestSnapshot = null;
|
|
429
|
+
|
|
430
|
+
/** Re-sync every injected "收起过程/展开过程" label with the real state. */
|
|
431
|
+
function syncTurnButtonLabels() {
|
|
432
|
+
var btns = document.querySelectorAll("[data-sc-seq]");
|
|
433
|
+
for (var b = 0; b < btns.length; b++) {
|
|
434
|
+
var s = Number(btns[b].dataset.scSeq);
|
|
435
|
+
btns[b].textContent = collapsedUserSeqs.has(s) ? "展开过程" : "收起过程";
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Collapse every question's working process in the current window. */
|
|
440
|
+
function collapseAllTurns(snapshot) {
|
|
441
|
+
if (!snapshot) return 0;
|
|
442
|
+
var nodes = legacyNodesOf(snapshot);
|
|
443
|
+
var n = 0;
|
|
444
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
445
|
+
var node = nodes[i];
|
|
446
|
+
if (node.kind !== "user" && node.kind !== "steering") continue;
|
|
447
|
+
var row = rowElementForSeq(snapshot, node.seq);
|
|
448
|
+
if (row === null) continue;
|
|
449
|
+
collapsedUserSeqs.add(node.seq);
|
|
450
|
+
n += applyBetween(row, turnEndRow(row), true);
|
|
451
|
+
}
|
|
452
|
+
syncTurnButtonLabels();
|
|
453
|
+
return n;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function turnEndRow(userRow) {
|
|
457
|
+
var asRow = null;
|
|
458
|
+
var el = userRow.nextElementSibling;
|
|
459
|
+
while (el !== null) {
|
|
460
|
+
if (el.nodeType !== 1) { el = el.nextElementSibling; continue; }
|
|
461
|
+
var kind = el.getAttribute ? el.getAttribute("data-chat-flow-kind") : null;
|
|
462
|
+
if (kind === "user" || kind === "steering") break;
|
|
463
|
+
if (kind === "assistant-step" || kind === "assistant") asRow = el;
|
|
464
|
+
el = el.nextElementSibling;
|
|
465
|
+
}
|
|
466
|
+
return asRow;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function applyBetween(userRow, asRow, collapse) {
|
|
470
|
+
var want = collapse ? "none" : "";
|
|
471
|
+
var count = 0;
|
|
472
|
+
var el = userRow.nextElementSibling;
|
|
473
|
+
while (el !== null && el !== asRow) {
|
|
474
|
+
if (el.nodeType === 1 && typeof el.matches === "function" && el.matches("[data-chat-anchor-key]")) {
|
|
475
|
+
if (el.style.display !== want) { el.style.display = want; count++; }
|
|
476
|
+
}
|
|
477
|
+
el = el.nextElementSibling;
|
|
478
|
+
}
|
|
479
|
+
if (asRow !== null) {
|
|
480
|
+
var thinks = asRow.querySelectorAll('[data-variant="think"]');
|
|
481
|
+
for (var t = 0; t < thinks.length; t++) {
|
|
482
|
+
if (thinks[t].style.display !== want) { thinks[t].style.display = want; count++; }
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return count;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Inject a "收起过程/展开过程" button into every user question row's
|
|
489
|
+
* actions row (beside the copy button). Idempotent per row. */
|
|
490
|
+
function injectTurnButtons(snapshot) {
|
|
491
|
+
if (!snapshot || !snapshot.chat || !snapshot.chat.nodes) return;
|
|
492
|
+
var nodes = snapshot.chat.nodes.values();
|
|
493
|
+
var keySeq = {};
|
|
494
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
495
|
+
var n = nodes[i];
|
|
496
|
+
if (typeof n.anchorSeq === "number" && typeof n.key === "string") keySeq[n.key] = n.anchorSeq;
|
|
497
|
+
}
|
|
498
|
+
var rows = document.querySelectorAll('[data-chat-flow-kind="user"], [data-chat-flow-kind="steering"]');
|
|
499
|
+
for (var r = 0; r < rows.length; r++) {
|
|
500
|
+
var row = rows[r];
|
|
501
|
+
if (row.dataset && row.dataset.scConsoleBtn) continue;
|
|
502
|
+
var key = row.getAttribute ? row.getAttribute("data-chat-anchor-key") : null;
|
|
503
|
+
var seq = key !== null ? keySeq[key] : undefined;
|
|
504
|
+
if (typeof seq !== "number") continue;
|
|
505
|
+
var copy = null;
|
|
506
|
+
var buttons = row.querySelectorAll("button");
|
|
507
|
+
for (var b = 0; b < buttons.length; b++) {
|
|
508
|
+
var al = buttons[b].getAttribute ? (buttons[b].getAttribute("aria-label") || "") : "";
|
|
509
|
+
if (/copy|复制/i.test(al)) { copy = buttons[b]; break; }
|
|
510
|
+
}
|
|
511
|
+
var holder = copy !== null && copy.parentElement ? copy.parentElement : null;
|
|
512
|
+
if (holder === null) continue;
|
|
513
|
+
if (row.dataset) row.dataset.scConsoleBtn = "1";
|
|
514
|
+
var btn = document.createElement("button");
|
|
515
|
+
btn.type = "button";
|
|
516
|
+
btn.dataset.scSeq = String(seq);
|
|
517
|
+
btn.style.cssText = "border:none;background:transparent;cursor:pointer;color:var(--dsw-alias-label-tertiary,#888);font-size:12px;padding:2px 4px;flex:none";
|
|
518
|
+
btn.textContent = collapsedUserSeqs.has(seq) ? "展开过程" : "收起过程";
|
|
519
|
+
btn.addEventListener("click", function () {
|
|
520
|
+
var s = Number(this.dataset.scSeq);
|
|
521
|
+
var userRow = this.closest('[data-chat-anchor-key]');
|
|
522
|
+
if (!userRow) return;
|
|
523
|
+
var asRow = turnEndRow(userRow);
|
|
524
|
+
manualSeqs.add(s);
|
|
525
|
+
if (collapsedUserSeqs.has(s)) {
|
|
526
|
+
collapsedUserSeqs.delete(s);
|
|
527
|
+
applyBetween(userRow, asRow, false);
|
|
528
|
+
this.textContent = "收起过程";
|
|
529
|
+
} else {
|
|
530
|
+
collapsedUserSeqs.add(s);
|
|
531
|
+
applyBetween(userRow, asRow, true);
|
|
532
|
+
this.textContent = "展开过程";
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
holder.appendChild(btn);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ------------------------------------------------------------- component
|
|
540
|
+
function countQuestions(snapshot) {
|
|
541
|
+
var nodes = legacyNodesOf(snapshot);
|
|
542
|
+
var n = 0;
|
|
543
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
544
|
+
if (nodes[i].kind === "user" || nodes[i].kind === "steering") n++;
|
|
545
|
+
}
|
|
546
|
+
return n;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Second-level list of one question's turn: AI ask-user questions
|
|
550
|
+
* (parsed args + the user's answer) and steering answers. */
|
|
551
|
+
function questionDetail(snapshot, qSeq) {
|
|
552
|
+
var asks = [];
|
|
553
|
+
var answers = [];
|
|
554
|
+
if (!snapshot) return { items: [] };
|
|
555
|
+
var nodes = legacyNodesOf(snapshot);
|
|
556
|
+
var nextUser = Infinity;
|
|
557
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
558
|
+
var n = nodes[i];
|
|
559
|
+
if (n.kind === "user" && n.seq > qSeq && n.seq < nextUser) nextUser = n.seq;
|
|
560
|
+
}
|
|
561
|
+
for (var j = 0; j < nodes.length; j++) {
|
|
562
|
+
var m = nodes[j];
|
|
563
|
+
if (m.seq <= qSeq || m.seq >= nextUser) continue;
|
|
564
|
+
if (m.kind === "tool-result") {
|
|
565
|
+
var name = m.call ? m.call.name : "";
|
|
566
|
+
if (name && /ask[_-]?user/i.test(name)) {
|
|
567
|
+
var questions = parseAskQuestions(m.call ? m.call.argsRaw : undefined);
|
|
568
|
+
var answer = parseAskAnswers(m.content);
|
|
569
|
+
asks.push({
|
|
570
|
+
seq: m.seq,
|
|
571
|
+
time: m.time,
|
|
572
|
+
questions: questions,
|
|
573
|
+
answer: answer
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
} else if (m.kind === "steering") {
|
|
577
|
+
answers.push({ seq: m.seq, time: m.time, text: preview(contentText(m.content)) });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
var items = [];
|
|
581
|
+
for (var a = 0; a < asks.length; a++) {
|
|
582
|
+
items.push({
|
|
583
|
+
kind: "ask",
|
|
584
|
+
seq: asks[a].seq,
|
|
585
|
+
time: asks[a].time,
|
|
586
|
+
questions: asks[a].questions,
|
|
587
|
+
answer: asks[a].answer,
|
|
588
|
+
text: asks[a].questions.length > 0 ? questionsPreview(asks[a].questions) : "AI 提问"
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
for (var b = 0; b < answers.length; b++) {
|
|
592
|
+
items.push({ kind: "answer", seq: answers[b].seq, time: answers[b].time, text: answers[b].text });
|
|
593
|
+
}
|
|
594
|
+
items.sort(function (x, y) { return x.seq - y.seq; });
|
|
595
|
+
return { items: items };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** Tooltip lines for a second-level item. */
|
|
599
|
+
function detailLines(item) {
|
|
600
|
+
var lines = [];
|
|
601
|
+
if (item.kind === "ask") {
|
|
602
|
+
lines.push("— AI 提问 —");
|
|
603
|
+
if (item.questions && item.questions.length > 0) {
|
|
604
|
+
for (var i = 0; i < item.questions.length; i++) {
|
|
605
|
+
var q = item.questions[i];
|
|
606
|
+
var line = "Q" + (i + 1) + " " + q.question;
|
|
607
|
+
var opts = q.options.map(optionDetail).filter(function (x) { return x !== ""; });
|
|
608
|
+
if (opts.length > 0) line += "\n 选项:" + opts.join(" / ");
|
|
609
|
+
lines.push(line);
|
|
610
|
+
}
|
|
611
|
+
} else {
|
|
612
|
+
lines.push("(无文本)");
|
|
613
|
+
}
|
|
614
|
+
lines.push("— 用户回答 —");
|
|
615
|
+
lines.push(item.answer && item.answer !== "" ? item.answer : "(未回答)");
|
|
616
|
+
} else {
|
|
617
|
+
lines.push("— 用户回答 —");
|
|
618
|
+
lines.push(item.text || "(无文本)");
|
|
619
|
+
}
|
|
620
|
+
return lines;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ---- data-driven tool list (future tools append here) ----
|
|
624
|
+
// Monochrome line icons (currentColor), matching the official sidebar look.
|
|
625
|
+
function svgIcon(children) {
|
|
626
|
+
return react.createElement("svg", {
|
|
627
|
+
viewBox: "0 0 24 24", width: 16, height: 16,
|
|
628
|
+
fill: "none", stroke: "currentColor", strokeWidth: 2,
|
|
629
|
+
strokeLinecap: "round", strokeLinejoin: "round",
|
|
630
|
+
style: { display: "block", margin: "auto" }
|
|
631
|
+
}, children);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
var TOOLS = [
|
|
635
|
+
{
|
|
636
|
+
id: "questions",
|
|
637
|
+
label: "会话记录",
|
|
638
|
+
icon: svgIcon([
|
|
639
|
+
react.createElement("line", { key: "a", x1: 4, y1: 6, x2: 20, y2: 6 }),
|
|
640
|
+
react.createElement("line", { key: "b", x1: 4, y1: 12, x2: 20, y2: 12 }),
|
|
641
|
+
react.createElement("line", { key: "c", x1: 4, y1: 18, x2: 13, y2: 18 })
|
|
642
|
+
])
|
|
643
|
+
}
|
|
644
|
+
];
|
|
645
|
+
|
|
646
|
+
var SETTINGS_ICON = svgIcon([
|
|
647
|
+
react.createElement("line", { key: "a", x1: 4, y1: 7, x2: 20, y2: 7 }),
|
|
648
|
+
react.createElement("circle", { key: "ca", cx: 9, cy: 7, r: 2.2 }),
|
|
649
|
+
react.createElement("line", { key: "b", x1: 4, y1: 13, x2: 20, y2: 13 }),
|
|
650
|
+
react.createElement("circle", { key: "cb", cx: 15, cy: 13, r: 2.2 }),
|
|
651
|
+
react.createElement("line", { key: "c", x1: 4, y1: 19, x2: 20, y2: 19 }),
|
|
652
|
+
react.createElement("circle", { key: "cc", cx: 7, cy: 19, r: 2.2 })
|
|
653
|
+
]);
|
|
654
|
+
|
|
655
|
+
/** User-provided sleeping-cat icon (64x64 PNG, embedded as base64). */
|
|
656
|
+
var CAT_ICON_B64 = "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAONElEQVR42t1b6a8VxRLns/+CifGjCYkfRBYDiFGR5bqwKoo8IoKyKIKIBEQF3KOJsruAGMGFHVyAxBDiw7jgAg8EFQR3jRubwr3nTHfXm1/P1ExPT3fPnOuXlzc3nTPnzkyf7uqqX/2quqYLFQ5F5UNZ58pz3//qoRzjzsffJf9HlDZhNJl+Rsan7x5pNO7T/J9w3MdNee7h78q4R1X0L6zvkXXOTZkCkMk/VTP+bFo32s3Vmcz7yL6LTvTju09Zq1nVf52+pUsDmjUHZEtWWVoUEoQwJmEIv2RqIS2rM0ZZMRblEkDdVQkJxjaVqKCmSknLDs3JmUIQ1mREC+NQnrGZ2qN8GODr1HXuUzlhTd5YWSWsyStrgLYGmJrSqmnxuQ3m0iUA0cLqmpNTHiATnon51FUEvIuywDW0QJE1NuFA/5IGkMdWhWOgts2qgOqa57agRcWkQ4dINSn9VNYCqVzrVHxN8bgL91FIACLgOpTHLkVAiNJSP/EPuASbkQvspDW+EGCqVgUgnCquVGdcmY0N5NAmXjVprFgIaFsFcVkXBE3VEsaAkskrJQI4IUo/WPQ4rtUSAe9Qh+DUwYfIDYIq5gHKyQWEw4eLFkxAtYDidl/KoSWtrLp9jcme0wT4ovC7vxLoiJrqLypWRASEKFtc/dC1SgHU4QGtTL7OvbzKoVjDHgN1UgABJqiokZqAqJiIY2IqHzDYXhQ1SQhh+fFin0pF+l6lVHyvjFsjblHWpJQpc7QxIGpB7YW18kEq3CrtjSxvQHoyaJnTk3Z0KOL/iVhAUeG+oOOL78P9eM4fd9RpphBKXgAXGjUR1CVhlU32yJEjNGnSJFq3bl1BCNAIe9J//fUXHTx4kN544w1a9eKL9Nxzz9HKlStpw4YN9Mknn9Cff/5ZeCbRFo4pbFD1RbOV4TDbU8PooK79JqvCgzx27BhddtlleiIDBw6kV155JbHuKMp+af/+/fTYY4/RmDFjqK2tjW644Qa688476b777qN58+bR3Llzafr06TT2X2Ppmmuuoeuvv15fe/fddzOzglBhNkX32bTmEHWWCEU1MCC5lnCBXACDBw+m1157TZ8fPnyY+vTpQ7///js1m0166aWXaPjw4TR02DB69NFH6f3339caEDKFjo4O+vzzz2nJkiV044036v5XrFhBjUbD0KrORLG1vIB0AE8eqcEmMQBelWeffZZuvfVWfd7e3q4/n3jiCbruuuv0xG+77Tb66KOPShOOlTruoz3WkrNxw2eHbgBJHig/c+DAAZowYQJdfvnltGPHDsu0VA0M8AogcqB0Ym+JujXjQTVIpuwvUT/K1Pvjjz/Wq/3111+ng0pU/ptvvqGuXbvSF198UQDJBOXRV9PyPsZKai8BQTSS35Y5hgAfIIR777039RYm+FYBYAkEybIlUYnKOL788kuaM2eOttNhsVqz6idonUt51KhR9Omnn+pzmEIpsitMPuBq4/ukbOjF4JUH2OK3WePqaUIwHM6zNidOnKD58+fTxIkTtaSffvpp2rdvHx0/fpxmzJhBAwYMoMWLF9GPP/5orK4omAiO7du3U8+ePWnPnj2WayRnAlarvnKjeULXoQ05zwBwXnvttdn3RAiyVQHkD4DE4ABSw6ahbmvWrKEFCxbQiBEjqFevXvTUU09lQMRxtxAdpR9iIcD2e/ToQV999VXJK+ScIcpMJwzMiTrDfPj+6dOn0fjx4w0Bq4AJOPMBZQHAHa1atSpTN1ZhaEbuk5OBlIEmV13YL47XX39dC9A0I4ZBUyvg+//4448SgcoFkrs6aAs/Cw9RHK8IhsLefQGeLDqbNWuWPsdqQzBMTUFbE5QOcwO+n1f89ttvp+eff15rENyjqQ179+6lyZMnU//+/TURYq0oE6+ieST3JG730ksvzYTnz1VUJEWZ1sK2IVX46qRDmfL3cMjM7pFXmT8PHTpE/fr1o969e2tQxP959WBuQHVM/OzZs47Nj1DUGHuoFBgfeOABjVu5J3KF9FF1UpRt94477tD+vdyhL3IrssJnnnlGny9btkxzAgAWGpMcHDNnzqTRo0dn33MwjaykiC9520y1kei7777T4JwvmrACohogmCFx6uquuuoqjfxJh02LK3B+QGamA8oKCjxy5EjN/thv7969m+666y668MILMyHg+pAhQwpBT/Lbbr/tTaqoHHBhZps3b04XzUfuqJoK84SgBWvXrs06TMxAZATE5gdwd5jgrl27smsgSAiOwAhZK4ABV155pSZLOXrLiryk8oTXUcYPli9frvlJgi8NhxDrpMVVHq6OHTtWA1TZh5Pm6UxCQFOx0okwcnTnfmDboMVHjx7V3xctWqQ9TdEt+uOOYjZKlnaPTJd78803Z7jlTuxSOB/AQIgJDB06NCM7OL799lttawhhEanh+Pvvv+n888+nxx9/nM7E5++88046sWY2uXvuuYdmz56d9QPBwizKq9/CJqsWShFsf/nlF7r66qsz1ln2Vk43KCwByAylMUnE+Hx8+OGHdNNNN2mGaP7/7bff1qEt7Hz79m2FmABuD5SVhQFtAIVmV5j8nplllk5sKm+hUykWgBtEmH3mzJnOCqBZ4AN33303LY3D0XXr19PLL79cyvxA0rDrqVOnatpragtAFMLCakNL+Dh58qQWwKlTpyy0zjPJyV8oE1WcOH/++uuv2n3nXsWXgyzlBJO0eIL0iUuBf0ZnF1xwAb355pu0bds2+v777zO7xeTxwz///LP+YRbIli1b6JZbbinkB7BSjCGnYzfVFl8rkpbibrIqgWBxZ4pt/oMPPtAul48D+w9kpmmadzK/KFQhknNsRm6oNVjgtGnTdFbmySefpJ9++slBZ5PvL8ZpLTA5xOyI11nlIRSePH+Cu+/e/W8jgqxKe+foLaXKNAmehIEVB7QUGSXbDRbDbleFiKUmPEHk7IDe55xzjnaJQH6oF1YcqS9QW9BQrDgIDQ+GMcSM0sDz33vvPdq0aZNOnS1YMN/wAlVpOJF5Fu5z3LhxelHMUBtj5FQcKLs7TqnMCic3tbd3aJ/erVs3vfKQLlJTV1xxhQ5xcQ3prfPOOy+TOg+QVx9gBN+MvADsHhEm3B/Y4eHDRyw79QtASWGk2kmDbTECJG2eYIKnT582+hVVGyPuBAK7QtgpXCH7exxgbwA8E+zAGDnzwwAElzRo0CBtQuASHR0NR4pFenKNyb4A1BjehDXyhx9+0ONBIpVBkFcfQMwakQhL1t8YCeXORsZh7OrUA8B1DY0FAFeGH+fJbt26lS655BLNE0xfv3Tp0sIqIXprNtt1Q4ZHp8VSF4hEB9hbEnmW/fuDDz5Iffv2pdWrV2d9cl4CwAs8KCZGKFBAERSAKARFoKttbYM1z77//vs1nz9x4njmw9nfYxCwbZgD0uIXX3xxFt3V3Qgxj99++43Wx+4XqS9oGJIy7DnYCzE3QcIFmlhMivgCKGcwJCwBqMzXJmrdoLfeekvnCWACAMAEfBoF3gCsgMpfdNFFdO6552rvsXPnTi1EBFXgAzAnaM6ZM2c1FwCgAmy3xV4HGgMuD8xAqguqvnHjRoPYKL3qrFFgpMAoUOBCMkTJAJg6M0LSs59PmT2adojUGCc7k60rWeD+WB0ERqDHSHTAN4MNIivEDcHR8Ph/EOioGFynTJlCDz30kAZbeJti2i2ZOPcPoENuEnkETrWFM0GVTFAadTeyXLGB9JPI1Q48vnv37pormOwQ15kg2TwB5gDfzWkvaAQmUs4WU8HGzSAM9yOQQhoeJsEYlKfqq7bGneFw5NzvK6ewRaruUZblAdsD2H322WclW8fAMUBMwnRhvg3QJPVWTpjif7BzpOgAdDAxZn/J3kVEvtxGCyYQBcrbymkpBkgMAKgMH48kCAIfbJSwzXbmwLPABOANYhFEdzAhbJEx5eYNltzXVxVVlou2uoSlZld3lEtd4MZy96Y0EEEtwQgxaHxiAg8//DAtXLhQ7+u9+uqrOvcHdAdje+GFF3QwBVKFlUXwhGgOE0a6DHgAzCnoYrpjVS68rqptkK5YgDyFB3aGRnljcpFljPMD6P6feCMF2aSFCxdpISAfAIGAxaEBxID4jzzySLzJslgLBrvHwAkXhhS3xj01hKUECHlqGgsCcExOMRu0faohrAwwZaYR2OC0hdGpisBUxQFu9jiUsouvyDE+u8aozENqbo9LxzsFdo5OlJKqyaZFlJa+NNLsUHv6nXeWIw1wUSR04w1Qe+OkHCX6IsdmRXFH0b07mGCzZgGycGRqhQdIpaPs3edhXFWlrRRnVxVKCV80mOy4qEKZjLBW3wcmqgJ4XG982DxDOKpKqcVy+dCk7VR7rQoR4SmSiKyBi0CRtKjxLhJ5yua5NrhuYZR/48S1MVrDDUrP6tYpjhaVAOR+ccIepLLKZ6NOlfL8Ax7gK5gOkQxVqB9UTrsWjhchlONlCenIEItOvL1iV5e3VCQVSleJ8Jtlrnp+Z7W4CgCf9KTHo5oCcfWjXDxAtFAqW4dtuV5piyreFRDe0vwySNoErRlotpeTVQKQniLlcJ7e/9qMzz0qcscjVV7FHldcTEVnkcXUjZytw2jJs94qseLbXaFSdlXhNmUNk6KKFyKqzK9hTdIvAJVVlxQEYJfKMnApx2SVxxVGgTdO7TJ8X9RJng3SKgLEk24Y5+Zqt2fXVdr4eUMA/HDTMQkVUFHZAguriyHCk8r2PYtJdWSTq25NlwCqYmdV88WqOq/ZmsJseN5Kq/P2hygw2GxrzzFhVSJDSR9dyvl56YiyXNfJUccjHCUtrmZvx4lAn8qi0q4Xr0VN/i+Kr9zFz3eh/4tDel7lER4XnQvvv1YfIKQMyGk4AAAAAElFTkSuQmCC";
|
|
657
|
+
|
|
658
|
+
function sleepingCatIcon() {
|
|
659
|
+
return react.createElement("img", {
|
|
660
|
+
src: "data:image/png;base64," + CAT_ICON_B64,
|
|
661
|
+
alt: "展开工具栏",
|
|
662
|
+
width: 28, height: 28,
|
|
663
|
+
style: { display: "block", margin: "auto", pointerEvents: "none" }
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** Double-chevron-up icon: collapse everything. */
|
|
668
|
+
var COLLAPSE_ALL_ICON = svgIcon([
|
|
669
|
+
react.createElement("path", { key: "a", d: "M5 13 l7 -7 7 7" }),
|
|
670
|
+
react.createElement("path", { key: "b", d: "M5 19 l7 -7 7 7" })
|
|
671
|
+
]);
|
|
672
|
+
|
|
673
|
+
// ---- tool rail: EMBEDDED in the conversation interface — a flat, borderless
|
|
674
|
+
// narrow strip at the conversation's left edge (the chat content makes
|
|
675
|
+
// room via a margin rule). Collapsed: a flat small arrow at the middle
|
|
676
|
+
// of the conversation's left edge. ----
|
|
677
|
+
function ToolRail() {
|
|
678
|
+
var uiState = useUi();
|
|
679
|
+
|
|
680
|
+
// Esc + click-outside dismissal for the floating panels of this plugin.
|
|
681
|
+
react.useEffect(function () {
|
|
682
|
+
function onKey(e) {
|
|
683
|
+
if (e.key === "Escape") setUi({ open: false, settingsOpen: false });
|
|
684
|
+
}
|
|
685
|
+
function onPointerDown(e) {
|
|
686
|
+
var t = e.target;
|
|
687
|
+
if (t && t.closest && t.closest('[data-sc-rail],[data-sc-panel],[data-sc-settings]')) return;
|
|
688
|
+
var patch = { settingsOpen: false };
|
|
689
|
+
if (ui.open && !ui.pinned) patch.open = false;
|
|
690
|
+
setUi(patch);
|
|
691
|
+
}
|
|
692
|
+
window.addEventListener("keydown", onKey);
|
|
693
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
694
|
+
return function () {
|
|
695
|
+
window.removeEventListener("keydown", onKey);
|
|
696
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
697
|
+
};
|
|
698
|
+
}, []);
|
|
699
|
+
|
|
700
|
+
// Body attribute: lets the injected stylesheet shift the chat flow so the
|
|
701
|
+
// expanded rail is truly embedded (no message is covered by it).
|
|
702
|
+
react.useEffect(function () {
|
|
703
|
+
if (uiState.railCollapsed) delete document.body.dataset.scRailExpanded;
|
|
704
|
+
else document.body.dataset.scRailExpanded = "1";
|
|
705
|
+
}, [uiState.railCollapsed]);
|
|
706
|
+
|
|
707
|
+
// Track the DSH sidebar's right edge and the chat scrollport bounds so
|
|
708
|
+
// the rail stays glued: top = the line under the "对话/轨迹" tabs,
|
|
709
|
+
// bottom = the chat area's bottom, left = the sidebar's right edge.
|
|
710
|
+
// A ResizeObserver follows the sidebar's collapse/expand animation frame
|
|
711
|
+
// by frame (no more 800ms polling jerks); a slow interval remains as a
|
|
712
|
+
// fallback for anything the observer cannot see.
|
|
713
|
+
react.useEffect(function () {
|
|
714
|
+
var ro = null;
|
|
715
|
+
var timer = null;
|
|
716
|
+
|
|
717
|
+
function measure() {
|
|
718
|
+
var layer = document.querySelector('[data-shell-overlay]');
|
|
719
|
+
var frame = layer !== null ? layer.parentElement : null;
|
|
720
|
+
var col = frame !== null ? frame.firstElementChild : null;
|
|
721
|
+
var left = 300;
|
|
722
|
+
if (col !== null && typeof col.getBoundingClientRect === "function") {
|
|
723
|
+
left = col.getBoundingClientRect().right;
|
|
724
|
+
}
|
|
725
|
+
var sp = document.querySelector('[data-conversation-scroll]');
|
|
726
|
+
var top = 64;
|
|
727
|
+
var bottom = 600;
|
|
728
|
+
if (sp !== null && typeof sp.getBoundingClientRect === "function") {
|
|
729
|
+
var r = sp.getBoundingClientRect();
|
|
730
|
+
bottom = r.bottom;
|
|
731
|
+
// Kiss the line under the "对话/轨迹" view-tabs bar: locate the tab
|
|
732
|
+
// button by its exact label and take its row's bottom edge; the
|
|
733
|
+
// scrollport's top edge is the fallback.
|
|
734
|
+
var spTop = r.top;
|
|
735
|
+
var tabsLine = null;
|
|
736
|
+
var tabBtn = null;
|
|
737
|
+
var all = document.querySelectorAll("button");
|
|
738
|
+
for (var t = 0; t < all.length; t++) {
|
|
739
|
+
var el = all[t];
|
|
740
|
+
var txt = (el.textContent || "").trim();
|
|
741
|
+
if (txt !== "对话" && txt !== "轨迹") continue;
|
|
742
|
+
var er = el.getBoundingClientRect();
|
|
743
|
+
if (er.bottom > spTop + 1) continue;
|
|
744
|
+
if (tabBtn === null || er.bottom > tabBtn.getBoundingClientRect().bottom) tabBtn = el;
|
|
745
|
+
}
|
|
746
|
+
if (tabBtn !== null && tabBtn.parentElement !== null &&
|
|
747
|
+
typeof tabBtn.parentElement.getBoundingClientRect === "function") {
|
|
748
|
+
tabsLine = tabBtn.parentElement.getBoundingClientRect().bottom;
|
|
749
|
+
}
|
|
750
|
+
top = tabsLine !== null ? tabsLine : spTop - 1;
|
|
751
|
+
}
|
|
752
|
+
var patch = {};
|
|
753
|
+
if (Math.abs(left - ui.railLeft) > 1) patch.railLeft = left;
|
|
754
|
+
if (Math.abs(top - ui.railTop) > 1) patch.railTop = top;
|
|
755
|
+
if (Math.abs(bottom - ui.railBottom) > 1) patch.railBottom = bottom;
|
|
756
|
+
if (Object.keys(patch).length > 0) setUi(patch);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function ensureObserved() {
|
|
760
|
+
if (ro !== null) return;
|
|
761
|
+
if (typeof ResizeObserver !== "function") return;
|
|
762
|
+
var layer = document.querySelector('[data-shell-overlay]');
|
|
763
|
+
var frame = layer !== null ? layer.parentElement : null;
|
|
764
|
+
var col = frame !== null ? frame.firstElementChild : null;
|
|
765
|
+
var sp = document.querySelector('[data-conversation-scroll]');
|
|
766
|
+
var targets = [];
|
|
767
|
+
if (col !== null) targets.push(col);
|
|
768
|
+
if (sp !== null) targets.push(sp);
|
|
769
|
+
if (targets.length === 0) return;
|
|
770
|
+
ro = new ResizeObserver(function () { measure(); });
|
|
771
|
+
for (var i = 0; i < targets.length; i++) ro.observe(targets[i]);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
measure();
|
|
775
|
+
ensureObserved();
|
|
776
|
+
timer = window.setInterval(function () { measure(); ensureObserved(); }, 500);
|
|
777
|
+
window.addEventListener("resize", measure);
|
|
778
|
+
return function () {
|
|
779
|
+
window.clearInterval(timer);
|
|
780
|
+
window.removeEventListener("resize", measure);
|
|
781
|
+
if (ro !== null) ro.disconnect();
|
|
782
|
+
};
|
|
783
|
+
}, []);
|
|
784
|
+
|
|
785
|
+
var chatTop = uiState.railTop;
|
|
786
|
+
var chatHeight = Math.max(200, uiState.railBottom - uiState.railTop);
|
|
787
|
+
|
|
788
|
+
// Official-sidebar look: same fill, right border, hover fills.
|
|
789
|
+
var sidebarFill = "var(--dsw-specific-sidebar-fill, #f7f7f8)";
|
|
790
|
+
var hoverFill = "var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.12))";
|
|
791
|
+
|
|
792
|
+
function hoverProps() {
|
|
793
|
+
return {
|
|
794
|
+
onMouseEnter: function (e) { e.currentTarget.style.background = hoverFill; },
|
|
795
|
+
onMouseLeave: function (e) { e.currentTarget.style.background = "transparent"; }
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// ---- collapsed: a small sidebar-style button at the MIDDLE of the
|
|
800
|
+
// conversation's left edge; idle shows the sleeping-cat icon, hover
|
|
801
|
+
// shows only the tooltip (no color change) ----
|
|
802
|
+
if (uiState.railCollapsed) {
|
|
803
|
+
return react.createElement("button", {
|
|
804
|
+
type: "button",
|
|
805
|
+
"data-sc-rail": "1",
|
|
806
|
+
title: "展开工具栏",
|
|
807
|
+
onClick: function () { setUi({ railCollapsed: false }); },
|
|
808
|
+
style: {
|
|
809
|
+
position: "fixed", left: uiState.railLeft + 4, top: chatTop + chatHeight / 2 - 28,
|
|
810
|
+
zIndex: 30, pointerEvents: "auto",
|
|
811
|
+
width: 56, height: 56, padding: 0,
|
|
812
|
+
border: "none", cursor: "pointer",
|
|
813
|
+
background: "transparent",
|
|
814
|
+
color: "var(--dsw-alias-label-secondary, #555)",
|
|
815
|
+
borderRadius: "0 50% 50% 0"
|
|
816
|
+
}
|
|
817
|
+
}, sleepingCatIcon());
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// ---- expanded: flat embedded strip in the official sidebar's language ----
|
|
821
|
+
var railStyle = {
|
|
822
|
+
position: "fixed", left: uiState.railLeft, top: chatTop, zIndex: 30, pointerEvents: "auto",
|
|
823
|
+
width: 38, height: chatHeight,
|
|
824
|
+
display: "flex", flexDirection: "column", alignItems: "center",
|
|
825
|
+
padding: "6px 0 6px", gap: 4,
|
|
826
|
+
background: sidebarFill,
|
|
827
|
+
borderRight: "1px solid var(--dsw-alias-border-l1, rgba(127,127,127,.2))",
|
|
828
|
+
borderTopRightRadius: 0, borderBottomRightRadius: 0
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
var iconStyle = {
|
|
832
|
+
width: 30, height: 30, borderRadius: 7, border: "none", cursor: "pointer",
|
|
833
|
+
background: "transparent", fontSize: 15, lineHeight: "30px", padding: 0,
|
|
834
|
+
flex: "none", color: "var(--dsw-alias-label-secondary, #555)",
|
|
835
|
+
display: "flex", alignItems: "center", justifyContent: "center"
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
var buttons = [];
|
|
839
|
+
// Top: the bigger collapse button, in the official sidebar's language.
|
|
840
|
+
buttons.push(react.createElement("button", {
|
|
841
|
+
key: "collapse",
|
|
842
|
+
type: "button",
|
|
843
|
+
title: "收起工具栏",
|
|
844
|
+
style: Object.assign({}, iconStyle, { width: 34, height: 34, fontSize: 13, fontWeight: 700, color: "rgba(70,70,75,.9)" }),
|
|
845
|
+
onClick: function () { setUi({ railCollapsed: true }); }
|
|
846
|
+
}, "«"));
|
|
847
|
+
buttons.push(react.createElement("div", { key: "sep", style: { width: 24, height: 1, background: "rgba(127,127,127,.18)", margin: "4px 0 6px", flex: "none" } }, null));
|
|
848
|
+
for (var i = 0; i < TOOLS.length; i++) {
|
|
849
|
+
(function (tool) {
|
|
850
|
+
buttons.push(react.createElement("button", {
|
|
851
|
+
key: tool.id,
|
|
852
|
+
type: "button",
|
|
853
|
+
title: tool.label,
|
|
854
|
+
style: iconStyle,
|
|
855
|
+
onClick: function () {
|
|
856
|
+
if (tool.id === "questions") setUi({ open: !ui.open });
|
|
857
|
+
}
|
|
858
|
+
}, tool.icon));
|
|
859
|
+
})(TOOLS[i]);
|
|
860
|
+
}
|
|
861
|
+
buttons.push(react.createElement("button", {
|
|
862
|
+
key: "settings",
|
|
863
|
+
type: "button",
|
|
864
|
+
title: "设置",
|
|
865
|
+
style: iconStyle,
|
|
866
|
+
onClick: function () { setUi({ settingsOpen: !ui.settingsOpen }); }
|
|
867
|
+
}, SETTINGS_ICON));
|
|
868
|
+
buttons.push(react.createElement("button", {
|
|
869
|
+
key: "collapse-all",
|
|
870
|
+
type: "button",
|
|
871
|
+
title: "一键收起所有问题的过程",
|
|
872
|
+
style: iconStyle,
|
|
873
|
+
onClick: function () { collapseAllTurns(latestSnapshot); }
|
|
874
|
+
}, COLLAPSE_ALL_ICON));
|
|
875
|
+
buttons.push(react.createElement("div", { key: "spacer", style: { flex: "1" } }, null));
|
|
876
|
+
|
|
877
|
+
// Hover treatment identical to the expand arrow (translucent fill).
|
|
878
|
+
return react.createElement("div", { "data-sc-rail": "1", style: railStyle },
|
|
879
|
+
buttons.map(function (btn) {
|
|
880
|
+
if (btn.key === "sep" || btn.key === "spacer") return btn;
|
|
881
|
+
return react.cloneElement(btn, hoverProps());
|
|
882
|
+
}));
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// ---- settings panel (extensible) ----
|
|
886
|
+
function SettingsPanel() {
|
|
887
|
+
var uiState = useUi();
|
|
888
|
+
if (!uiState.settingsOpen) return null;
|
|
889
|
+
var s = uiState.settings;
|
|
890
|
+
var rowStyle = { display: "flex", alignItems: "center", gap: 8 };
|
|
891
|
+
var labelStyle = { flex: "1" };
|
|
892
|
+
var inputStyle = { width: 56, padding: "2px 6px" };
|
|
893
|
+
var sectionTitle = { fontWeight: 600, fontSize: 12, color: "rgba(127,127,127,.85)", margin: "10px 0 6px" };
|
|
894
|
+
|
|
895
|
+
return react.createElement("div", {
|
|
896
|
+
"data-sc-settings": "1",
|
|
897
|
+
style: {
|
|
898
|
+
position: "fixed", left: Math.max(8, uiState.railLeft + 52), top: 72, zIndex: 30, pointerEvents: "auto",
|
|
899
|
+
width: 320, maxWidth: "calc(100vw - 40px)",
|
|
900
|
+
background: "var(--dsw-alias-bg-base, #ffffff)", color: "var(--dsw-alias-label-primary, #111)",
|
|
901
|
+
border: "1px solid rgba(127,127,127,.3)", borderRadius: 12,
|
|
902
|
+
boxShadow: "0 8px 32px rgba(0,0,0,.2)",
|
|
903
|
+
display: "flex", flexDirection: "column", padding: "4px 14px 12px", fontSize: 12
|
|
904
|
+
}
|
|
905
|
+
},
|
|
906
|
+
react.createElement("div", { style: { padding: "8px 0", borderBottom: "1px solid rgba(127,127,127,.25)", fontWeight: 600, fontSize: 13, display: "flex", alignItems: "center", gap: 6 } },
|
|
907
|
+
react.createElement("span", { style: { display: "flex", alignItems: "center" } }, SETTINGS_ICON),
|
|
908
|
+
react.createElement("span", null, "设置")),
|
|
909
|
+
react.createElement("div", { style: sectionTitle }, "通用"),
|
|
910
|
+
react.createElement("div", { style: rowStyle },
|
|
911
|
+
react.createElement("input", {
|
|
912
|
+
type: "checkbox",
|
|
913
|
+
checked: s.autoExpand,
|
|
914
|
+
onChange: function (e) {
|
|
915
|
+
ui.settings.autoExpand = e.target.checked;
|
|
916
|
+
persistSettings();
|
|
917
|
+
setUi({ settings: Object.assign({}, ui.settings) });
|
|
918
|
+
}
|
|
919
|
+
}),
|
|
920
|
+
react.createElement("span", { style: labelStyle }, "打开时自动加载更早的内容")),
|
|
921
|
+
react.createElement("div", { style: rowStyle },
|
|
922
|
+
react.createElement("span", { style: labelStyle }, "最多扩展页数"),
|
|
923
|
+
react.createElement("input", {
|
|
924
|
+
type: "number", min: 1, max: 12,
|
|
925
|
+
value: s.maxPages,
|
|
926
|
+
style: inputStyle,
|
|
927
|
+
onChange: function (e) {
|
|
928
|
+
var v = parseInt(e.target.value, 10);
|
|
929
|
+
if (isNaN(v)) return;
|
|
930
|
+
ui.settings.maxPages = Math.max(1, Math.min(12, v));
|
|
931
|
+
persistSettings();
|
|
932
|
+
setUi({ settings: Object.assign({}, ui.settings) });
|
|
933
|
+
}
|
|
934
|
+
})),
|
|
935
|
+
react.createElement("div", { style: rowStyle },
|
|
936
|
+
react.createElement("span", { style: labelStyle }, "问题列表最多显示条数"),
|
|
937
|
+
react.createElement("input", {
|
|
938
|
+
type: "number", min: 10, max: 500, step: 10,
|
|
939
|
+
value: s.maxQuestions,
|
|
940
|
+
style: inputStyle,
|
|
941
|
+
onChange: function (e) {
|
|
942
|
+
var v = parseInt(e.target.value, 10);
|
|
943
|
+
if (isNaN(v)) return;
|
|
944
|
+
ui.settings.maxQuestions = Math.max(10, Math.min(500, v));
|
|
945
|
+
persistSettings();
|
|
946
|
+
setUi({ settings: Object.assign({}, ui.settings) });
|
|
947
|
+
}
|
|
948
|
+
})),
|
|
949
|
+
react.createElement("div", { style: sectionTitle }, "回答收起"),
|
|
950
|
+
react.createElement("div", { style: rowStyle },
|
|
951
|
+
react.createElement("input", {
|
|
952
|
+
type: "checkbox",
|
|
953
|
+
checked: s.autoCollapse,
|
|
954
|
+
onChange: function (e) {
|
|
955
|
+
ui.settings.autoCollapse = e.target.checked;
|
|
956
|
+
persistSettings();
|
|
957
|
+
setUi({ settings: Object.assign({}, ui.settings) });
|
|
958
|
+
}
|
|
959
|
+
}),
|
|
960
|
+
react.createElement("span", { style: labelStyle }, "模型回答完成后自动收起过程")),
|
|
961
|
+
react.createElement("div", { style: { marginTop: 6, color: "rgba(127,127,127,.7)", lineHeight: 1.6 } },
|
|
962
|
+
"开启后,模型每回答完一个问题,该回合的工作过程会自动收起,只显示问题与最终输出;想看过程点“展开过程”即可。"),
|
|
963
|
+
react.createElement("div", { style: { marginTop: 12, borderTop: "1px solid rgba(127,127,127,.15)", paddingTop: 8, color: "rgba(127,127,127,.55)" } },
|
|
964
|
+
"更多定制化内容将在此添加 · Esc 或点击外部关闭"));
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// ---- questions console panel ----
|
|
968
|
+
function SessionConsolePanel(props) {
|
|
969
|
+
var state = props.useStore(function (s) { return s; });
|
|
970
|
+
var summary = props.useSessions(function (s) {
|
|
971
|
+
var cur = s.current;
|
|
972
|
+
return cur === undefined ? undefined : s.byId[cur];
|
|
973
|
+
});
|
|
974
|
+
var uiState = useUi();
|
|
975
|
+
var noticeState = react.useState("");
|
|
976
|
+
var expandingState = react.useState(false);
|
|
977
|
+
var expandedState = react.useState({});
|
|
978
|
+
var tipState = react.useState(null);
|
|
979
|
+
var panelRef = react.useRef(null);
|
|
980
|
+
var notice = noticeState[0], setNotice = noticeState[1];
|
|
981
|
+
var expanding = expandingState[0], setExpanding = expandingState[1];
|
|
982
|
+
var expanded = expandedState[0], setExpanded = expandedState[1];
|
|
983
|
+
var tip = tipState[0], setTip = tipState[1];
|
|
984
|
+
|
|
985
|
+
function flashNotice(text) {
|
|
986
|
+
setNotice(text);
|
|
987
|
+
window.setTimeout(function () { setNotice(""); }, 2600);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function toggleExpanded(seq) {
|
|
991
|
+
var next = {};
|
|
992
|
+
var keys = Object.keys(expanded);
|
|
993
|
+
for (var i = 0; i < keys.length; i++) if (keys[i] !== String(seq)) next[keys[i]] = true;
|
|
994
|
+
if (!expanded[seq]) next[seq] = true;
|
|
995
|
+
setExpanded(next);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function showTip(e, lines) {
|
|
999
|
+
var rect = e.currentTarget.getBoundingClientRect();
|
|
1000
|
+
var x = Math.min(rect.left, window.innerWidth - 360);
|
|
1001
|
+
var y = rect.bottom + 6;
|
|
1002
|
+
setTip({ x: Math.max(8, x), y: Math.max(8, y), lines: lines });
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function hideTip() { setTip(null); }
|
|
1006
|
+
|
|
1007
|
+
// Drag by the header (not by buttons inside it).
|
|
1008
|
+
function startDrag(e) {
|
|
1009
|
+
if (e.button !== 0) return;
|
|
1010
|
+
var target = e.target;
|
|
1011
|
+
if (target && target.closest && target.closest("button")) return;
|
|
1012
|
+
e.preventDefault();
|
|
1013
|
+
var startX = e.clientX, startY = e.clientY;
|
|
1014
|
+
var rect = panelRef.current.getBoundingClientRect();
|
|
1015
|
+
var left0 = rect.left, top0 = rect.top;
|
|
1016
|
+
function move(ev) {
|
|
1017
|
+
var left = Math.max(8, Math.min(window.innerWidth - 140, left0 + ev.clientX - startX));
|
|
1018
|
+
var top = Math.max(8, Math.min(window.innerHeight - 60, top0 + ev.clientY - startY));
|
|
1019
|
+
setUi({ pos: { x: left, y: top } });
|
|
1020
|
+
}
|
|
1021
|
+
function up() {
|
|
1022
|
+
window.removeEventListener("pointermove", move);
|
|
1023
|
+
window.removeEventListener("pointerup", up);
|
|
1024
|
+
persistPos();
|
|
1025
|
+
}
|
|
1026
|
+
window.addEventListener("pointermove", move);
|
|
1027
|
+
window.addEventListener("pointerup", up);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Inject per-question collapse buttons + auto-collapse new/current turns
|
|
1031
|
+
// (per setting) + re-apply collapsed turns on every snapshot change.
|
|
1032
|
+
// A short delayed pass catches rows that render after the snapshot commit.
|
|
1033
|
+
react.useEffect(function () {
|
|
1034
|
+
if (!state.snapshot) return;
|
|
1035
|
+
if (uiState.settings.autoCollapse) {
|
|
1036
|
+
var nodes = legacyNodesOf(state.snapshot);
|
|
1037
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
1038
|
+
var n = nodes[i];
|
|
1039
|
+
if (n.kind !== "assistant" || !n.messageId) continue;
|
|
1040
|
+
var qSeq = -1;
|
|
1041
|
+
for (var j = 0; j < nodes.length; j++) {
|
|
1042
|
+
var m = nodes[j];
|
|
1043
|
+
if ((m.kind === "user" || m.kind === "steering") && m.seq < n.seq && m.seq > qSeq) qSeq = m.seq;
|
|
1044
|
+
}
|
|
1045
|
+
if (qSeq >= 0 && !manualSeqs.has(qSeq)) collapsedUserSeqs.add(qSeq);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
injectTurnButtons(state.snapshot);
|
|
1049
|
+
collapsedUserSeqs.forEach(function (seq) {
|
|
1050
|
+
var row = rowElementForSeq(state.snapshot, seq);
|
|
1051
|
+
if (row === null) return;
|
|
1052
|
+
applyBetween(row, turnEndRow(row), true);
|
|
1053
|
+
});
|
|
1054
|
+
syncTurnButtonLabels();
|
|
1055
|
+
var timer = window.setTimeout(function () {
|
|
1056
|
+
injectTurnButtons(state.snapshot);
|
|
1057
|
+
collapsedUserSeqs.forEach(function (seq) {
|
|
1058
|
+
var row = rowElementForSeq(state.snapshot, seq);
|
|
1059
|
+
if (row === null) return;
|
|
1060
|
+
applyBetween(row, turnEndRow(row), true);
|
|
1061
|
+
});
|
|
1062
|
+
syncTurnButtonLabels();
|
|
1063
|
+
}, 150);
|
|
1064
|
+
return function () { window.clearTimeout(timer); };
|
|
1065
|
+
}, [state.snapshot, uiState.settings.autoCollapse]);
|
|
1066
|
+
|
|
1067
|
+
// Bounded backward window expansion on open / session switch (per settings).
|
|
1068
|
+
react.useEffect(function () {
|
|
1069
|
+
if (!uiState.open || !state.session) return;
|
|
1070
|
+
if (!uiState.settings.autoExpand) return;
|
|
1071
|
+
var cancelled = false;
|
|
1072
|
+
var session = state.session;
|
|
1073
|
+
var maxPages = uiState.settings.maxPages;
|
|
1074
|
+
var TARGET = 40;
|
|
1075
|
+
setExpanding(true);
|
|
1076
|
+
(async function () {
|
|
1077
|
+
var pages = 0;
|
|
1078
|
+
for (; pages < maxPages; pages++) {
|
|
1079
|
+
if (cancelled) return;
|
|
1080
|
+
var snap = session.getSnapshot();
|
|
1081
|
+
if (!snap || !snap.hasMore) break;
|
|
1082
|
+
if (countQuestions(snap) >= TARGET) break;
|
|
1083
|
+
try {
|
|
1084
|
+
await session.loadOlder();
|
|
1085
|
+
} catch (e) {
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
if (!cancelled) setExpanding(false);
|
|
1090
|
+
})();
|
|
1091
|
+
return function () { cancelled = true; };
|
|
1092
|
+
}, [uiState.open, state.session, uiState.settings.maxPages, uiState.settings.autoExpand]);
|
|
1093
|
+
|
|
1094
|
+
// Questions = live window (kept jumpable).
|
|
1095
|
+
var questions = [];
|
|
1096
|
+
var nodes = legacyNodesOf(state.snapshot);
|
|
1097
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
1098
|
+
var n = nodes[i];
|
|
1099
|
+
if (n.kind === "user" || n.kind === "steering") {
|
|
1100
|
+
questions.push({
|
|
1101
|
+
seq: n.seq,
|
|
1102
|
+
kind: n.kind,
|
|
1103
|
+
time: n.time,
|
|
1104
|
+
text: preview(contentText(n.content))
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
questions.sort(function (a, b) { return b.seq - a.seq; });
|
|
1109
|
+
var maxQ = uiState.settings.maxQuestions;
|
|
1110
|
+
var truncated = questions.length > maxQ;
|
|
1111
|
+
if (truncated) questions = questions.slice(0, maxQ);
|
|
1112
|
+
|
|
1113
|
+
function onJump(seq) {
|
|
1114
|
+
jumpWithExpand(state.session, seq, uiState.settings.maxPages, flashNotice);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
var questionItems = [];
|
|
1118
|
+
for (var k = 0; k < questions.length; k++) {
|
|
1119
|
+
var q = questions[k];
|
|
1120
|
+
var qDetail = questionDetail(state.snapshot, q.seq);
|
|
1121
|
+
var hasItems = qDetail.items.length > 0;
|
|
1122
|
+
var isOpen = !!expanded[q.seq] && hasItems;
|
|
1123
|
+
var detail = isOpen ? qDetail : null;
|
|
1124
|
+
var subItems = [];
|
|
1125
|
+
if (detail && detail.items.length > 0) {
|
|
1126
|
+
for (var s = 0; s < detail.items.length; s++) {
|
|
1127
|
+
var it = detail.items[s];
|
|
1128
|
+
var rowStyle = { padding: "4px 14px 4px 34px", cursor: "pointer", fontSize: 12, lineHeight: 1.5, display: "flex", gap: 8, alignItems: "baseline" };
|
|
1129
|
+
var labelStyle = { flex: "none", fontWeight: 600 };
|
|
1130
|
+
var timeStyle = { flex: "none", color: "rgba(127,127,127,.7)" };
|
|
1131
|
+
var textStyle = { flex: "1", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: "rgba(127,127,127,.85)" };
|
|
1132
|
+
if (it.kind === "ask") {
|
|
1133
|
+
// Two rows: what AI asked, and how the user answered.
|
|
1134
|
+
subItems.push(react.createElement("li", {
|
|
1135
|
+
key: "sub-" + q.seq + "-" + it.seq,
|
|
1136
|
+
onClick: function (seq) { return function () { onJump(seq); }; }(it.seq),
|
|
1137
|
+
onMouseEnter: function (item) { return function (e) { showTip(e, detailLines(item)); }; }(it),
|
|
1138
|
+
onMouseLeave: hideTip,
|
|
1139
|
+
title: "点击跳转 · 悬浮查看详情",
|
|
1140
|
+
style: rowStyle
|
|
1141
|
+
},
|
|
1142
|
+
react.createElement("span", { style: Object.assign({}, labelStyle, { color: "#b45309" }) }, "AI 提问"),
|
|
1143
|
+
react.createElement("span", { style: textStyle }, it.text),
|
|
1144
|
+
react.createElement("span", { style: timeStyle }, it.time ? formatTime(it.time) : "")));
|
|
1145
|
+
subItems.push(react.createElement("li", {
|
|
1146
|
+
key: "sub-" + q.seq + "-" + it.seq + "-a",
|
|
1147
|
+
onClick: function (seq) { return function () { onJump(seq); }; }(it.seq),
|
|
1148
|
+
onMouseEnter: function (item) { return function (e) { showTip(e, detailLines(item)); }; }(it),
|
|
1149
|
+
onMouseLeave: hideTip,
|
|
1150
|
+
title: "点击跳转 · 悬浮查看详情",
|
|
1151
|
+
style: Object.assign({}, rowStyle, { paddingLeft: 46 })
|
|
1152
|
+
},
|
|
1153
|
+
react.createElement("span", { style: Object.assign({}, labelStyle, { color: "#059669" }) }, "回答"),
|
|
1154
|
+
react.createElement("span", { style: textStyle }, it.answer && it.answer !== "" ? it.answer : "(未回答)")));
|
|
1155
|
+
} else {
|
|
1156
|
+
subItems.push(react.createElement("li", {
|
|
1157
|
+
key: "sub-" + q.seq + "-" + it.seq,
|
|
1158
|
+
onClick: function (seq) { return function () { onJump(seq); }; }(it.seq),
|
|
1159
|
+
onMouseEnter: function (item) { return function (e) { showTip(e, detailLines(item)); }; }(it),
|
|
1160
|
+
onMouseLeave: hideTip,
|
|
1161
|
+
title: "点击跳转 · 悬浮查看详情",
|
|
1162
|
+
style: rowStyle
|
|
1163
|
+
},
|
|
1164
|
+
react.createElement("span", { style: Object.assign({}, labelStyle, { color: "#059669" }) }, "用户回答"),
|
|
1165
|
+
react.createElement("span", { style: textStyle }, it.text),
|
|
1166
|
+
react.createElement("span", { style: timeStyle }, it.time ? formatTime(it.time) : "")));
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
if (detail && detail.items.length === 0) {
|
|
1171
|
+
subItems.push(react.createElement("li", {
|
|
1172
|
+
key: "sub-none-" + q.seq,
|
|
1173
|
+
style: { padding: "4px 14px 4px 34px", fontSize: 11, color: "rgba(127,127,127,.55)" }
|
|
1174
|
+
}, "本回合没有 AI 提问/用户回答记录"));
|
|
1175
|
+
}
|
|
1176
|
+
questionItems.push(react.createElement("li", {
|
|
1177
|
+
key: q.seq,
|
|
1178
|
+
style: { padding: 0, borderBottom: "1px solid rgba(127,127,127,.08)", fontSize: 12, lineHeight: 1.5 }
|
|
1179
|
+
},
|
|
1180
|
+
react.createElement("div", {
|
|
1181
|
+
onClick: function (seq) { return function () { onJump(seq); }; }(q.seq),
|
|
1182
|
+
style: { padding: "8px 14px", cursor: "pointer", display: "flex", gap: 8, alignItems: "baseline" }
|
|
1183
|
+
},
|
|
1184
|
+
hasItems
|
|
1185
|
+
? react.createElement("span", {
|
|
1186
|
+
onClick: function (seq, ev) { return function (e) { e.stopPropagation(); toggleExpanded(seq); }; }(q.seq),
|
|
1187
|
+
style: { flex: "none", cursor: "pointer", color: "rgba(127,127,127,.7)", width: 14, textAlign: "center" }
|
|
1188
|
+
}, isOpen ? "▾" : "▸")
|
|
1189
|
+
: react.createElement("span", { style: { flex: "none", width: 14 } }, null),
|
|
1190
|
+
react.createElement("span", { style: { flex: "none", fontWeight: 600, color: q.kind === "steering" ? "#7c3aed" : "#2563eb" } },
|
|
1191
|
+
q.kind === "steering" ? "插话" : "问题"),
|
|
1192
|
+
react.createElement("span", { style: { flex: "1", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: "rgba(127,127,127,.85)" } }, q.text),
|
|
1193
|
+
react.createElement("span", { style: { flex: "none", color: "rgba(127,127,127,.7)" } },
|
|
1194
|
+
q.time ? formatTime(q.time) : "")),
|
|
1195
|
+
isOpen ? react.createElement("ul", { style: { margin: 0, padding: 0, listStyle: "none", borderTop: "1px dashed rgba(127,127,127,.12)" } }, subItems) : null));
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
var body;
|
|
1199
|
+
if (!state.snapshot) {
|
|
1200
|
+
body = react.createElement("div", { style: { padding: 16, color: "rgba(127,127,127,.7)", fontSize: 12 } }, "当前没有会话");
|
|
1201
|
+
} else if (questionItems.length === 0) {
|
|
1202
|
+
body = react.createElement("div", { style: { padding: 16, color: "rgba(127,127,127,.7)", fontSize: 12 } },
|
|
1203
|
+
expanding ? "正在加载更早的对话…" : "这个会话还没有提问");
|
|
1204
|
+
} else {
|
|
1205
|
+
body = react.createElement("div", null,
|
|
1206
|
+
react.createElement("ul", { style: { margin: 0, padding: 0, listStyle: "none", maxHeight: "55vh", overflowY: "auto" } }, questionItems),
|
|
1207
|
+
truncated
|
|
1208
|
+
? react.createElement("div", { style: { padding: "6px 14px", fontSize: 11, color: "rgba(127,127,127,.55)", borderTop: "1px solid rgba(127,127,127,.08)" } },
|
|
1209
|
+
"仅显示最近 " + maxQ + " 条(共 " + (questions.length + truncated ? "更多" : "") + "),可在设置中调整")
|
|
1210
|
+
: null);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
var buttonStyle = {
|
|
1214
|
+
border: "none", background: "transparent", cursor: "pointer",
|
|
1215
|
+
color: "var(--dsw-alias-label-secondary, #555)", fontSize: 14,
|
|
1216
|
+
padding: "2px 6px", flex: "none", lineHeight: "16px"
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
var header = react.createElement("div", {
|
|
1220
|
+
onPointerDown: startDrag,
|
|
1221
|
+
style: { padding: "8px 10px", borderBottom: "1px solid rgba(127,127,127,.25)", display: "flex", gap: 6, alignItems: "center", cursor: "move", userSelect: "none" }
|
|
1222
|
+
},
|
|
1223
|
+
react.createElement("span", { style: { display: "flex", alignItems: "center" } }, TOOLS[0] ? TOOLS[0].icon : null),
|
|
1224
|
+
react.createElement("span", {
|
|
1225
|
+
style: { flex: "1", minWidth: 0, fontWeight: 600, fontSize: 13, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }
|
|
1226
|
+
}, "会话记录"),
|
|
1227
|
+
react.createElement("button", {
|
|
1228
|
+
style: Object.assign({}, buttonStyle, uiState.pinned ? { color: "#2563eb" } : null),
|
|
1229
|
+
title: uiState.pinned ? "已固定:点击外部不关闭(Esc 仍可关闭)" : "固定窗口:点击外部不关闭",
|
|
1230
|
+
onClick: function () { setUi({ pinned: !uiState.pinned }); }
|
|
1231
|
+
}, "📌"));
|
|
1232
|
+
|
|
1233
|
+
var noticeBar = notice
|
|
1234
|
+
? react.createElement("div", { style: { padding: "4px 14px", fontSize: 11, color: "#2563eb", borderBottom: "1px solid rgba(127,127,127,.1)" } }, notice)
|
|
1235
|
+
: null;
|
|
1236
|
+
|
|
1237
|
+
var statusLine = state.snapshot
|
|
1238
|
+
? react.createElement("div", { style: { padding: "4px 14px", fontSize: 11, color: "rgba(127,127,127,.75)", borderTop: "1px solid rgba(127,127,127,.1)", display: "flex", gap: 8 } },
|
|
1239
|
+
react.createElement("span", null, state.snapshot.running ? "运行中" : "空闲"),
|
|
1240
|
+
react.createElement("span", null, questions.length + " 个问题"),
|
|
1241
|
+
react.createElement("span", { style: { flex: "1" } }, null),
|
|
1242
|
+
react.createElement("span", null, uiState.pinned ? "已固定 · Esc 关闭" : "外部点击关闭 · Esc 关闭"))
|
|
1243
|
+
: null;
|
|
1244
|
+
|
|
1245
|
+
if (!uiState.open) return null;
|
|
1246
|
+
|
|
1247
|
+
var panelStyle = {
|
|
1248
|
+
position: "fixed", zIndex: 30, pointerEvents: "auto",
|
|
1249
|
+
width: 440, maxWidth: "calc(100vw - 32px)",
|
|
1250
|
+
background: "var(--dsw-alias-bg-base, #ffffff)", color: "var(--dsw-alias-label-primary, #111)",
|
|
1251
|
+
border: "1px solid rgba(127,127,127,.3)", borderRadius: 12,
|
|
1252
|
+
boxShadow: "0 8px 32px rgba(0,0,0,.22)",
|
|
1253
|
+
display: "flex", flexDirection: "column", overflow: "hidden"
|
|
1254
|
+
};
|
|
1255
|
+
if (uiState.pos !== null) {
|
|
1256
|
+
panelStyle.left = uiState.pos.x;
|
|
1257
|
+
panelStyle.top = uiState.pos.y;
|
|
1258
|
+
} else {
|
|
1259
|
+
panelStyle.left = Math.max(8, uiState.railLeft + 52);
|
|
1260
|
+
panelStyle.top = 72;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
var tipEl = null;
|
|
1264
|
+
if (tip !== null) {
|
|
1265
|
+
var tipLines = [];
|
|
1266
|
+
for (var t = 0; t < tip.lines.length; t++) {
|
|
1267
|
+
tipLines.push(react.createElement("div", { key: "l" + t, style: { margin: t > 0 ? "4px 0 0" : 0, whiteSpace: "pre-wrap", wordBreak: "break-word" } }, tip.lines[t]));
|
|
1268
|
+
}
|
|
1269
|
+
tipEl = react.createElement("div", {
|
|
1270
|
+
style: {
|
|
1271
|
+
position: "fixed", left: tip.x, top: tip.y, zIndex: 40, pointerEvents: "none",
|
|
1272
|
+
maxWidth: 340, padding: "8px 10px", borderRadius: 8, fontSize: 12, lineHeight: 1.5,
|
|
1273
|
+
background: "rgba(17,24,39,.94)", color: "#f1f5f9",
|
|
1274
|
+
boxShadow: "0 4px 16px rgba(0,0,0,.3)"
|
|
1275
|
+
}
|
|
1276
|
+
}, tipLines);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
return react.createElement("div", {
|
|
1280
|
+
ref: panelRef,
|
|
1281
|
+
"data-sc-panel": "1",
|
|
1282
|
+
style: panelStyle
|
|
1283
|
+
}, header, noticeBar, body, statusLine, tipEl);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// ------------------------------------------------------------- plugin
|
|
1287
|
+
function apply(ctx) {
|
|
1288
|
+
var slots = ctx.get("slots");
|
|
1289
|
+
if (slots === undefined) return;
|
|
1290
|
+
ctx.effect(function () {
|
|
1291
|
+
// Embedded-rail stylesheet: when the rail is expanded, the chat flow
|
|
1292
|
+
// makes room so the rail never covers messages (true embedding).
|
|
1293
|
+
var styleTag = null;
|
|
1294
|
+
if (typeof document !== "undefined") {
|
|
1295
|
+
styleTag = document.createElement("style");
|
|
1296
|
+
styleTag.dataset.plugin = "dsh-plugin-session-console";
|
|
1297
|
+
styleTag.dataset.pluginCss = "dsh-plugin-session-console/rail.css";
|
|
1298
|
+
styleTag.textContent = [
|
|
1299
|
+
'body[data-sc-rail-expanded] [data-chat-flow] {',
|
|
1300
|
+
' padding-left: 40px;',
|
|
1301
|
+
'}'
|
|
1302
|
+
].join("");
|
|
1303
|
+
document.head.appendChild(styleTag);
|
|
1304
|
+
}
|
|
1305
|
+
var cleanup = null;
|
|
1306
|
+
var offToolbar = slots.inject("shell.overlay", function () {
|
|
1307
|
+
return slots.register({
|
|
1308
|
+
name: "shell.overlay",
|
|
1309
|
+
id: "session-console-rail"
|
|
1310
|
+
}, ToolRail);
|
|
1311
|
+
});
|
|
1312
|
+
var offPanel = slots.inject("shell.overlay", function () {
|
|
1313
|
+
return slots.register({
|
|
1314
|
+
name: "shell.overlay",
|
|
1315
|
+
id: "session-console-panel",
|
|
1316
|
+
store: createStore,
|
|
1317
|
+
inject: function (actions) {
|
|
1318
|
+
var sessions = ctx.get("sessions");
|
|
1319
|
+
if (sessions !== undefined) cleanup = wireSessions(sessions, actions);
|
|
1320
|
+
return {};
|
|
1321
|
+
}
|
|
1322
|
+
}, SessionConsolePanel);
|
|
1323
|
+
});
|
|
1324
|
+
var offSettings = slots.inject("shell.overlay", function () {
|
|
1325
|
+
return slots.register({
|
|
1326
|
+
name: "shell.overlay",
|
|
1327
|
+
id: "session-console-settings"
|
|
1328
|
+
}, SettingsPanel);
|
|
1329
|
+
});
|
|
1330
|
+
return function () {
|
|
1331
|
+
if (cleanup) { cleanup(); cleanup = null; }
|
|
1332
|
+
if (styleTag !== null && styleTag.parentNode !== null) styleTag.parentNode.removeChild(styleTag);
|
|
1333
|
+
offToolbar();
|
|
1334
|
+
offPanel();
|
|
1335
|
+
offSettings();
|
|
1336
|
+
};
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
exports.inject = inject;
|
|
1341
|
+
exports.apply = apply;
|
|
1342
|
+
return module.exports;
|
|
1343
|
+
}
|
|
1344
|
+
});
|