@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 " + (lr.color || "#888"),
2142
- "--role-color": lr.color || "#888"
2305
+ border: "2px solid " + liveColor,
2306
+ "--role-color": liveColor
2143
2307
  },
2144
- children: lr.name.slice(0, 1)
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", {