@roaming-ai/dsh-group-chat 0.3.2 → 0.4.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/lib/client.js
CHANGED
|
@@ -122,6 +122,7 @@ window.__ModuleLoader__.load({
|
|
|
122
122
|
".dsgc-msg.mine{flex-direction:row-reverse}",
|
|
123
123
|
".dsgc-avatar{width:28px;height:28px;box-sizing:border-box;border:2px solid var(--dsw-alias-border-l3,rgba(128,128,128,.4));border-radius:50%;background:var(--dsw-alias-bg-module-platform,rgba(128,128,128,.12));color:var(--dsw-alias-label-primary,inherit);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;flex:none;margin-top:1px}",
|
|
124
124
|
".dsgc-avatar.mine{border-color:transparent;background:var(--dsw-alias-button-info-fill,#4f6ef7);color:var(--dsw-alias-label-primary-foreground,#fff)}",
|
|
125
|
+
".dsgc-avatar .dsgc-orb{display:block}",
|
|
125
126
|
".dsgc-msgbody{max-width:76%;min-width:0;display:flex;flex-direction:column;gap:5px}",
|
|
126
127
|
".dsgc-msg.mine .dsgc-msgbody{align-items:flex-end}",
|
|
127
128
|
".dsgc-msghead{display:flex;gap:6px;align-items:center;min-width:0;font-size:12px;color:var(--dsw-alias-label-secondary,inherit)}",
|
|
@@ -2125,6 +2126,167 @@ window.__ModuleLoader__.load({
|
|
|
2125
2126
|
});
|
|
2126
2127
|
}
|
|
2127
2128
|
//#endregion
|
|
2129
|
+
//#region src/client/components/SpeakerOrb.tsx
|
|
2130
|
+
/**
|
|
2131
|
+
* 发言人执行态点阵光球(live 行头像内容):角色色单色点阵 Canvas 2D 动画。
|
|
2132
|
+
* thinking(轨道热斑游走=等待首字节/推理中)与 listening(向外涟漪=正文
|
|
2133
|
+
* 流出)两态间弹簧缩放 + 权重交叉淡入;裁掉参考实现 MatrixOrb 的 label、
|
|
2134
|
+
* level 与 idle 态(本场景只有执行中一种挂载时机),尺寸钉死为头像内容盒。
|
|
2135
|
+
* @module dsh-group-chat/client/SpeakerOrb
|
|
2136
|
+
*/
|
|
2137
|
+
const TAU = Math.PI * 2;
|
|
2138
|
+
const STATES = ["thinking", "listening"];
|
|
2139
|
+
/** 头像内容盒:28px 外框减 2px 描边 ×2;圆形轮廓由 d 截断近似。 */
|
|
2140
|
+
const SIZE = 24;
|
|
2141
|
+
const GRID = 7;
|
|
2142
|
+
/** 点阵铺开比例:外圈点贴近描边又不被 border-radius 裁切。 */
|
|
2143
|
+
const SPREAD = .86;
|
|
2144
|
+
const SCALE = {
|
|
2145
|
+
thinking: .94,
|
|
2146
|
+
listening: 1
|
|
2147
|
+
};
|
|
2148
|
+
const DAMPING = 26;
|
|
2149
|
+
const ATTACK = .22;
|
|
2150
|
+
const RELEASE = .08;
|
|
2151
|
+
/** thinking 态热斑轨道(点阵归一化坐标)。 */
|
|
2152
|
+
const ORBITERS = [
|
|
2153
|
+
{
|
|
2154
|
+
radius: .62,
|
|
2155
|
+
speed: 2.2,
|
|
2156
|
+
phase: 0,
|
|
2157
|
+
spread: .42
|
|
2158
|
+
},
|
|
2159
|
+
{
|
|
2160
|
+
radius: .4,
|
|
2161
|
+
speed: -1.7,
|
|
2162
|
+
phase: 2.1,
|
|
2163
|
+
spread: .36
|
|
2164
|
+
},
|
|
2165
|
+
{
|
|
2166
|
+
radius: .8,
|
|
2167
|
+
speed: 1.15,
|
|
2168
|
+
phase: 4,
|
|
2169
|
+
spread: .34
|
|
2170
|
+
}
|
|
2171
|
+
];
|
|
2172
|
+
function envelope(t) {
|
|
2173
|
+
const slow = .5 + .5 * Math.sin(t * .62 + .4);
|
|
2174
|
+
const fast = .5 + .5 * Math.sin(t * 1.9 + 1.1);
|
|
2175
|
+
return .22 + .78 * (.45 + .55 * slow) * fast;
|
|
2176
|
+
}
|
|
2177
|
+
function intensityOf(state, d, nx, ny, t, amplitude) {
|
|
2178
|
+
if (state === "listening") return .32 + amplitude * (.34 + .38 * (.5 + .5 * Math.sin(d * 4.2 - t * 3)));
|
|
2179
|
+
let heat = 0;
|
|
2180
|
+
for (const o of ORBITERS) {
|
|
2181
|
+
const a = t * o.speed + o.phase;
|
|
2182
|
+
const dx = nx - Math.cos(a) * o.radius;
|
|
2183
|
+
const dy = ny - Math.sin(a) * o.radius;
|
|
2184
|
+
heat += Math.exp(-(dx * dx + dy * dy) / (o.spread * o.spread));
|
|
2185
|
+
}
|
|
2186
|
+
return .26 + .8 * Math.min(1, heat);
|
|
2187
|
+
}
|
|
2188
|
+
function subscribeToZoom(onChange) {
|
|
2189
|
+
window.addEventListener("resize", onChange);
|
|
2190
|
+
return () => window.removeEventListener("resize", onChange);
|
|
2191
|
+
}
|
|
2192
|
+
function useDevicePixelRatio() {
|
|
2193
|
+
return (0, react.useSyncExternalStore)(subscribeToZoom, () => Math.min(window.devicePixelRatio || 1, 4), () => 1);
|
|
2194
|
+
}
|
|
2195
|
+
function SpeakerOrb(props) {
|
|
2196
|
+
const { state, color } = props;
|
|
2197
|
+
const canvasRef = (0, react.useRef)(null);
|
|
2198
|
+
const stateRef = (0, react.useRef)(state);
|
|
2199
|
+
const redrawRef = (0, react.useRef)(null);
|
|
2200
|
+
const dpr = useDevicePixelRatio();
|
|
2201
|
+
(0, react.useEffect)(() => {
|
|
2202
|
+
stateRef.current = state;
|
|
2203
|
+
}, [state]);
|
|
2204
|
+
(0, react.useEffect)(() => {
|
|
2205
|
+
const canvas = canvasRef.current;
|
|
2206
|
+
const ctx = canvas?.getContext("2d");
|
|
2207
|
+
if (!canvas || !ctx) return;
|
|
2208
|
+
const buffer = Math.round(SIZE * dpr);
|
|
2209
|
+
canvas.width = canvas.height = buffer;
|
|
2210
|
+
ctx.scale(buffer / SIZE, buffer / SIZE);
|
|
2211
|
+
ctx.fillStyle = color;
|
|
2212
|
+
const half = 3;
|
|
2213
|
+
const spacing = SIZE * SPREAD / 6;
|
|
2214
|
+
const maxRadius = spacing * .6;
|
|
2215
|
+
const center = SIZE / 2;
|
|
2216
|
+
const weights = {
|
|
2217
|
+
thinking: 0,
|
|
2218
|
+
listening: 0
|
|
2219
|
+
};
|
|
2220
|
+
weights[stateRef.current] = 1;
|
|
2221
|
+
const draw = (t, amplitude, scale) => {
|
|
2222
|
+
ctx.clearRect(0, 0, SIZE, SIZE);
|
|
2223
|
+
for (let iy = 0; iy < GRID; iy++) for (let ix = 0; ix < GRID; ix++) {
|
|
2224
|
+
const nx = (ix - half) / half;
|
|
2225
|
+
const ny = (iy - half) / half;
|
|
2226
|
+
const d = Math.hypot(nx, ny);
|
|
2227
|
+
if (d > 1.12) continue;
|
|
2228
|
+
let blended = 0;
|
|
2229
|
+
for (const s of STATES) {
|
|
2230
|
+
if (weights[s] < .001) continue;
|
|
2231
|
+
blended += weights[s] * intensityOf(s, d, nx, ny, t, amplitude);
|
|
2232
|
+
}
|
|
2233
|
+
const intensity = Math.min(1, Math.max(0, blended));
|
|
2234
|
+
const radius = maxRadius * Math.exp(-d * d * 1.7) * intensity * scale;
|
|
2235
|
+
if (radius * dpr < .5) continue;
|
|
2236
|
+
ctx.beginPath();
|
|
2237
|
+
ctx.arc(center + (ix - half) * spacing * scale, center + (iy - half) * spacing * scale, radius, 0, TAU);
|
|
2238
|
+
ctx.fill();
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
if (typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
|
2242
|
+
redrawRef.current = () => {
|
|
2243
|
+
const current = stateRef.current;
|
|
2244
|
+
for (const s of STATES) weights[s] = s === current ? 1 : 0;
|
|
2245
|
+
draw(0, envelope(0), SCALE[current]);
|
|
2246
|
+
};
|
|
2247
|
+
redrawRef.current();
|
|
2248
|
+
return () => {
|
|
2249
|
+
redrawRef.current = null;
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
let t = 0;
|
|
2253
|
+
let amplitude = 0;
|
|
2254
|
+
let scale = SCALE[stateRef.current];
|
|
2255
|
+
let velocity = 0;
|
|
2256
|
+
let last = performance.now();
|
|
2257
|
+
let raf = 0;
|
|
2258
|
+
const frame = (now) => {
|
|
2259
|
+
const dt = Math.min((now - last) / 1e3, .05);
|
|
2260
|
+
last = now;
|
|
2261
|
+
t += dt;
|
|
2262
|
+
const current = stateRef.current;
|
|
2263
|
+
const target = envelope(t);
|
|
2264
|
+
const rate = target > amplitude ? ATTACK : RELEASE;
|
|
2265
|
+
amplitude += (target - amplitude) * (1 - Math.pow(1 - rate, dt * 60));
|
|
2266
|
+
const step = 1 - Math.pow(.84, dt * 60);
|
|
2267
|
+
for (const s of STATES) weights[s] += ((s === current ? 1 : 0) - weights[s]) * step;
|
|
2268
|
+
velocity += (-180 * (scale - SCALE[current]) - DAMPING * velocity) * dt;
|
|
2269
|
+
scale += velocity * dt;
|
|
2270
|
+
draw(t, amplitude, scale);
|
|
2271
|
+
raf = requestAnimationFrame(frame);
|
|
2272
|
+
};
|
|
2273
|
+
raf = requestAnimationFrame(frame);
|
|
2274
|
+
return () => cancelAnimationFrame(raf);
|
|
2275
|
+
}, [color, dpr]);
|
|
2276
|
+
(0, react.useEffect)(() => {
|
|
2277
|
+
redrawRef.current?.();
|
|
2278
|
+
}, [state]);
|
|
2279
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
|
|
2280
|
+
ref: canvasRef,
|
|
2281
|
+
"aria-hidden": true,
|
|
2282
|
+
className: "dsgc-orb",
|
|
2283
|
+
style: {
|
|
2284
|
+
width: SIZE,
|
|
2285
|
+
height: SIZE
|
|
2286
|
+
}
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
//#endregion
|
|
2128
2290
|
//#region src/client/components/MessageFlow.tsx
|
|
2129
2291
|
function MessageFlow(props) {
|
|
2130
2292
|
const { snap, sess, busyNow, msgById, action, onRetrySpeak } = props;
|
|
@@ -2133,15 +2295,20 @@ window.__ModuleLoader__.load({
|
|
|
2133
2295
|
const replaceMsg = replaceId ? msgById[replaceId] : null;
|
|
2134
2296
|
const liveRoleId = snap.run.currentRoleId || replaceMsg && (replaceMsg.failedRoleId || replaceMsg.speaker) || null;
|
|
2135
2297
|
const lr = busyNow && liveRoleId ? roleById(snap, liveRoleId) : null;
|
|
2298
|
+
const liveColor = lr ? lr.color || "#888" : "#888";
|
|
2299
|
+
const orbState = snap.run.partial ? "listening" : "thinking";
|
|
2136
2300
|
const live = lr ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2137
2301
|
className: "dsgc-msg live",
|
|
2138
2302
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2139
2303
|
className: "dsgc-avatar",
|
|
2140
2304
|
style: {
|
|
2141
|
-
border: "2px solid " +
|
|
2142
|
-
"--role-color":
|
|
2305
|
+
border: "2px solid " + liveColor,
|
|
2306
|
+
"--role-color": liveColor
|
|
2143
2307
|
},
|
|
2144
|
-
children:
|
|
2308
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpeakerOrb, {
|
|
2309
|
+
state: orbState,
|
|
2310
|
+
color: liveColor
|
|
2311
|
+
})
|
|
2145
2312
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2146
2313
|
className: "dsgc-msgbody",
|
|
2147
2314
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|