diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index 7569ea5..b3c11dd 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -6045,10 +6045,28 @@ export default function ChatPage() { setIsDragOver(false); if (event.dataTransfer.files.length) addFiles(event.dataTransfer.files); }} - agentPanel={voiceMode === "agent" && !activeThread && agentOverlayMode !== "immersive" ? ( + agentPanel={voiceMode === "agent" && !activeThread ? (
+ {agentOverlayMode === "immersive" ? ( +
+ ) : null} sendMessage(text, { forceVoiceResponse: true })} onRealtimeTranscript={(event) => persistRealtimeTranscript(event, { @@ -6061,7 +6079,8 @@ export default function ChatPage() { isLoading={isLoading} accentColor={resolvedVisualAccentColor} autoActivate - compact + compact={agentOverlayMode !== "immersive"} + immersive={agentOverlayMode === "immersive"} isMicMuted={agentMicMuted} isAgentMuted={agentAudioMuted} onMicMutedChange={setAgentMicMuted} @@ -6076,25 +6095,33 @@ export default function ChatPage() { voiceSettings={resolvedVoiceSettings} visualSettings={resolvedVisualSettings} /> -
+
@@ -6103,75 +6130,6 @@ export default function ChatPage() { />
- {voiceMode === "agent" && !activeThread && agentOverlayMode === "immersive" ? ( -
-
- sendMessage(text, { forceVoiceResponse: true })} - onRealtimeTranscript={(event) => persistRealtimeTranscript(event, { - sessionKey: activeVoiceSessionKey, - storeKey: activeVoiceStoreKey, - setVisibleMessages: setMessages, - })} - isPlayingAudio={isPlayingAudio} - onInterrupt={interruptAudio} - isLoading={isLoading} - accentColor={resolvedVisualAccentColor} - autoActivate - immersive - isMicMuted={agentMicMuted} - isAgentMuted={agentAudioMuted} - onMicMutedChange={setAgentMicMuted} - onAgentMutedChange={handleAgentAudioMutedChange} - agent={selectedAgent?.callsign} - gatewayAgent={delegatedViaAgent?.callsign ?? selectedAgent?.callsign} - companyId={company?.id} - sessionKey={selectedSessionBelongsToAgent(selectedSessionKey, selectedAgent?.callsign) - ? selectedSessionKey ?? gatewaySessionKeyForAgent(selectedAgent) - : gatewaySessionKeyForAgent(selectedAgent)} - realtimeRuntimeId={selectedAgent?.runtimeId ?? undefined} - voiceSettings={resolvedVoiceSettings} - visualSettings={resolvedVisualSettings} - /> -
- - -
-
- ) : null}
diff --git a/src/components/chat/agent-visualizer.tsx b/src/components/chat/agent-visualizer.tsx index 0192130..5004ef7 100644 --- a/src/components/chat/agent-visualizer.tsx +++ b/src/components/chat/agent-visualizer.tsx @@ -310,6 +310,11 @@ function drawNeuralConstellation(ctx: CanvasRenderingContext2D, frame: DrawFrame const nodes = frame.compact ? 16 : frame.immersive ? 34 : 24; const spreadX = size * (frame.immersive ? 0.36 : 0.34); const spreadY = size * (frame.immersive ? 0.3 : 0.28); + const voiceEnergy = frame.state === "listening" || frame.state === "speaking" + ? clamp(frame.visualVolume * 1.6 + frame.motionLevel * 0.7) + : frame.state === "processing" + ? 0.58 + : 0.16; drawBackground(ctx, frame, ink, false); @@ -329,6 +334,7 @@ function drawNeuralConstellation(ctx: CanvasRenderingContext2D, frame: DrawFrame ctx.save(); ctx.globalCompositeOperation = "lighter"; + const links: Array<{ a: typeof points[number]; b: typeof points[number]; i: number; j: number; distance: number; threshold: number }> = []; for (let i = 0; i < points.length; i += 1) { for (let j = i + 1; j < points.length; j += 1) { const a = points[i]; @@ -338,9 +344,39 @@ function drawNeuralConstellation(ctx: CanvasRenderingContext2D, frame: DrawFrame const distance = Math.hypot(dx, dy); const threshold = size * (frame.compact ? 0.19 : 0.16); if (distance > threshold) continue; - const route = frame.state === "processing" ? Math.sin(frame.primaryPhase * 2.6 + i * 0.8 + j) * 0.5 + 0.5 : 0.25; - const alpha = (1 - distance / threshold) * (0.15 + energy * 0.24 + route * 0.18); - line(ctx, a.x, a.y, b.x, b.y, rgba(route > 0.7 ? accent : cool, alpha), 1 + route * 1.4); + links.push({ a, b, i, j, distance, threshold }); + const routeWave = Math.sin(frame.primaryPhase * 2.4 + i * 0.8 + j * 0.47) * 0.5 + 0.5; + const voiceWave = Math.sin(frame.tertiaryPhase * 3.6 - Math.min(a.rank, b.rank) * 0.7) * 0.5 + 0.5; + const route = frame.state === "processing" ? routeWave : frame.state === "speaking" ? voiceWave : frame.state === "listening" ? 1 - voiceWave : 0.22; + const alpha = (1 - distance / threshold) * (0.1 + energy * 0.16 + route * voiceEnergy * 0.44); + const width = 0.7 + route * voiceEnergy * 2.4; + line(ctx, a.x, a.y, b.x, b.y, rgba(route > 0.54 ? accent : cool, alpha), width); + } + } + + if (voiceEnergy > 0.18) { + const packetCount = frame.compact ? 10 : frame.immersive ? 26 : 18; + for (let n = 0; n < Math.min(packetCount, links.length); n += 1) { + const link = links[(n * 7 + Math.floor(frame.secondaryPhase * 2)) % links.length]; + if (!link) continue; + const progress = (frame.tertiaryPhase * (frame.state === "speaking" ? 0.46 : -0.38) + n * 0.131) % 1; + const t = progress < 0 ? progress + 1 : progress; + const x = link.a.x + (link.b.x - link.a.x) * t; + const y = link.a.y + (link.b.y - link.a.y) * t; + const packetAlpha = (1 - link.distance / link.threshold) * voiceEnergy * (frame.state === "idle" ? 0.18 : 0.58); + glow(ctx, x, y, size * 0.035, accent, packetAlpha * 0.12); + circle(ctx, x, y, 1.4 + voiceEnergy * 2.8, rgba(accent, packetAlpha), 1, rgba(mix(accent, [255, 255, 255], 0.28), packetAlpha * 0.45)); + } + } + + if (voiceEnergy > 0.22) { + const rings = frame.state === "speaking" ? 3 : 2; + for (let i = 0; i < rings; i += 1) { + const pulse = (frame.secondaryPhase * (frame.state === "speaking" ? 0.38 : -0.3) + i / rings) % 1; + const t = pulse < 0 ? pulse + 1 : pulse; + const radius = size * (0.08 + t * 0.34); + const alpha = (1 - t) * voiceEnergy * 0.24; + circle(ctx, cx, cy, radius, rgba(frame.state === "speaking" ? frame.speaking : frame.listening, alpha), 1.2 + voiceEnergy * 1.5); } } @@ -355,8 +391,8 @@ function drawNeuralConstellation(ctx: CanvasRenderingContext2D, frame: DrawFrame : frame.state === "processing" ? wave * 0.82 : 0.24; - const radius = (frame.compact ? 2.1 : 3.1) + active * energy * (frame.immersive ? 5.8 : 3.8); - glow(ctx, point.x, point.y, radius * 4.2, accent, active * 0.08); + const radius = (frame.compact ? 2.1 : 3.1) + active * energy * (frame.immersive ? 5.8 : 3.8) + voiceEnergy * active * 1.8; + glow(ctx, point.x, point.y, radius * (4.2 + voiceEnergy * 2.8), accent, active * (0.08 + voiceEnergy * 0.08)); circle(ctx, point.x, point.y, radius, rgba(mix(accent, [232, 249, 246], 0.35), 0.42 + active * 0.36), 1, rgba(accent, 0.12 + active * 0.18)); } ctx.restore();