@medalsocial/meda 0.1.1 → 0.3.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.
Files changed (73) hide show
  1. package/README.md +7 -2
  2. package/dist/chat/index.d.ts +11 -0
  3. package/dist/chat/index.js +5 -0
  4. package/dist/chat/latency-badge.d.ts +7 -0
  5. package/dist/chat/latency-badge.js +19 -0
  6. package/dist/chat/latency-breakdown.d.ts +8 -0
  7. package/dist/chat/latency-breakdown.js +9 -0
  8. package/dist/chat/public.d.ts +1 -0
  9. package/dist/chat/public.js +1 -0
  10. package/dist/chat/tool-call-block.d.ts +6 -0
  11. package/dist/chat/tool-call-block.js +19 -0
  12. package/dist/chat/transcript-stream.d.ts +10 -0
  13. package/dist/chat/transcript-stream.js +40 -0
  14. package/dist/chat/turn-card.d.ts +10 -0
  15. package/dist/chat/turn-card.js +23 -0
  16. package/dist/chat/types.d.ts +36 -0
  17. package/dist/chat/types.js +1 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.js +3 -0
  20. package/dist/lib/format-time.d.ts +9 -0
  21. package/dist/lib/format-time.js +34 -0
  22. package/dist/panel/index.d.ts +6 -0
  23. package/dist/panel/index.js +3 -0
  24. package/dist/panel/inspector-field.d.ts +9 -0
  25. package/dist/panel/inspector-field.js +4 -0
  26. package/dist/panel/inspector-json.d.ts +7 -0
  27. package/dist/panel/inspector-json.js +73 -0
  28. package/dist/panel/inspector.d.ts +7 -0
  29. package/dist/panel/inspector.js +12 -0
  30. package/dist/panel/public.d.ts +1 -0
  31. package/dist/panel/public.js +1 -0
  32. package/dist/panel/types.d.ts +6 -0
  33. package/dist/panel/types.js +1 -0
  34. package/dist/styles.css +22 -0
  35. package/dist/timeline/date-switcher.d.ts +11 -0
  36. package/dist/timeline/date-switcher.js +62 -0
  37. package/dist/timeline/event-card.d.ts +9 -0
  38. package/dist/timeline/event-card.js +26 -0
  39. package/dist/timeline/index.d.ts +13 -0
  40. package/dist/timeline/index.js +6 -0
  41. package/dist/timeline/live-indicator.d.ts +7 -0
  42. package/dist/timeline/live-indicator.js +5 -0
  43. package/dist/timeline/public.d.ts +1 -0
  44. package/dist/timeline/public.js +1 -0
  45. package/dist/timeline/scrub-bar.d.ts +14 -0
  46. package/dist/timeline/scrub-bar.js +44 -0
  47. package/dist/timeline/timeline-rail.d.ts +19 -0
  48. package/dist/timeline/timeline-rail.js +64 -0
  49. package/dist/timeline/timeline-tape.d.ts +19 -0
  50. package/dist/timeline/timeline-tape.js +66 -0
  51. package/dist/timeline/types.d.ts +40 -0
  52. package/dist/timeline/types.js +1 -0
  53. package/dist/voice/index.d.ts +9 -0
  54. package/dist/voice/index.js +4 -0
  55. package/dist/voice/public.d.ts +1 -0
  56. package/dist/voice/public.js +1 -0
  57. package/dist/voice/types.d.ts +8 -0
  58. package/dist/voice/types.js +1 -0
  59. package/dist/voice/use-mic-capture.d.ts +14 -0
  60. package/dist/voice/use-mic-capture.js +185 -0
  61. package/dist/voice/voice-level.d.ts +8 -0
  62. package/dist/voice/voice-level.js +29 -0
  63. package/dist/voice/voice-orb-scene.d.ts +12 -0
  64. package/dist/voice/voice-orb-scene.js +121 -0
  65. package/dist/voice/voice-orb-shader.d.ts +2 -0
  66. package/dist/voice/voice-orb-shader.js +255 -0
  67. package/dist/voice/voice-orb-volume.d.ts +12 -0
  68. package/dist/voice/voice-orb-volume.js +21 -0
  69. package/dist/voice/voice-orb.d.ts +45 -0
  70. package/dist/voice/voice-orb.js +123 -0
  71. package/dist/voice/voice-status-pill.d.ts +7 -0
  72. package/dist/voice/voice-status-pill.js +26 -0
  73. package/package.json +36 -3
@@ -0,0 +1,121 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // Portions adapted from ElevenLabs UI's Orb component (MIT):
4
+ // https://github.com/elevenlabs/ui/blob/main/apps/www/registry/elevenlabs-ui/ui/orb.tsx
5
+ // Copyright (c) ElevenLabs Inc.
6
+ // Adaptations: Meda token-driven theming, 5-phase state model, outputLevel uniform,
7
+ // procedural simplex noise (no texture asset required).
8
+ import { useFrame } from '@react-three/fiber';
9
+ import * as React from 'react';
10
+ import * as THREE from 'three';
11
+ import { FRAGMENT_SHADER, VERTEX_SHADER } from './voice-orb-shader.js';
12
+ import { createVolumeSmoother } from './voice-orb-volume.js';
13
+ // Maps phase name to numeric target for uPhaseT
14
+ const PHASE_TARGET = {
15
+ idle: 0,
16
+ listening: 1,
17
+ thinking: 2,
18
+ speaking: 3,
19
+ error: 4,
20
+ };
21
+ // splitmix32 — fast deterministic PRNG seeded at mount
22
+ function splitmix32(seed) {
23
+ let a = seed;
24
+ return () => {
25
+ a = (a + 0x9e3779b9) | 0;
26
+ let t = a ^ (a >>> 16);
27
+ t = Math.imul(t, 0x21f0aaad);
28
+ t = t ^ (t >>> 15);
29
+ t = Math.imul(t, 0x735a2d97);
30
+ t = t ^ (t >>> 15);
31
+ return (t >>> 0) / 4294967296;
32
+ };
33
+ }
34
+ export function Scene({ level = 0, outputLevel = 0, phase = 'idle', colors, reducedMotion = false, pressed = false, variant = 'aurora', }) {
35
+ const meshRef = React.useRef(null);
36
+ // Smoothers for audio signals
37
+ const inputSmoother = React.useRef(createVolumeSmoother());
38
+ const outputSmoother = React.useRef(createVolumeSmoother());
39
+ // Transition state refs (avoid re-renders)
40
+ const phaseTRef = React.useRef(PHASE_TARGET[phase]);
41
+ const errorFlashRef = React.useRef(0);
42
+ const pressedRef = React.useRef(pressed ? 1 : 0);
43
+ const prevPhaseRef = React.useRef(phase);
44
+ // Color targets
45
+ const colorARef = React.useRef(new THREE.Color(colors[0]));
46
+ const colorBRef = React.useRef(new THREE.Color(colors[1]));
47
+ // Sync latest prop values to refs
48
+ React.useEffect(() => {
49
+ colorARef.current.set(colors[0]);
50
+ colorBRef.current.set(colors[1]);
51
+ }, [colors]);
52
+ React.useEffect(() => {
53
+ // Trigger error flash envelope when entering error phase
54
+ if (phase === 'error' && prevPhaseRef.current !== 'error') {
55
+ errorFlashRef.current = 1;
56
+ }
57
+ prevPhaseRef.current = phase;
58
+ }, [phase]);
59
+ // Random offsets for the 7 oval blobs (seeded once at mount)
60
+ const offsets = React.useMemo(() => {
61
+ const rng = splitmix32(Math.floor(Math.random() * 2 ** 32));
62
+ return new Float32Array(Array.from({ length: 7 }, () => rng() * Math.PI * 2));
63
+ }, []);
64
+ // Build uniforms once — intentionally empty dep array; all values are mutated
65
+ // in useFrame each tick. Initial values come from props captured at mount.
66
+ // biome-ignore lint/correctness/useExhaustiveDependencies: uniform object is mutated in useFrame; re-creating it would reset animation state
67
+ const uniforms = React.useMemo(() => ({
68
+ uTime: { value: 0 },
69
+ uAnimation: { value: 0.1 },
70
+ uInputVolume: { value: 0 },
71
+ uOutputVolume: { value: 0 },
72
+ uColorA: new THREE.Uniform(new THREE.Color(colors[0])),
73
+ uColorB: new THREE.Uniform(new THREE.Color(colors[1])),
74
+ uPhaseT: { value: PHASE_TARGET[phase] },
75
+ uErrorFlash: { value: 0 },
76
+ uPressed: { value: 0 },
77
+ uOpacity: { value: 0 },
78
+ uOffsets: { value: offsets },
79
+ uVariant: { value: variant === 'aurora' ? 1 : 0 },
80
+ }), [] // intentionally empty — uniforms are mutated in useFrame
81
+ );
82
+ useFrame((_, delta) => {
83
+ const mat = meshRef.current?.material;
84
+ if (!mat)
85
+ return;
86
+ const u = mat.uniforms;
87
+ // Fade in on first render
88
+ if (u.uOpacity.value < 1) {
89
+ u.uOpacity.value = Math.min(1, u.uOpacity.value + delta * 2);
90
+ }
91
+ // Time & animation — frozen under reduced motion
92
+ if (!reducedMotion) {
93
+ u.uTime.value += delta * 0.5;
94
+ u.uAnimation.value += delta * (0.1 + (1 - (u.uOutputVolume.value - 1) ** 2) * 0.9);
95
+ }
96
+ // Smooth audio signals; clamp to 0 under reduced motion
97
+ inputSmoother.current.update(reducedMotion ? 0 : Math.min(1, Math.max(0, level)));
98
+ outputSmoother.current.update(reducedMotion ? 0 : Math.min(1, Math.max(0, outputLevel)));
99
+ u.uInputVolume.value = inputSmoother.current.value;
100
+ u.uOutputVolume.value = outputSmoother.current.value;
101
+ // Phase interpolation — 220ms transition
102
+ const phaseTarget = PHASE_TARGET[phase];
103
+ phaseTRef.current += (phaseTarget - phaseTRef.current) * Math.min(1, delta / 0.22);
104
+ u.uPhaseT.value = phaseTRef.current;
105
+ // Error flash decay over ~1.5s
106
+ if (errorFlashRef.current > 0) {
107
+ errorFlashRef.current = Math.max(0, errorFlashRef.current - delta / 1.5);
108
+ u.uErrorFlash.value = errorFlashRef.current;
109
+ }
110
+ // Press envelope
111
+ const pressTarget = pressed ? 1 : 0;
112
+ pressedRef.current += (pressTarget - pressedRef.current) * Math.min(1, delta / 0.1);
113
+ u.uPressed.value = pressedRef.current;
114
+ // Color lerp
115
+ u.uColorA.value.lerp(colorARef.current, 0.08);
116
+ u.uColorB.value.lerp(colorBRef.current, 0.08);
117
+ // Variant — switched live (no animation; consumer changes are instant)
118
+ u.uVariant.value = variant === 'aurora' ? 1 : 0;
119
+ });
120
+ return (_jsxs("mesh", { ref: meshRef, children: [_jsx("circleGeometry", { args: [3.5, 64] }), _jsx("shaderMaterial", { uniforms: uniforms, vertexShader: VERTEX_SHADER, fragmentShader: FRAGMENT_SHADER, transparent: true })] }));
121
+ }
@@ -0,0 +1,2 @@
1
+ export declare const VERTEX_SHADER = "\nuniform float uTime;\nvarying vec2 vUv;\n\nvoid main() {\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n";
2
+ export declare const FRAGMENT_SHADER = "\n\nvec3 mod289_3(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }\nvec4 mod289_4(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }\nvec4 permute(vec4 x) { return mod289_4(((x * 34.0) + 10.0) * x); }\nvec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }\n\nfloat snoise(vec3 v) {\n const vec2 C = vec2(1.0/6.0, 1.0/3.0);\n const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);\n\n vec3 i = floor(v + dot(v, C.yyy));\n vec3 x0 = v - i + dot(i, C.xxx);\n\n vec3 g = step(x0.yzx, x0.xyz);\n vec3 l = 1.0 - g;\n vec3 i1 = min(g.xyz, l.zxy);\n vec3 i2 = max(g.xyz, l.zxy);\n\n vec3 x1 = x0 - i1 + C.xxx;\n vec3 x2 = x0 - i2 + C.yyy;\n vec3 x3 = x0 - D.yyy;\n\n i = mod289_3(i);\n vec4 p = permute(permute(permute(\n i.z + vec4(0.0, i1.z, i2.z, 1.0))\n + i.y + vec4(0.0, i1.y, i2.y, 1.0))\n + i.x + vec4(0.0, i1.x, i2.x, 1.0));\n\n float n_ = 0.142857142857;\n vec3 ns = n_ * D.wyz - D.xzx;\n\n vec4 j = p - 49.0 * floor(p * ns.z * ns.z);\n\n vec4 x_ = floor(j * ns.z);\n vec4 y_ = floor(j - 7.0 * x_);\n\n vec4 x = x_ * ns.x + ns.yyyy;\n vec4 y = y_ * ns.x + ns.yyyy;\n vec4 h = 1.0 - abs(x) - abs(y);\n\n vec4 b0 = vec4(x.xy, y.xy);\n vec4 b1 = vec4(x.zw, y.zw);\n\n vec4 s0 = floor(b0) * 2.0 + 1.0;\n vec4 s1 = floor(b1) * 2.0 + 1.0;\n vec4 sh = -step(h, vec4(0.0));\n\n vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;\n vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;\n\n vec3 p0 = vec3(a0.xy, h.x);\n vec3 p1 = vec3(a0.zw, h.y);\n vec3 p2 = vec3(a1.xy, h.z);\n vec3 p3 = vec3(a1.zw, h.w);\n\n vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));\n p0 *= norm.x;\n p1 *= norm.y;\n p2 *= norm.z;\n p3 *= norm.w;\n\n vec4 m = max(0.5 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);\n m = m * m;\n return 105.0 * dot(m * m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));\n}\n\n// Fractional Brownian Motion: stacked octaves of simplex noise\nfloat fbm(vec3 p) {\n float value = 0.0;\n float amplitude = 0.5;\n float frequency = 1.0;\n for (int i = 0; i < 4; i++) {\n value += amplitude * snoise(p * frequency);\n amplitude *= 0.5;\n frequency *= 2.0;\n }\n return value;\n}\n\n\nuniform float uTime;\nuniform float uAnimation;\nuniform float uInputVolume;\nuniform float uOutputVolume;\nuniform vec3 uColorA;\nuniform vec3 uColorB;\nuniform float uPhaseT;\nuniform float uErrorFlash;\nuniform float uPressed;\nuniform float uOpacity;\nuniform float uVariant; // 0 = metal, 1 = aurora\n\nconst vec3 ERROR_COLOR = vec3(0.85, 0.18, 0.18);\nconst float PI = 3.14159265358979323846;\n\nvarying vec2 vUv;\n\n// =============================================================================\n// METAL look \u2014 smooth chrome-like sphere with strong specular + depth shading.\n// Apple Siri / Vision Pro adjacent.\n// =============================================================================\nvec4 metalLook(vec2 uv, float r, vec3 cA, vec3 cB, float drive) {\n // Two octaves of noise for surface variation\n float n = fbm(vec3(uv * 1.4, uAnimation * 0.18)) * 0.5 + 0.5;\n float n2 = fbm(vec3(uv * 2.6 + 4.7, uAnimation * 0.10)) * 0.5 + 0.5;\n\n // Vertical light bias \u2014 top brighter than bottom (3D illusion)\n float topLight = smoothstep(-1.0, 1.0, uv.y * 0.6 + 0.4);\n float mixT = clamp(n * 0.55 + topLight * 0.45 + n2 * 0.08, 0.0, 1.0);\n\n // Light cream color for the highlight band \u2014 gives the chrome look.\n // Brightens cA toward white for the upper portion of the sphere.\n vec3 highlightTone = mix(cA, vec3(0.95, 0.92, 0.98), 0.55);\n vec3 deepTone = mix(cB, vec3(0.04, 0.02, 0.10), 0.55);\n\n // 4-stop ramp: cream highlight \u2192 cA \u2192 cB \u2192 deep core\n vec3 base;\n if (mixT > 0.66) {\n base = mix(cA, highlightTone, (mixT - 0.66) / 0.34);\n } else if (mixT > 0.33) {\n base = mix(cB, cA, (mixT - 0.33) / 0.33);\n } else {\n base = mix(deepTone, cB, mixT / 0.33);\n }\n\n // Specular: tight bright spot top-left\n vec2 specCenter = vec2(-0.35, 0.45);\n float spec = smoothstep(0.45, 0.0, length(uv - specCenter));\n base += vec3(1.0) * spec * 0.32;\n\n // Bottom shadow for depth\n float shadow = smoothstep(0.3, 1.0, -uv.y) * 0.35;\n base = mix(base, base * 0.55, shadow);\n\n // Inner glow on audio\n float glow = (1.0 - r) * (1.0 - r);\n base += cA * drive * glow * 0.5;\n\n return vec4(base, 1.0);\n}\n\n// =============================================================================\n// AURORA look \u2014 soft layered ribbons drifting across a translucent disc.\n// Northern-lights aesthetic. Calm, atmospheric.\n// =============================================================================\nvec4 auroraLook(vec2 uv, float r, vec3 cA, vec3 cB, float drive) {\n float t = uAnimation * 0.25;\n\n // Disc base \u2014 deep, translucent. cB darkened.\n vec3 baseColor = mix(cB * 0.25, cB * 0.55, smoothstep(1.0, 0.0, r));\n\n // Three drifting ribbons at different y-offsets, angles, and speeds.\n // Each ribbon is a horizontally-stretched soft band whose vertical position\n // oscillates with time + noise.\n\n // Ribbon 1 \u2014 upper, primary color\n float y1 = uv.y - (sin(t * 0.7 + uv.x * 1.5) * 0.18) - 0.15;\n float band1 = exp(-pow(y1 / 0.18, 2.0));\n float n1 = fbm(vec3(uv.x * 2.0 + t, uv.y * 0.8, t * 0.4)) * 0.5 + 0.5;\n vec3 ribbon1 = cA * band1 * n1 * 1.4;\n\n // Ribbon 2 \u2014 middle, accent color (cooler \u2014 shift cA toward cyan)\n vec3 cool = mix(cA, vec3(0.36, 0.83, 1.0), 0.45);\n float y2 = uv.y - (sin(t * 0.55 + uv.x * 2.2 + 1.7) * 0.22) + 0.05;\n float band2 = exp(-pow(y2 / 0.14, 2.0));\n float n2 = fbm(vec3(uv.x * 2.6 + t * 0.6 + 3.1, uv.y * 0.6, t * 0.3)) * 0.5 + 0.5;\n vec3 ribbon2 = cool * band2 * n2 * 1.0;\n\n // Ribbon 3 \u2014 lower, warm accent (shift cA toward pink)\n vec3 warm = mix(cA, vec3(1.0, 0.49, 0.83), 0.55);\n float y3 = uv.y - (sin(t * 0.4 + uv.x * 1.8 + 4.2) * 0.18) + 0.30;\n float band3 = exp(-pow(y3 / 0.16, 2.0));\n float n3 = fbm(vec3(uv.x * 2.2 + t * 0.45 + 7.9, uv.y * 0.7, t * 0.35)) * 0.5 + 0.5;\n vec3 ribbon3 = warm * band3 * n3 * 0.85;\n\n // Stack ribbons additively over the disc\n vec3 base = baseColor + ribbon1 + ribbon2 + ribbon3;\n\n // Audio drive intensifies all ribbons + brightens center\n base += (ribbon1 + ribbon2 + ribbon3) * drive * 0.6;\n float glow = (1.0 - r) * (1.0 - r);\n base += cA * drive * glow * 0.35;\n\n // Subtle inner rim accent for legibility against dark backgrounds\n float rim = smoothstep(0.82, 0.98, r);\n base += cA * rim * 0.22;\n\n return vec4(base, 1.0);\n}\n\nvoid main() {\n vec2 uv = vUv * 2.0 - 1.0;\n float r = length(uv);\n\n // Discard fragments outside the unit circle so the canvas stays clean.\n if (r > 1.0) {\n gl_FragColor = vec4(0.0);\n return;\n }\n\n // ---- Phase masks (0..1 for each phase) ---------------------------------\n // uPhaseT smoothly interpolates between numeric phases:\n // 0=idle, 1=listening, 2=thinking, 3=speaking, 4=error.\n float isListening = clamp(1.0 - abs(uPhaseT - 1.0), 0.0, 1.0);\n float isThinking = clamp(1.0 - abs(uPhaseT - 2.0), 0.0, 1.0);\n float isSpeaking = clamp(1.0 - abs(uPhaseT - 3.0), 0.0, 1.0);\n\n float inputDrive = uInputVolume * isListening;\n float outputDrive = uOutputVolume * isSpeaking;\n float thinkPulse = isThinking * (0.5 + 0.5 * sin(uTime * 2.0));\n float drive = max(inputDrive, max(outputDrive, thinkPulse * 0.4));\n\n // ---- Resolve effective colors with error tint --------------------------\n vec3 cA = mix(uColorA, ERROR_COLOR, uErrorFlash);\n vec3 cB = mix(uColorB, ERROR_COLOR * 0.55, uErrorFlash * 0.7);\n\n // ---- Pick variant ------------------------------------------------------\n vec4 result;\n if (uVariant > 0.5) {\n result = auroraLook(uv, r, cA, cB, drive);\n } else {\n result = metalLook(uv, r, cA, cB, drive);\n }\n\n vec3 base = result.rgb;\n\n // ---- Press: slight core brighten ---------------------------------------\n base = mix(base, base * 1.18, uPressed * (1.0 - smoothstep(0.0, 0.55, r)));\n\n // ---- Edge falloff: smooth alpha at the disc boundary -------------------\n float alpha = smoothstep(1.0, 0.92, r);\n\n gl_FragColor = vec4(base, alpha * uOpacity);\n}\n";
@@ -0,0 +1,255 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Portions adapted from ElevenLabs UI's Orb component (MIT):
3
+ // https://github.com/elevenlabs/ui/blob/main/apps/www/registry/elevenlabs-ui/ui/orb.tsx
4
+ // Copyright (c) ElevenLabs Inc.
5
+ // Adaptations: Meda token-driven theming, 5-phase state model (idle/listening/thinking/
6
+ // speaking/error), outputLevel uniform, procedural simplex noise (replaces texture sampling).
7
+ // Simplex noise implementation based on Stefan Gustavson's public-domain GLSL noise.
8
+ export const VERTEX_SHADER = /* glsl */ `
9
+ uniform float uTime;
10
+ varying vec2 vUv;
11
+
12
+ void main() {
13
+ vUv = uv;
14
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
15
+ }
16
+ `;
17
+ // Stefan Gustavson's simplex noise — public domain
18
+ // https://github.com/stegu/webgl-noise (public domain)
19
+ const SIMPLEX_NOISE_GLSL = /* glsl */ `
20
+ vec3 mod289_3(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
21
+ vec4 mod289_4(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
22
+ vec4 permute(vec4 x) { return mod289_4(((x * 34.0) + 10.0) * x); }
23
+ vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
24
+
25
+ float snoise(vec3 v) {
26
+ const vec2 C = vec2(1.0/6.0, 1.0/3.0);
27
+ const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
28
+
29
+ vec3 i = floor(v + dot(v, C.yyy));
30
+ vec3 x0 = v - i + dot(i, C.xxx);
31
+
32
+ vec3 g = step(x0.yzx, x0.xyz);
33
+ vec3 l = 1.0 - g;
34
+ vec3 i1 = min(g.xyz, l.zxy);
35
+ vec3 i2 = max(g.xyz, l.zxy);
36
+
37
+ vec3 x1 = x0 - i1 + C.xxx;
38
+ vec3 x2 = x0 - i2 + C.yyy;
39
+ vec3 x3 = x0 - D.yyy;
40
+
41
+ i = mod289_3(i);
42
+ vec4 p = permute(permute(permute(
43
+ i.z + vec4(0.0, i1.z, i2.z, 1.0))
44
+ + i.y + vec4(0.0, i1.y, i2.y, 1.0))
45
+ + i.x + vec4(0.0, i1.x, i2.x, 1.0));
46
+
47
+ float n_ = 0.142857142857;
48
+ vec3 ns = n_ * D.wyz - D.xzx;
49
+
50
+ vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
51
+
52
+ vec4 x_ = floor(j * ns.z);
53
+ vec4 y_ = floor(j - 7.0 * x_);
54
+
55
+ vec4 x = x_ * ns.x + ns.yyyy;
56
+ vec4 y = y_ * ns.x + ns.yyyy;
57
+ vec4 h = 1.0 - abs(x) - abs(y);
58
+
59
+ vec4 b0 = vec4(x.xy, y.xy);
60
+ vec4 b1 = vec4(x.zw, y.zw);
61
+
62
+ vec4 s0 = floor(b0) * 2.0 + 1.0;
63
+ vec4 s1 = floor(b1) * 2.0 + 1.0;
64
+ vec4 sh = -step(h, vec4(0.0));
65
+
66
+ vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
67
+ vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
68
+
69
+ vec3 p0 = vec3(a0.xy, h.x);
70
+ vec3 p1 = vec3(a0.zw, h.y);
71
+ vec3 p2 = vec3(a1.xy, h.z);
72
+ vec3 p3 = vec3(a1.zw, h.w);
73
+
74
+ vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));
75
+ p0 *= norm.x;
76
+ p1 *= norm.y;
77
+ p2 *= norm.z;
78
+ p3 *= norm.w;
79
+
80
+ vec4 m = max(0.5 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
81
+ m = m * m;
82
+ return 105.0 * dot(m * m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));
83
+ }
84
+
85
+ // Fractional Brownian Motion: stacked octaves of simplex noise
86
+ float fbm(vec3 p) {
87
+ float value = 0.0;
88
+ float amplitude = 0.5;
89
+ float frequency = 1.0;
90
+ for (int i = 0; i < 4; i++) {
91
+ value += amplitude * snoise(p * frequency);
92
+ amplitude *= 0.5;
93
+ frequency *= 2.0;
94
+ }
95
+ return value;
96
+ }
97
+ `;
98
+ export const FRAGMENT_SHADER = /* glsl */ `
99
+ ${SIMPLEX_NOISE_GLSL}
100
+
101
+ uniform float uTime;
102
+ uniform float uAnimation;
103
+ uniform float uInputVolume;
104
+ uniform float uOutputVolume;
105
+ uniform vec3 uColorA;
106
+ uniform vec3 uColorB;
107
+ uniform float uPhaseT;
108
+ uniform float uErrorFlash;
109
+ uniform float uPressed;
110
+ uniform float uOpacity;
111
+ uniform float uVariant; // 0 = metal, 1 = aurora
112
+
113
+ const vec3 ERROR_COLOR = vec3(0.85, 0.18, 0.18);
114
+ const float PI = 3.14159265358979323846;
115
+
116
+ varying vec2 vUv;
117
+
118
+ // =============================================================================
119
+ // METAL look — smooth chrome-like sphere with strong specular + depth shading.
120
+ // Apple Siri / Vision Pro adjacent.
121
+ // =============================================================================
122
+ vec4 metalLook(vec2 uv, float r, vec3 cA, vec3 cB, float drive) {
123
+ // Two octaves of noise for surface variation
124
+ float n = fbm(vec3(uv * 1.4, uAnimation * 0.18)) * 0.5 + 0.5;
125
+ float n2 = fbm(vec3(uv * 2.6 + 4.7, uAnimation * 0.10)) * 0.5 + 0.5;
126
+
127
+ // Vertical light bias — top brighter than bottom (3D illusion)
128
+ float topLight = smoothstep(-1.0, 1.0, uv.y * 0.6 + 0.4);
129
+ float mixT = clamp(n * 0.55 + topLight * 0.45 + n2 * 0.08, 0.0, 1.0);
130
+
131
+ // Light cream color for the highlight band — gives the chrome look.
132
+ // Brightens cA toward white for the upper portion of the sphere.
133
+ vec3 highlightTone = mix(cA, vec3(0.95, 0.92, 0.98), 0.55);
134
+ vec3 deepTone = mix(cB, vec3(0.04, 0.02, 0.10), 0.55);
135
+
136
+ // 4-stop ramp: cream highlight → cA → cB → deep core
137
+ vec3 base;
138
+ if (mixT > 0.66) {
139
+ base = mix(cA, highlightTone, (mixT - 0.66) / 0.34);
140
+ } else if (mixT > 0.33) {
141
+ base = mix(cB, cA, (mixT - 0.33) / 0.33);
142
+ } else {
143
+ base = mix(deepTone, cB, mixT / 0.33);
144
+ }
145
+
146
+ // Specular: tight bright spot top-left
147
+ vec2 specCenter = vec2(-0.35, 0.45);
148
+ float spec = smoothstep(0.45, 0.0, length(uv - specCenter));
149
+ base += vec3(1.0) * spec * 0.32;
150
+
151
+ // Bottom shadow for depth
152
+ float shadow = smoothstep(0.3, 1.0, -uv.y) * 0.35;
153
+ base = mix(base, base * 0.55, shadow);
154
+
155
+ // Inner glow on audio
156
+ float glow = (1.0 - r) * (1.0 - r);
157
+ base += cA * drive * glow * 0.5;
158
+
159
+ return vec4(base, 1.0);
160
+ }
161
+
162
+ // =============================================================================
163
+ // AURORA look — soft layered ribbons drifting across a translucent disc.
164
+ // Northern-lights aesthetic. Calm, atmospheric.
165
+ // =============================================================================
166
+ vec4 auroraLook(vec2 uv, float r, vec3 cA, vec3 cB, float drive) {
167
+ float t = uAnimation * 0.25;
168
+
169
+ // Disc base — deep, translucent. cB darkened.
170
+ vec3 baseColor = mix(cB * 0.25, cB * 0.55, smoothstep(1.0, 0.0, r));
171
+
172
+ // Three drifting ribbons at different y-offsets, angles, and speeds.
173
+ // Each ribbon is a horizontally-stretched soft band whose vertical position
174
+ // oscillates with time + noise.
175
+
176
+ // Ribbon 1 — upper, primary color
177
+ float y1 = uv.y - (sin(t * 0.7 + uv.x * 1.5) * 0.18) - 0.15;
178
+ float band1 = exp(-pow(y1 / 0.18, 2.0));
179
+ float n1 = fbm(vec3(uv.x * 2.0 + t, uv.y * 0.8, t * 0.4)) * 0.5 + 0.5;
180
+ vec3 ribbon1 = cA * band1 * n1 * 1.4;
181
+
182
+ // Ribbon 2 — middle, accent color (cooler — shift cA toward cyan)
183
+ vec3 cool = mix(cA, vec3(0.36, 0.83, 1.0), 0.45);
184
+ float y2 = uv.y - (sin(t * 0.55 + uv.x * 2.2 + 1.7) * 0.22) + 0.05;
185
+ float band2 = exp(-pow(y2 / 0.14, 2.0));
186
+ float n2 = fbm(vec3(uv.x * 2.6 + t * 0.6 + 3.1, uv.y * 0.6, t * 0.3)) * 0.5 + 0.5;
187
+ vec3 ribbon2 = cool * band2 * n2 * 1.0;
188
+
189
+ // Ribbon 3 — lower, warm accent (shift cA toward pink)
190
+ vec3 warm = mix(cA, vec3(1.0, 0.49, 0.83), 0.55);
191
+ float y3 = uv.y - (sin(t * 0.4 + uv.x * 1.8 + 4.2) * 0.18) + 0.30;
192
+ float band3 = exp(-pow(y3 / 0.16, 2.0));
193
+ float n3 = fbm(vec3(uv.x * 2.2 + t * 0.45 + 7.9, uv.y * 0.7, t * 0.35)) * 0.5 + 0.5;
194
+ vec3 ribbon3 = warm * band3 * n3 * 0.85;
195
+
196
+ // Stack ribbons additively over the disc
197
+ vec3 base = baseColor + ribbon1 + ribbon2 + ribbon3;
198
+
199
+ // Audio drive intensifies all ribbons + brightens center
200
+ base += (ribbon1 + ribbon2 + ribbon3) * drive * 0.6;
201
+ float glow = (1.0 - r) * (1.0 - r);
202
+ base += cA * drive * glow * 0.35;
203
+
204
+ // Subtle inner rim accent for legibility against dark backgrounds
205
+ float rim = smoothstep(0.82, 0.98, r);
206
+ base += cA * rim * 0.22;
207
+
208
+ return vec4(base, 1.0);
209
+ }
210
+
211
+ void main() {
212
+ vec2 uv = vUv * 2.0 - 1.0;
213
+ float r = length(uv);
214
+
215
+ // Discard fragments outside the unit circle so the canvas stays clean.
216
+ if (r > 1.0) {
217
+ gl_FragColor = vec4(0.0);
218
+ return;
219
+ }
220
+
221
+ // ---- Phase masks (0..1 for each phase) ---------------------------------
222
+ // uPhaseT smoothly interpolates between numeric phases:
223
+ // 0=idle, 1=listening, 2=thinking, 3=speaking, 4=error.
224
+ float isListening = clamp(1.0 - abs(uPhaseT - 1.0), 0.0, 1.0);
225
+ float isThinking = clamp(1.0 - abs(uPhaseT - 2.0), 0.0, 1.0);
226
+ float isSpeaking = clamp(1.0 - abs(uPhaseT - 3.0), 0.0, 1.0);
227
+
228
+ float inputDrive = uInputVolume * isListening;
229
+ float outputDrive = uOutputVolume * isSpeaking;
230
+ float thinkPulse = isThinking * (0.5 + 0.5 * sin(uTime * 2.0));
231
+ float drive = max(inputDrive, max(outputDrive, thinkPulse * 0.4));
232
+
233
+ // ---- Resolve effective colors with error tint --------------------------
234
+ vec3 cA = mix(uColorA, ERROR_COLOR, uErrorFlash);
235
+ vec3 cB = mix(uColorB, ERROR_COLOR * 0.55, uErrorFlash * 0.7);
236
+
237
+ // ---- Pick variant ------------------------------------------------------
238
+ vec4 result;
239
+ if (uVariant > 0.5) {
240
+ result = auroraLook(uv, r, cA, cB, drive);
241
+ } else {
242
+ result = metalLook(uv, r, cA, cB, drive);
243
+ }
244
+
245
+ vec3 base = result.rgb;
246
+
247
+ // ---- Press: slight core brighten ---------------------------------------
248
+ base = mix(base, base * 1.18, uPressed * (1.0 - smoothstep(0.0, 0.55, r)));
249
+
250
+ // ---- Edge falloff: smooth alpha at the disc boundary -------------------
251
+ float alpha = smoothstep(1.0, 0.92, r);
252
+
253
+ gl_FragColor = vec4(base, alpha * uOpacity);
254
+ }
255
+ `;
@@ -0,0 +1,12 @@
1
+ export interface VolumeSmoother {
2
+ /** Call each frame with the raw 0..1 signal. */
3
+ update(raw: number): void;
4
+ /** Current smoothed value 0..1. */
5
+ readonly value: number;
6
+ }
7
+ /**
8
+ * Creates a volume smoother with asymmetric attack/decay envelopes.
9
+ * @param attackAlpha - Per-frame alpha toward higher values (default 0.85 ≈ 50ms at 60Hz)
10
+ * @param decayAlpha - Per-frame alpha toward lower values (default 0.30 ≈ 300ms at 60Hz)
11
+ */
12
+ export declare function createVolumeSmoother(attackAlpha?: number, decayAlpha?: number): VolumeSmoother;
@@ -0,0 +1,21 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Volume signal smoother: fast attack (~50ms), slow decay (~300ms).
3
+ // Attack/decay constants are frame-rate independent when called at ~60Hz.
4
+ /**
5
+ * Creates a volume smoother with asymmetric attack/decay envelopes.
6
+ * @param attackAlpha - Per-frame alpha toward higher values (default 0.85 ≈ 50ms at 60Hz)
7
+ * @param decayAlpha - Per-frame alpha toward lower values (default 0.30 ≈ 300ms at 60Hz)
8
+ */
9
+ export function createVolumeSmoother(attackAlpha = 0.85, decayAlpha = 0.3) {
10
+ let _value = 0;
11
+ return {
12
+ update(raw) {
13
+ const clamped = Math.min(1, Math.max(0, raw));
14
+ const alpha = clamped > _value ? attackAlpha : decayAlpha;
15
+ _value = _value + (clamped - _value) * alpha;
16
+ },
17
+ get value() {
18
+ return _value;
19
+ },
20
+ };
21
+ }
@@ -0,0 +1,45 @@
1
+ import * as React from 'react';
2
+ import type { TurnPhase } from './types.js';
3
+ import { type VoiceOrbVariant } from './voice-orb-scene.js';
4
+ export type { VoiceOrbVariant } from './voice-orb-scene.js';
5
+ /**
6
+ * Convert any CSS color token to a hex string the Three.js shader can consume.
7
+ *
8
+ * Handles:
9
+ * - hex: #RGB / #RRGGBB / #RRGGBBAA
10
+ * - Tailwind/shadcn space-separated HSL: "271 36% 60%"
11
+ * - hsl(), rgb(), oklch(), color(), var() — resolved via browser CSSOM
12
+ */
13
+ export declare function parseColor(value: string, fallback?: string): string;
14
+ export interface VoiceOrbProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
15
+ /** Held / not held. Drives the squish + halo intensity. */
16
+ pressed: boolean;
17
+ /** 0..1 mic input level. Drives shader input-volume uniform. */
18
+ level?: number;
19
+ /** 0..1 TTS playback level. Drives shader output-volume uniform. */
20
+ outputLevel?: number;
21
+ /** Visual phase. Drives state-specific shader behavior. */
22
+ phase?: TurnPhase;
23
+ /** Diameter in px. Defaults to 144. */
24
+ size?: number;
25
+ disabled?: boolean;
26
+ /** A11y label. Defaults to "Hold to talk". */
27
+ label?: string;
28
+ /**
29
+ * Override the gradient pair [colorA, colorB]. If omitted, reads --primary
30
+ * and --accent from CSS at mount and on theme change.
31
+ */
32
+ colors?: [string, string];
33
+ /**
34
+ * Visual variant.
35
+ *
36
+ * - `'aurora'` (default): soft layered ribbons of color drifting across a
37
+ * translucent disc. Northern-lights aesthetic. Calm, atmospheric, reads
38
+ * like motion even at idle. Default for Medal apps.
39
+ * - `'metal'`: smooth chrome-like sphere with strong specular + depth
40
+ * shading. Apple Siri / Vision Pro adjacent. Use for object-like,
41
+ * premium-feeling presence.
42
+ */
43
+ variant?: VoiceOrbVariant;
44
+ }
45
+ export declare const VoiceOrb: React.ForwardRefExoticComponent<VoiceOrbProps & React.RefAttributes<HTMLButtonElement>>;
@@ -0,0 +1,123 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { Canvas } from '@react-three/fiber';
4
+ import * as React from 'react';
5
+ import { Scene } from './voice-orb-scene.js';
6
+ // Note: CSS for `.meda-voice-orb` is shipped via @medalsocial/meda/styles.css
7
+ // (re-exported from globals.css). We avoid a JS-side CSS import here so
8
+ // Meda's tsc-only build pipeline doesn't need CSS bundling.
9
+ // ---------------------------------------------------------------------------
10
+ // Theming helpers
11
+ // ---------------------------------------------------------------------------
12
+ const FALLBACK_COLOR = '#9A6AC2'; // Pilot purple
13
+ function hslToHex(h, s, l) {
14
+ const sl = s / 100;
15
+ const ll = l / 100;
16
+ const a = sl * Math.min(ll, 1 - ll);
17
+ const f = (n) => {
18
+ const k = (n + h / 30) % 12;
19
+ const color = ll - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
20
+ return Math.round(255 * color)
21
+ .toString(16)
22
+ .padStart(2, '0');
23
+ };
24
+ return `#${f(0)}${f(8)}${f(4)}`;
25
+ }
26
+ /**
27
+ * Convert any CSS color token to a hex string the Three.js shader can consume.
28
+ *
29
+ * Handles:
30
+ * - hex: #RGB / #RRGGBB / #RRGGBBAA
31
+ * - Tailwind/shadcn space-separated HSL: "271 36% 60%"
32
+ * - hsl(), rgb(), oklch(), color(), var() — resolved via browser CSSOM
33
+ */
34
+ export function parseColor(value, fallback = FALLBACK_COLOR) {
35
+ const v = value.trim();
36
+ if (!v)
37
+ return fallback;
38
+ // Already a hex literal — pass through directly.
39
+ if (/^#[0-9a-fA-F]{3,8}$/.test(v))
40
+ return v;
41
+ // Tailwind/shadcn convention: "H S% L%" (no leading "hsl(")
42
+ const hslMatch = v.match(/^(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)%\s+(\d+(?:\.\d+)?)%$/);
43
+ if (hslMatch) {
44
+ return hslToHex(Number(hslMatch[1]), Number(hslMatch[2]), Number(hslMatch[3]));
45
+ }
46
+ // Functional notation — hsl(), rgb(), oklch(), color(), var(), etc.
47
+ // Let the browser resolve it via a throw-away element.
48
+ if (typeof document !== 'undefined' && (v.includes('(') || v.startsWith('var'))) {
49
+ const probe = document.createElement('div');
50
+ probe.style.display = 'none';
51
+ probe.style.color = v;
52
+ document.body.appendChild(probe);
53
+ const computed = getComputedStyle(probe).color;
54
+ document.body.removeChild(probe);
55
+ const m = computed.match(/rgb(?:a)?\((\d+)[,\s]+(\d+)[,\s]+(\d+)/);
56
+ if (m) {
57
+ const r = Number(m[1]);
58
+ const g = Number(m[2]);
59
+ const b = Number(m[3]);
60
+ return `#${[r, g, b].map((x) => x.toString(16).padStart(2, '0')).join('')}`;
61
+ }
62
+ }
63
+ return fallback;
64
+ }
65
+ function readMedaColors(el) {
66
+ const style = getComputedStyle(el);
67
+ const primary = style.getPropertyValue('--primary').trim();
68
+ const accent = style.getPropertyValue('--accent').trim();
69
+ const colorA = parseColor(primary);
70
+ const colorB = accent ? parseColor(accent) : colorA;
71
+ return [colorA, colorB];
72
+ }
73
+ const DEFAULT_COLORS = ['#9A6AC2', '#7B4FAB'];
74
+ // ---------------------------------------------------------------------------
75
+ // Component
76
+ // ---------------------------------------------------------------------------
77
+ export const VoiceOrb = React.forwardRef(function VoiceOrb({ pressed, level = 0, outputLevel = 0, phase = 'idle', size = 144, disabled, label = 'Hold to talk', colors: colorsProp, variant = 'aurora', className, style, ...rest }, ref) {
78
+ const buttonRef = React.useRef(null);
79
+ // Merge forwarded ref
80
+ React.useImperativeHandle(ref, () => buttonRef.current);
81
+ // Resolved theme colors (from CSSOM unless overridden by prop)
82
+ const [resolvedColors, setResolvedColors] = React.useState(colorsProp ?? DEFAULT_COLORS);
83
+ // prefers-reduced-motion
84
+ const [reducedMotion, setReducedMotion] = React.useState(false);
85
+ React.useEffect(() => {
86
+ const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
87
+ setReducedMotion(mq.matches);
88
+ const handler = (e) => setReducedMotion(e.matches);
89
+ mq.addEventListener('change', handler);
90
+ return () => mq.removeEventListener('change', handler);
91
+ }, []);
92
+ // Theme color resolution
93
+ React.useEffect(() => {
94
+ if (colorsProp) {
95
+ setResolvedColors(colorsProp);
96
+ return;
97
+ }
98
+ const update = () => {
99
+ setResolvedColors(readMedaColors(document.documentElement));
100
+ };
101
+ update();
102
+ const observer = new MutationObserver(update);
103
+ observer.observe(document.documentElement, {
104
+ attributes: true,
105
+ attributeFilter: ['class', 'data-theme'],
106
+ });
107
+ return () => observer.disconnect();
108
+ }, [colorsProp]);
109
+ const btnStyle = {
110
+ width: size,
111
+ height: size,
112
+ ...style,
113
+ };
114
+ return (_jsx("button", { ref: buttonRef, type: "button", "aria-pressed": pressed, "aria-label": label, disabled: disabled, "data-phase": phase, "data-pressed": pressed, className: [
115
+ 'meda-voice-orb',
116
+ 'relative inline-flex items-center justify-center rounded-full',
117
+ 'select-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
118
+ 'transition-transform duration-200 ease-out',
119
+ disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer',
120
+ pressed ? 'scale-[0.97]' : 'scale-100',
121
+ className ?? '',
122
+ ].join(' '), style: btnStyle, ...rest, children: _jsx(Canvas, { gl: { alpha: true, antialias: true, premultipliedAlpha: true }, style: { pointerEvents: 'none' }, children: _jsx(Scene, { level: level, outputLevel: outputLevel, phase: phase, colors: resolvedColors, reducedMotion: reducedMotion, pressed: pressed, variant: variant }) }) }));
123
+ });
@@ -0,0 +1,7 @@
1
+ import type { TurnPhase } from './types.js';
2
+ export interface VoiceStatusPillProps {
3
+ phase: TurnPhase;
4
+ thinkingForMs?: number;
5
+ className?: string;
6
+ }
7
+ export declare function VoiceStatusPill({ phase, thinkingForMs, className }: VoiceStatusPillProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ const PHASE_LABEL = {
3
+ idle: 'Idle',
4
+ listening: 'Listening',
5
+ thinking: 'Thinking',
6
+ speaking: 'Speaking',
7
+ error: 'Error',
8
+ };
9
+ export function VoiceStatusPill({ phase, thinkingForMs, className }) {
10
+ const tone = phase === 'listening'
11
+ ? 'bg-primary text-primary-foreground'
12
+ : phase === 'speaking'
13
+ ? 'bg-emerald-500 text-white'
14
+ : phase === 'error'
15
+ ? 'bg-destructive text-destructive-foreground'
16
+ : 'bg-muted text-muted-foreground';
17
+ const seconds = thinkingForMs ? (thinkingForMs / 1000).toFixed(1) : null;
18
+ return (_jsxs("span", { role: "status", "data-phase": phase, className: [
19
+ 'inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium',
20
+ tone,
21
+ className ?? '',
22
+ ].join(' '), children: [_jsx("span", { className: [
23
+ 'size-1.5 rounded-full bg-current',
24
+ phase === 'thinking' ? 'animate-pulse' : '',
25
+ ].join(' ') }), PHASE_LABEL[phase], phase === 'thinking' && seconds && _jsxs("span", { className: "opacity-70", children: ["\u00B7 ", seconds, "s"] })] }));
26
+ }