From 86003aede3c0b0ea172d85dad857c54af088e0d0 Mon Sep 17 00:00:00 2001 From: Damien Tanner Date: Thu, 18 Sep 2025 17:06:28 +0100 Subject: [PATCH 1/2] upgrade react-sdk --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index abdae9e..989f3b3 100644 --- a/package.json +++ b/package.json @@ -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", From 935f3cd11af96f89793894e9109333f118d02b69 Mon Sep 17 00:00:00 2001 From: Damien Tanner Date: Thu, 18 Sep 2025 17:26:43 +0100 Subject: [PATCH 2/2] new connect behaviuor --- app/agent/page.tsx | 16 ---- app/page.tsx | 6 +- app/ui/ConnectScreen.tsx | 51 ------------- app/ui/VoiceAgent.tsx | 154 +++++++++++++++++++++++++++------------ package-lock.json | 16 ++-- 5 files changed, 119 insertions(+), 124 deletions(-) delete mode 100644 app/agent/page.tsx delete mode 100644 app/ui/ConnectScreen.tsx diff --git a/app/agent/page.tsx b/app/agent/page.tsx deleted file mode 100644 index 7a2e326..0000000 --- a/app/agent/page.tsx +++ /dev/null @@ -1,16 +0,0 @@ -'use client'; - -import dynamic from 'next/dynamic'; -import { useCallback } from 'react'; -import { useRouter } from 'next/navigation'; - -const VoiceAgent = dynamic(() => import('../ui/VoiceAgent'), { ssr: false }); - -export default function AgentPage() { - const router = useRouter(); - const handleDisconnect = useCallback(() => { - router.push('/'); - }, [router]); - - return ; -} diff --git a/app/page.tsx b/app/page.tsx index c0b7507..53312e1 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,7 @@ -import { ConnectScreen } from './ui/ConnectScreen'; +'use client'; + +import VoiceAgent from './ui/VoiceAgent'; export default function Home() { - return ; + return ; } diff --git a/app/ui/ConnectScreen.tsx b/app/ui/ConnectScreen.tsx deleted file mode 100644 index ba34ded..0000000 --- a/app/ui/ConnectScreen.tsx +++ /dev/null @@ -1,51 +0,0 @@ -'use client'; - -import { useRouter } from 'next/navigation'; -import { useState } from 'react'; -import { HeaderBar } from './HeaderBar'; - -type ConnectScreenProps = { - agentId?: string; -}; - -export function ConnectScreen({ agentId }: ConnectScreenProps) { - const router = useRouter(); - const [isNavigating, setIsNavigating] = useState(false); - const hasAgentId = Boolean(agentId); - - function handleConnect() { - if (!hasAgentId) return; - setIsNavigating(true); - router.push('/agent'); - } - - return ( -
- - -
-
-

Connect to your Layercode Voice Agent

-

Press connect to begin a session with your Layercode voice agent.

-
- - {!hasAgentId ? ( -

- Set NEXT_PUBLIC_LAYERCODE_AGENT_ID in your environment to enable the connect flow. -

- ) : null} -
-
- ); -} diff --git a/app/ui/VoiceAgent.tsx b/app/ui/VoiceAgent.tsx index 770b212..e73bcb1 100644 --- a/app/ui/VoiceAgent.tsx +++ b/app/ui/VoiceAgent.tsx @@ -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'; @@ -46,6 +46,7 @@ function VoiceAgentInner({ agentId, onDisconnect }: { agentId: string; onDisconn const userTurnIndex = useRef>({}); const assistantTurnIndex = useRef>({}); const notifyRef = useRef(null); + const [pendingAction, setPendingAction] = useState<'connect' | 'disconnect' | null>(null); useEffect(() => { // Prepare notification audio element @@ -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() }]); }, @@ -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' ? ( + + ) : null; + return (
- - End Session - - ) : null - } - /> - -
-
- - {/*

16-bit PCM audio data • 8000 Hz • Mono

*/} -
-
-
- { - if (isMuted) unmute(); - else mute(); - }} - /> -
{isMuted ? 'Muted' : 'Live'}
-
-
-
- - {/*

16-bit PCM audio data • 16000 Hz • Mono

*/} + +
+
+

Connect to your Layercode Voice Agent

+

+ {isConnected ? 'You are connected to your agent. Disconnect to end the current session.' : 'Press connect to begin a session with your Layercode voice agent.'} +

+
-
- -
+ {isConnected ? ( + <> +
+
+ +
+
+
+ { + if (isMuted) unmute(); + else mute(); + }} + /> +
{isMuted ? 'Muted' : 'Live'}
+
+
+
+ +
+
-
- -
+
+ +
+ +
+ +
+ + ) : null}
); } diff --git a/package-lock.json b/package-lock.json index 1b54ba0..829e2dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,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", @@ -8940,9 +8940,9 @@ } }, "node_modules/@layercode/js-sdk": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@layercode/js-sdk/-/js-sdk-2.0.4.tgz", - "integrity": "sha512-o04IhAqaQj9EMlry+05DziwTg7ZjmTd2Xtf5KdJGkbOQbCjLHIxl5DRIt3cFExKLmAmRzeDqnhdnKUyCQBZ69g==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@layercode/js-sdk/-/js-sdk-2.1.0.tgz", + "integrity": "sha512-A49H8bJVSXg+edaXeOfVVfSvdf1X99db7tCz6pdTwQ+X4ttSaRN1c5YgzcKZBMO8fi7ZjC57o29FYvmWtAVHvw==", "license": "MIT", "dependencies": { "@ricky0123/vad-web": "^0.0.24", @@ -8956,12 +8956,12 @@ "license": "MIT" }, "node_modules/@layercode/react-sdk": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@layercode/react-sdk/-/react-sdk-2.0.5.tgz", - "integrity": "sha512-5F+xI99531DMfox52xUTrO+UqvcDNFJW+vjiZAxW4ZEHWRLDSXu0PujxOixyWilUWUe6PSye0gaePzffEmXzeg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@layercode/react-sdk/-/react-sdk-2.1.0.tgz", + "integrity": "sha512-hWgKlxD+O03NtQTutuq6+TIpPRdg2libOLbSucKyPOyPQnJu3g9/taz4AKSBT5TaqKArlD20SVOaXxoyVkZ/Jw==", "license": "MIT", "dependencies": { - "@layercode/js-sdk": "^2.0.4" + "@layercode/js-sdk": "^2.1.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"