dsh-per-message-model 0.1.1 → 0.1.3
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/lib/client.js +107 -60
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -110,6 +110,58 @@ window.__ModuleLoader__.load({
|
|
|
110
110
|
return modelBadge(entry, t);
|
|
111
111
|
}
|
|
112
112
|
//#endregion
|
|
113
|
+
//#region client footer
|
|
114
|
+
/**
|
|
115
|
+
* Composer-dock model button: shows the LAST model actually used by the
|
|
116
|
+
* session (the folded `current` entry, i.e. the latest assistant message's
|
|
117
|
+
* provider/model), not the composer's selected default. Hover keeps the
|
|
118
|
+
* existing chip behavior (color change + title); a single click rolls the
|
|
119
|
+
* model name out from left to right like a scroll (icon rotates by a random
|
|
120
|
+
* angle); clicking again rolls it back (icon rotates back).
|
|
121
|
+
*/
|
|
122
|
+
function FooterModelBadge({ useProjection, t }) {
|
|
123
|
+
const [open, setOpen] = React.useState(false);
|
|
124
|
+
const [rot, setRot] = React.useState(0);
|
|
125
|
+
const value = useProjection("perMessageModel");
|
|
126
|
+
const current = value === void 0 || value === null || typeof value !== "object" || value.current === void 0
|
|
127
|
+
? void 0
|
|
128
|
+
: value.current;
|
|
129
|
+
const known = current !== void 0 && typeof current === "object";
|
|
130
|
+
const provider = known && typeof current.provider === "string" ? current.provider : null;
|
|
131
|
+
const model = known && typeof current.model === "string" ? current.model : null;
|
|
132
|
+
const label = provider !== null && model !== null
|
|
133
|
+
? provider + " / " + model
|
|
134
|
+
: t("badge.unknown");
|
|
135
|
+
const toggle = () => {
|
|
136
|
+
if (open) {
|
|
137
|
+
setOpen(false);
|
|
138
|
+
} else {
|
|
139
|
+
setRot(Math.round(90 + Math.random() * 180));
|
|
140
|
+
setOpen(true);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
return createElement("button", {
|
|
144
|
+
type: "button",
|
|
145
|
+
className: BADGE_PREFIX + "footer",
|
|
146
|
+
"data-dsh-per-message-model-footer": "true",
|
|
147
|
+
"aria-label": label,
|
|
148
|
+
title: label,
|
|
149
|
+
"aria-expanded": open ? "true" : "false",
|
|
150
|
+
onClick: toggle
|
|
151
|
+
}, [
|
|
152
|
+
createElement("span", {
|
|
153
|
+
key: "icon",
|
|
154
|
+
className: BADGE_PREFIX + "footer-icon",
|
|
155
|
+
style: { transform: open ? "rotate(" + rot + "deg)" : "rotate(0deg)" }
|
|
156
|
+
}, chipIcon()),
|
|
157
|
+
createElement("span", {
|
|
158
|
+
key: "label",
|
|
159
|
+
className: BADGE_PREFIX + "footer-label",
|
|
160
|
+
"data-open": open ? "true" : "false"
|
|
161
|
+
}, label)
|
|
162
|
+
]);
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
113
165
|
//#region client divider
|
|
114
166
|
/** Fixed Chinese copy for the model-switch divider (user-required). */
|
|
115
167
|
const SWITCH_COPY = "模型切换:";
|
|
@@ -160,77 +212,61 @@ window.__ModuleLoader__.load({
|
|
|
160
212
|
return div;
|
|
161
213
|
}
|
|
162
214
|
/**
|
|
163
|
-
* In-stream model-switch divider.
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
215
|
+
* In-stream model-switch divider. Session-scoped maintenance component
|
|
216
|
+
* (mounted on the always-alive session header seat, not a per-turn seat):
|
|
217
|
+
* it watches the live projection and keeps a divider node at EVERY switch
|
|
218
|
+
* boundary — strictly BETWEEN the last call of the previous model
|
|
219
|
+
* (fromTurn/fromStep anchor) and the first call of the new model
|
|
220
|
+
* (toTurn/toStep anchor). It prefers inserting after the from-step node;
|
|
221
|
+
* when that node is absent (windowed history) it falls back to inserting
|
|
222
|
+
* before the to-step node. A MutationObserver continuously repairs
|
|
223
|
+
* dividers lost to windowing/re-render, so they survive scrolling and
|
|
224
|
+
* turn lifecycle. Nodes are deduplicated by the switch's fromSeq.
|
|
172
225
|
*/
|
|
173
|
-
function ModelSwitchDivider({
|
|
226
|
+
function ModelSwitchDivider({ useProjection }) {
|
|
174
227
|
const value = useProjection("perMessageModel");
|
|
175
228
|
const switches = value === void 0 || value === null || typeof value !== "object" || !Array.isArray(value.switches)
|
|
176
229
|
? []
|
|
177
230
|
: value.switches;
|
|
178
|
-
// The owner prop is the full turn location object; its numeric id is
|
|
179
|
-
// exposed as `.turn` (matches the projection's fromTurn/toTurn).
|
|
180
|
-
const turnId = turn !== null && typeof turn === "object" && Number.isSafeInteger(turn.turn) ? turn.turn : null;
|
|
181
231
|
React.useEffect(() => {
|
|
182
232
|
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return;
|
|
183
|
-
const inserted = [];
|
|
184
233
|
let observer = null;
|
|
185
|
-
const pending = switches.filter((sw) =>
|
|
186
|
-
sw.fromTurn === turnId &&
|
|
187
|
-
sw.fromStep !== void 0 && sw.fromStep !== null &&
|
|
188
|
-
sw.to !== null && typeof sw.to === "object" &&
|
|
189
|
-
typeof sw.to.provider === "string" && typeof sw.to.model === "string"
|
|
190
|
-
);
|
|
191
234
|
const tryInsert = () => {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
235
|
+
for (const sw of switches) {
|
|
236
|
+
if (sw.to === null || typeof sw.to !== "object" ||
|
|
237
|
+
typeof sw.to.provider !== "string" || typeof sw.to.model !== "string") continue;
|
|
238
|
+
if (sw.fromTurn === void 0 || sw.fromTurn === null ||
|
|
239
|
+
sw.fromStep === void 0 || sw.fromStep === null) continue;
|
|
240
|
+
const seqKey = Number.isSafeInteger(sw.fromSeq) ? sw.fromSeq : (sw.fromTurn + ":" + sw.fromStep);
|
|
241
|
+
const existing = document.querySelector('[data-dsh-model-switch][data-from-seq="' + seqKey + '"]');
|
|
242
|
+
if (existing !== null) continue;
|
|
243
|
+
const fromKey = ASSISTANT_STEP_KEY_PREFIX + sw.fromTurn + ":" + sw.fromStep;
|
|
244
|
+
const toKey = Number.isSafeInteger(sw.toTurn) && Number.isSafeInteger(sw.toStep)
|
|
245
|
+
? ASSISTANT_STEP_KEY_PREFIX + sw.toTurn + ":" + sw.toStep
|
|
246
|
+
: null;
|
|
247
|
+
const fromAnchor = document.querySelector('[data-chat-anchor-key="' + fromKey + '"]');
|
|
248
|
+
const toAnchor = toKey !== null ? document.querySelector('[data-chat-anchor-key="' + toKey + '"]') : null;
|
|
249
|
+
const anchor = fromAnchor !== null ? fromAnchor : toAnchor;
|
|
250
|
+
if (anchor === null) continue;
|
|
198
251
|
const node = buildDividerNode(sw);
|
|
199
|
-
|
|
200
|
-
|
|
252
|
+
node.setAttribute("data-from-seq", String(seqKey));
|
|
253
|
+
if (fromAnchor !== null) fromAnchor.insertAdjacentElement("afterend", node);
|
|
254
|
+
else toAnchor.insertAdjacentElement("beforebegin", node);
|
|
201
255
|
}
|
|
202
|
-
return remaining;
|
|
203
256
|
};
|
|
204
|
-
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (left === 0) {
|
|
212
|
-
observer.disconnect();
|
|
213
|
-
observer = null;
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
observer.observe(document.body, { childList: true, subtree: true });
|
|
217
|
-
}
|
|
257
|
+
tryInsert();
|
|
258
|
+
// Keep repairing: windowed conversation rendering mounts/unmounts
|
|
259
|
+
// chat nodes while scrolling, so dividers must be re-inserted on
|
|
260
|
+
// any childList change. The callback is cheap (per-switch
|
|
261
|
+
// querySelector + dedupe) and exits immediately once all exist.
|
|
262
|
+
observer = new MutationObserver(() => tryInsert());
|
|
263
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
218
264
|
return () => {
|
|
219
265
|
if (observer !== null) observer.disconnect();
|
|
220
|
-
for (const node of inserted) node.remove();
|
|
221
266
|
};
|
|
222
|
-
}, [switches
|
|
267
|
+
}, [switches]);
|
|
223
268
|
return null;
|
|
224
269
|
}
|
|
225
|
-
/**
|
|
226
|
-
* Chain selector: this occupant is the only turnTail registrant, so it is
|
|
227
|
-
* always elected; the component itself decides whether to insert anything
|
|
228
|
-
* (select cannot reach the projection store synchronously, so the reactive
|
|
229
|
-
* read lives in the component via useProjection).
|
|
230
|
-
*/
|
|
231
|
-
function selectModelSwitch() {
|
|
232
|
-
return true;
|
|
233
|
-
}
|
|
234
270
|
//#endregion
|
|
235
271
|
//#region client plugin
|
|
236
272
|
/** Required services for the additive seats. */
|
|
@@ -250,10 +286,16 @@ window.__ModuleLoader__.load({
|
|
|
250
286
|
`div[class*="actions"]:has([data-slot="conversation.chat.assistant-actions"]) [class*="timeEnd"]{order:4}`,
|
|
251
287
|
// In-stream divider: full-width block, embeds in the message flow.
|
|
252
288
|
// Label (模型切换:) and arrow are gray; model names are darker/bolder.
|
|
253
|
-
`[data-dsh-model-switch]{display:flex;align-items:center;gap:8px;margin:10px 0 16px;padding:10px 14px;border:
|
|
289
|
+
`[data-dsh-model-switch]{display:flex;align-items:center;gap:8px;margin:10px 0 16px;padding:10px 14px;border:1.5px solid var(--dsw-alias-state-business-primary, #4f8cff);border-radius:10px;background:var(--dsw-alias-state-business-tertiary, rgba(79,140,255,.12));box-shadow:0 1px 3px rgba(0,0,0,.10);color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;user-select:none}`,
|
|
254
290
|
`[data-dsh-model-switch] [class*="-divider-label"]{flex:none;color:var(--dsw-alias-label-tertiary);font-weight:600}`,
|
|
255
291
|
`[data-dsh-model-switch] [class*="-divider-model"]{flex:none;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary);font-weight:700}`,
|
|
256
|
-
`[data-dsh-model-switch] [class*="-divider-arrow"]{flex:none;display:inline-flex;align-items:center;color:var(--dsw-alias-label-tertiary)}
|
|
292
|
+
`[data-dsh-model-switch] [class*="-divider-arrow"]{flex:none;display:inline-flex;align-items:center;color:var(--dsw-alias-label-tertiary)}`,
|
|
293
|
+
// Composer-dock model button: chip icon + unfoldable label (scroll).
|
|
294
|
+
`[data-dsh-per-message-model-footer]{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border:1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));border-radius:8px;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;max-width:100%;overflow:hidden}`,
|
|
295
|
+
`[data-dsh-per-message-model-footer]:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12))}`,
|
|
296
|
+
`[data-dsh-per-message-model-footer] [class*="-footer-icon"]{display:inline-flex;align-items:center;flex:none;transition:transform .45s cubic-bezier(.34,1.56,.64,1)}`,
|
|
297
|
+
`[data-dsh-per-message-model-footer] [class*="-footer-label"]{display:inline-block;overflow:hidden;white-space:nowrap;max-width:0;opacity:0;transition:max-width .45s cubic-bezier(.4,0,.2,1),opacity .35s ease;font-size:12px;line-height:16px;color:var(--dsw-alias-label-primary);font-weight:600}`,
|
|
298
|
+
`[data-dsh-per-message-model-footer] [class*="-footer-label"][data-open="true"]{max-width:360px;opacity:1}`
|
|
257
299
|
].join("\n");
|
|
258
300
|
document.head.appendChild(badgeStyle);
|
|
259
301
|
ctx.effect(() => () => badgeStyle.remove(), "per-message-model: css");
|
|
@@ -270,13 +312,18 @@ window.__ModuleLoader__.load({
|
|
|
270
312
|
locale: NS,
|
|
271
313
|
order: 40
|
|
272
314
|
}, PerMessageModelBadge));
|
|
273
|
-
ctx.slots.inject("conversation.
|
|
274
|
-
name: "conversation.
|
|
315
|
+
ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
|
|
316
|
+
name: "conversation.session.header.actions",
|
|
275
317
|
id: "dsh-per-message-model-switch",
|
|
276
318
|
locale: NS,
|
|
277
|
-
order:
|
|
278
|
-
select: selectModelSwitch
|
|
319
|
+
order: 90
|
|
279
320
|
}, ModelSwitchDivider));
|
|
321
|
+
ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
|
|
322
|
+
name: "conversation.input.dock",
|
|
323
|
+
id: "dsh-per-message-model-footer",
|
|
324
|
+
locale: NS,
|
|
325
|
+
order: 90
|
|
326
|
+
}, FooterModelBadge));
|
|
280
327
|
}
|
|
281
328
|
//#endregion
|
|
282
329
|
exports.apply = apply;
|