Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions app/agent/page.tsx

This file was deleted.

6 changes: 4 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ConnectScreen } from './ui/ConnectScreen';
'use client';

import VoiceAgent from './ui/VoiceAgent';

export default function Home() {
return <ConnectScreen agentId={process.env.NEXT_PUBLIC_LAYERCODE_AGENT_ID} />;
return <VoiceAgent />;
}
51 changes: 0 additions & 51 deletions app/ui/ConnectScreen.tsx

This file was deleted.

154 changes: 107 additions & 47 deletions app/ui/VoiceAgent.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { useLayercodeAgent } from '@layercode/react-sdk';
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import TranscriptConsole from './TranscriptConsole';
import PromptPane from './PromptPane';
import { HeaderBar } from './HeaderBar';
Expand Down Expand Up @@ -46,6 +46,7 @@ function VoiceAgentInner({ agentId, onDisconnect }: { agentId: string; onDisconn
const userTurnIndex = useRef<Record<string, number>>({});
const assistantTurnIndex = useRef<Record<string, number>>({});
const notifyRef = useRef<HTMLAudioElement | null>(null);
const [pendingAction, setPendingAction] = useState<'connect' | 'disconnect' | null>(null);

useEffect(() => {
// Prepare notification audio element
Expand Down Expand Up @@ -102,10 +103,17 @@ function VoiceAgentInner({ agentId, onDisconnect }: { agentId: string; onDisconn
}
}

const { userAudioAmplitude, agentAudioAmplitude, status, mute, unmute, isMuted } = useLayercodeAgent({
const resetConversationState = useCallback(() => {
setEntries([]);
setTurn('idle');
setUserSpeaking(false);
userTurnIndex.current = {};
assistantTurnIndex.current = {};
}, []);

const { userAudioAmplitude, agentAudioAmplitude, status, mute, unmute, isMuted, connect, disconnect } = useLayercodeAgent({
agentId,
authorizeSessionEndpoint: '/api/authorize',
_websocketUrl: 'wss://api-staging.layercode.com/v1/agents/web/websocket',
onMuteStateChange(isMuted) {
setEntries((prev) => [...prev, { role: 'data', text: `MIC → ${isMuted ? 'muted' : 'unmuted'}`, ts: Date.now() }]);
},
Expand Down Expand Up @@ -150,56 +158,108 @@ function VoiceAgentInner({ agentId, onDisconnect }: { agentId: string; onDisconn
}
});

const isConnecting = status === 'connecting' || pendingAction === 'connect';
const isConnected = status === 'connected';
const buttonDisabled = isConnected ? pendingAction === 'disconnect' : isConnecting;

const handleConnect = useCallback(async () => {
if (isConnecting) return;
setPendingAction('connect');
resetConversationState();
try {
await connect({ newConversation: true });
} catch (error) {
console.error('Failed to connect to Layercode agent', error);
} finally {
setPendingAction(null);
}
}, [connect, isConnecting, resetConversationState]);

const handleDisconnect = useCallback(async () => {
if (pendingAction === 'disconnect') return;
setPendingAction('disconnect');
try {
await disconnect({ clearConversationId: true });
resetConversationState();
onDisconnect?.();
} catch (error) {
console.error('Failed to disconnect from Layercode agent', error);
} finally {
setPendingAction(null);
}
}, [disconnect, onDisconnect, pendingAction, resetConversationState]);

const actionSlot =
isConnected || pendingAction === 'disconnect' ? (
<button
type="button"
onClick={handleDisconnect}
disabled={pendingAction === 'disconnect'}
className="px-3 py-1.5 rounded border border-gray-700/70 bg-gray-900/20 text-[11px] uppercase tracking-wider text-gray-200 hover:text-white hover:border-gray-500 transition-colors disabled:opacity-60"
>
{pendingAction === 'disconnect' ? 'Disconnecting…' : 'Disconnect'}
</button>
) : null;

return (
<div className="max-w-6xl mx-auto px-4 py-6 space-y-6 overflow-x-hidden">
<HeaderBar
agentId={agentId}
status={status}
turn={turn}
actionSlot={
onDisconnect ? (
<button
type="button"
onClick={onDisconnect}
className="px-3 py-1.5 rounded border border-gray-700/70 bg-gray-900/20 text-[11px] uppercase tracking-wider text-gray-200 hover:text-white hover:border-gray-500 transition-colors"
>
End Session
</button>
) : null
}
/>

<div className="grid grid-cols-1 md:grid-cols-3 gap-4 min-w-0">
<div className="hidden md:block rounded-md border border-neutral-800 bg-neutral-950/60 p-4">
<SpectrumVisualizer label="User" amplitude={userAudioAmplitude * 2} accent="#C4B5FD" />
{/* <p className="mt-2 text-[11px] leading-5 text-neutral-400">16-bit PCM audio data • 8000 Hz • Mono</p> */}
</div>
<div className="flex items-center justify-center">
<div className="flex flex-col items-center gap-3">
<MicrophoneButton
isMuted={isMuted}
userSpeaking={userSpeaking}
onToggle={() => {
if (isMuted) unmute();
else mute();
}}
/>
<div className={`text-[11px] uppercase tracking-wider ${isMuted ? 'text-red-300' : 'text-neutral-400'}`}>{isMuted ? 'Muted' : 'Live'}</div>
</div>
</div>
<div className="hidden md:block rounded-md border border-neutral-800 bg-neutral-950/60 p-4">
<SpectrumVisualizer label="Agent" amplitude={agentAudioAmplitude} accent="#67E8F9" />
{/* <p className="mt-2 text-[11px] leading-5 text-neutral-400">16-bit PCM audio data • 16000 Hz • Mono</p> */}
<HeaderBar agentId={agentId} status={status} turn={turn} actionSlot={actionSlot} />
<div className="rounded-md border border-neutral-800 bg-neutral-950/60 h-[70vh] flex flex-col justify-center items-center gap-6 text-center">
<div className="space-y-2">
<h1 className="text-2xl font-semibold text-neutral-100">Connect to your Layercode Voice Agent</h1>
<p className="text-neutral-400 text-sm max-w-md">
{isConnected ? 'You are connected to your agent. Disconnect to end the current session.' : 'Press connect to begin a session with your Layercode voice agent.'}
</p>
</div>
<button
type="button"
onClick={isConnected ? handleDisconnect : handleConnect}
disabled={buttonDisabled}
className={`px-6 py-3 rounded-md text-sm font-medium uppercase tracking-wider transition-colors border ${
isConnected
? 'border-rose-600 bg-rose-600/60 text-white hover:bg-rose-500/70 hover:border-rose-500 disabled:opacity-60'
: buttonDisabled
? 'border-neutral-800 text-neutral-600 bg-neutral-900 cursor-not-allowed'
: 'border-violet-600 bg-violet-600/60 text-white hover:bg-violet-500/70 hover:border-violet-500'
}`}
>
{isConnected ? (pendingAction === 'disconnect' ? 'Disconnecting…' : 'Disconnect') : isConnecting ? 'Connecting…' : 'Connect'}
</button>
</div>

<div className="rounded-md border border-neutral-800 overflow-hidden w-full max-w-full min-w-0">
<TranscriptConsole entries={entries} />
</div>
{isConnected ? (
<>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 min-w-0">
<div className="hidden md:block rounded-md border border-neutral-800 bg-neutral-950/60 p-4">
<SpectrumVisualizer label="User" amplitude={userAudioAmplitude * 2} accent="#C4B5FD" />
</div>
<div className="flex items-center justify-center">
<div className="flex flex-col items-center gap-3">
<MicrophoneButton
isMuted={isMuted}
userSpeaking={userSpeaking}
onToggle={() => {
if (isMuted) unmute();
else mute();
}}
/>
<div className={`text-[11px] uppercase tracking-wider ${isMuted ? 'text-red-300' : 'text-neutral-400'}`}>{isMuted ? 'Muted' : 'Live'}</div>
</div>
</div>
<div className="hidden md:block rounded-md border border-neutral-800 bg-neutral-950/60 p-4">
<SpectrumVisualizer label="Agent" amplitude={agentAudioAmplitude} accent="#67E8F9" />
</div>
</div>

<div className="rounded-md border border-neutral-800 overflow-hidden w-full max-w-full min-w-0">
<PromptPane />
</div>
<div className="rounded-md border border-neutral-800 overflow-hidden w-full max-w-full min-w-0">
<TranscriptConsole entries={entries} />
</div>

<div className="rounded-md border border-neutral-800 overflow-hidden w-full max-w-full min-w-0">
<PromptPane />
</div>
</>
) : null}
</div>
);
}
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"dependencies": {
"@ai-sdk/openai": "^2.0.27",
"@layercode/node-server-sdk": "^1.2.1",
"@layercode/react-sdk": "^2.0.5",
"@layercode/react-sdk": "^2.1.0",
"@opennextjs/cloudflare": "^1.8.1",
"ai": "^5.0.39",
"next": "15.5.2",
Expand Down