@vox-ai/react 0.3.0 → 0.5.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.
@@ -1 +1 @@
1
- {"version":3,"file":"lib.modern.js","sources":["../src/hooks/useVoxAI.tsx","../src/utils/constants.ts"],"sourcesContent":["import {\n LiveKitRoom,\n RoomAudioRenderer,\n useAudioWaveform,\n useChat,\n useDataChannel,\n useLocalParticipant,\n useParticipantTracks,\n useTrackTranscription,\n useVoiceAssistant,\n} from \"@livekit/components-react\";\nimport { Track } from \"livekit-client\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { createRoot, Root } from \"react-dom/client\";\nimport { HTTPS_API_ORIGIN, SDK_VERSION } from \"../utils/constants\";\n\ntype VoxConnectionDetail = {\n serverUrl: string;\n roomName: string;\n participantName: string;\n participantToken: string;\n};\n\n/**\n * VoxAgentState\n * @description The state of the agent\n */\nexport type VoxAgentState =\n | \"disconnected\"\n | \"connecting\"\n | \"initializing\"\n | \"listening\"\n | \"thinking\"\n | \"speaking\";\n\n/**\n * Function call related types\n */\nexport interface FunctionToolsExecuted {\n type: \"function_tools_executed\";\n function_calls: FunctionCallInfo[];\n function_call_outputs: FunctionCallResult[];\n}\n\nexport interface FunctionCallInfo {\n id: string;\n type: string;\n call_id: string;\n arguments: string;\n name: string;\n}\n\nexport interface FunctionCallResult {\n id: string;\n name: string;\n type: string;\n call_id: string;\n output: string;\n is_error: boolean;\n}\n\n/**\n * VoxMessage\n * @description The message type between the agent and the user\n */\nexport type VoxMessage = {\n id?: string;\n name: \"agent\" | \"user\" | \"tool\";\n message?: string;\n timestamp: number;\n isFinal?: boolean;\n tool?: FunctionToolsExecuted;\n};\n\n/**\n * VoxAIOptions\n * @description The callback functions for the useVoxAI hook\n */\nexport interface VoxAIOptions {\n onConnect?: () => void;\n onDisconnect?: () => void;\n onError?: (error: Error) => void;\n onMessage?: (message: VoxMessage) => void;\n}\n\n// Message channel event types\ntype MessageChannelEvent =\n | { type: \"state_update\"; state: VoxAgentState }\n | { type: \"transcription_update\"; transcriptions: TranscriptionSegment[] }\n | {\n type: \"waveform_update\";\n waveformData: number[];\n speaker: \"agent\" | \"user\";\n }\n | { type: \"function_tools_executed\"; tool: FunctionToolsExecuted };\n\ntype TranscriptionSegment = {\n id: string;\n text: string;\n isFinal: boolean;\n timestamp: number;\n speaker: \"agent\" | \"user\";\n};\n\n/**\n * ConnectParams\n * @param agentId - The agent ID\n * @param apiKey - The API key\n * @param dynamicVariables - The dynamic variables\n * @param metadata - 이 메타데이터는 아웃바운드 웹훅, 통화 기록에 포함됩니다.\n */\nexport interface ConnectParams {\n agentId: string;\n apiKey: string;\n dynamicVariables?: Record<string, any>;\n metadata?: Record<string, any>;\n}\n\n/**\n * useVoxAI\n * @description The hook for integrating with vox.ai voice assistant\n * @param options - The options for the useVoxAI hook\n * @returns The useVoxAI hook\n * @example\n * const { connect, disconnect, state, messages, send } = useVoxAI({\n * onConnect: () => console.log(\"Connected\"),\n * onDisconnect: () => console.log(\"Disconnected\"),\n * onError: (error) => console.error(\"Error:\", error),\n * onMessage: (message) => console.log(\"Message:\", message),\n * });\n */\nexport function useVoxAI(options: VoxAIOptions = {}) {\n // Connection state\n const [connectionDetail, setConnectionDetail] =\n useState<VoxConnectionDetail | null>(null);\n const [state, setState] = useState<VoxAgentState>(\"disconnected\");\n\n // Session timestamp to filter out stale asynchronous events\n const sessionTimestampRef = useRef<number>(Date.now());\n\n // Message handling\n const [transcriptMap, setTranscriptMap] = useState<Map<string, VoxMessage>>(\n new Map()\n );\n const [messages, setMessages] = useState<VoxMessage[]>([]);\n const prevMessagesRef = useRef<string>(\"\");\n\n // Track which messages we've already sent to the onMessage callback\n const processedMessageIdsRef = useRef<Set<string>>(new Set());\n\n // DOM manipulation for LiveKit portal\n const portalRootRef = useRef<HTMLDivElement | null>(null);\n const rootRef = useRef<Root | null>(null);\n\n // Communication channel\n const channelRef = useRef<MessageChannel | null>(null);\n\n // Add this near the start of your useVoxAI hook\n const livekitComponentRef = useRef<React.ReactNode>(null);\n\n // Replace the single waveform state with a map for multiple speakers\n const [waveformDataMap, setWaveformDataMap] = useState<\n Record<string, number[]>\n >({\n agent: [],\n user: [],\n });\n\n // Add back the waveform config reference\n const waveformConfigRef = useRef<{\n speaker?: \"agent\" | \"user\";\n barCount: number;\n updateInterval: number;\n } | null>(null);\n\n // Add a new state to track microphone status\n const [isMicEnabled, setIsMicEnabled] = useState<boolean>(true);\n\n // Update messages whenever transcriptMap changes\n useEffect(() => {\n const allMessages = Array.from(transcriptMap.values()).sort(\n (a, b) => a.timestamp - b.timestamp\n );\n\n // Only update if the messages have actually changed\n const messagesString = JSON.stringify(allMessages);\n if (messagesString !== prevMessagesRef.current) {\n prevMessagesRef.current = messagesString;\n setMessages(allMessages);\n\n // Only trigger onMessage for new final messages that haven't been processed yet\n if (options.onMessage) {\n allMessages\n .filter(\n (msg) =>\n msg.isFinal &&\n msg.id &&\n !processedMessageIdsRef.current.has(msg.id)\n )\n .forEach((msg) => {\n if (msg.id) {\n // Mark this message as processed\n processedMessageIdsRef.current.add(msg.id);\n // Call the callback\n options.onMessage?.(msg);\n }\n });\n }\n }\n }, [transcriptMap, options.onMessage]);\n\n // Initialize message channel - ensure ports are properly connected\n useEffect(() => {\n const channel = new MessageChannel();\n\n channel.port1.onmessage = (e) => {\n const data = e.data as MessageChannelEvent;\n\n if (data.type === \"state_update\") {\n setState(data.state);\n } else if (data.type === \"transcription_update\") {\n handleTranscriptionUpdate(data.transcriptions);\n } else if (data.type === \"waveform_update\" && data.speaker) {\n // Store the waveform data for the specific speaker\n setWaveformDataMap((prevMap) => ({\n ...prevMap,\n [data.speaker]: data.waveformData,\n }));\n } else if (data.type === \"function_tools_executed\" && data.tool) {\n // Handle function calls\n const functionCallsId = `function-calls-${Date.now()}`;\n setTranscriptMap((prevMap) => {\n const newMap = new Map(prevMap);\n newMap.set(functionCallsId, {\n id: functionCallsId,\n name: \"tool\",\n tool: data.tool,\n timestamp: Date.now(),\n isFinal: true,\n });\n return newMap;\n });\n }\n };\n\n // Start the port\n channel.port1.start();\n\n // Store the channel reference\n channelRef.current = channel;\n\n return () => {\n channelRef.current?.port1.close();\n channelRef.current?.port2.close();\n channelRef.current = null;\n };\n }, []);\n\n // Process incoming transcriptions and filter out stale events\n const handleTranscriptionUpdate = useCallback(\n (transcriptions: TranscriptionSegment[]) => {\n setTranscriptMap((prevMap) => {\n const newMap = new Map(prevMap);\n\n transcriptions.forEach((t) => {\n // Only process transcriptions generated after the current session timestamp\n if (t.timestamp < sessionTimestampRef.current) {\n return;\n }\n const messageType = t.speaker === \"agent\" ? \"agent\" : \"user\";\n // Use existing timestamp if we already have this segment\n const existingTimestamp = prevMap.get(t.id)?.timestamp || t.timestamp;\n\n newMap.set(t.id, {\n id: t.id,\n name: messageType,\n message: t.text,\n timestamp: existingTimestamp,\n isFinal: t.isFinal,\n });\n });\n\n return newMap;\n });\n },\n []\n );\n\n // Set up DOM portal for LiveKit\n useEffect(() => {\n const div = document.createElement(\"div\");\n div.style.display = \"none\";\n document.body.appendChild(div);\n portalRootRef.current = div;\n rootRef.current = createRoot(div);\n\n return () => {\n if (rootRef.current) {\n rootRef.current.unmount();\n }\n if (portalRootRef.current) {\n document.body.removeChild(portalRootRef.current);\n }\n };\n }, []);\n\n // Connect to VoxAI service - updated to include dynamicVariables\n const connect = useCallback(\n async ({ agentId, apiKey, dynamicVariables, metadata }: ConnectParams) => {\n try {\n // Prevent connecting if already in a connection state\n if (state !== \"disconnected\") {\n const errorMessage = `Connection attempt rejected: Already in a connection state (${state})`;\n console.warn(errorMessage);\n\n if (options.onError) {\n options.onError(new Error(errorMessage));\n }\n return Promise.reject(new Error(errorMessage));\n }\n\n // Update session timestamp for new connection\n sessionTimestampRef.current = Date.now();\n setState(\"connecting\");\n\n const response = await fetch(HTTPS_API_ORIGIN, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n agent_id: agentId,\n metadata: {\n runtime_context: {\n source: {\n type: \"react-sdk\",\n version: SDK_VERSION,\n },\n },\n call_web: {\n dynamic_variables: dynamicVariables || {},\n metadata: metadata || {},\n },\n },\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Connection failed (${response.status}): ${errorText}`\n );\n }\n\n const data = await response.json();\n setConnectionDetail(data);\n\n if (options.onConnect) {\n options.onConnect();\n }\n } catch (err) {\n setConnectionDetail(null);\n setTranscriptMap(new Map());\n setMessages([]);\n setState(\"disconnected\");\n\n const error = err instanceof Error ? err : new Error(String(err));\n\n if (options.onError) {\n options.onError(error);\n }\n }\n },\n [options, state]\n );\n\n // Disconnect from VoxAI service, updating the session timestamp to ignore stale events\n const disconnect = useCallback(() => {\n // Update session timestamp on disconnect\n sessionTimestampRef.current = Date.now();\n setConnectionDetail(null);\n setTranscriptMap(new Map());\n setMessages([]);\n setState(\"disconnected\");\n\n if (options.onDisconnect) {\n options.onDisconnect();\n }\n }, [options]);\n\n // Update the send function with debugging and error checking\n const send = useCallback(\n ({ message, digit }: { message?: string; digit?: number }) => {\n if (state === \"disconnected\") {\n console.warn(\"Cannot send message: Not connected to a conversation\");\n return;\n }\n\n if (message) {\n const messageId = `user-text-${Date.now()}`;\n setTranscriptMap((prevMap) => {\n const newMap = new Map(prevMap);\n newMap.set(messageId, {\n id: messageId,\n name: \"user\",\n message: message,\n timestamp: Date.now(),\n isFinal: true,\n });\n return newMap;\n });\n\n if (channelRef.current) {\n channelRef.current.port1.postMessage({\n type: \"send_text\",\n text: message,\n });\n } else {\n console.error(\"No message channel available to send message\");\n }\n }\n\n if (digit !== undefined) {\n if (channelRef.current) {\n channelRef.current.port1.postMessage({\n type: \"send_dtmf\",\n digit: digit,\n });\n } else {\n console.error(\"No message channel available to send DTMF\");\n }\n }\n },\n [state]\n );\n\n // Update the audioWaveform function to return data for the requested speaker\n const audioWaveform = useCallback(\n ({\n speaker = \"agent\",\n barCount = 10,\n updateInterval = 20,\n }: {\n speaker?: \"agent\" | \"user\";\n barCount?: number;\n updateInterval?: number;\n }): number[] => {\n waveformConfigRef.current = { speaker, barCount, updateInterval };\n\n if (channelRef.current) {\n channelRef.current.port1.postMessage({\n type: \"waveform_config\",\n config: { speaker, barCount, updateInterval },\n });\n }\n\n const speakerData = waveformDataMap[speaker] || [];\n return speakerData.length > 0\n ? speakerData.slice(0, barCount)\n : Array(barCount).fill(0);\n },\n [waveformDataMap]\n );\n\n // Add toggleMic function that will be exposed in the hook's return value\n const toggleMic = useCallback((value: boolean) => {\n setIsMicEnabled(value);\n if (channelRef.current) {\n channelRef.current.port1.postMessage({\n type: \"toggle_mic\",\n enabled: value,\n });\n } else {\n console.error(\"No message channel available to toggle microphone\");\n }\n }, []);\n\n // Add setVolume function that will be exposed in the hook's return value\n const setVolume = useCallback((volume: number) => {\n const validVolume = Math.min(Math.max(volume, 0), 1);\n if (channelRef.current) {\n channelRef.current.port1.postMessage({\n type: \"set_volume\",\n volume: validVolume,\n });\n } else {\n console.error(\"No message channel available to set volume\");\n }\n }, []);\n\n // Modify the useEffect hook that renders the LiveKit component\n useEffect(() => {\n if (!rootRef.current) return;\n\n if (connectionDetail) {\n if (!livekitComponentRef.current) {\n if (channelRef.current) {\n channelRef.current.port2.start();\n }\n\n livekitComponentRef.current = (\n <LiveKitRoom\n serverUrl={connectionDetail.serverUrl}\n token={connectionDetail.participantToken}\n audio={true}\n video={false}\n connect={true}\n onDisconnected={disconnect}\n onError={(error) => {\n console.error(\"LiveKit connection error:\", error);\n disconnect();\n if (options.onError) {\n options.onError(\n new Error(`LiveKit connection error: ${error.message}`)\n );\n }\n }}\n >\n <RoomAudioRenderer />\n {channelRef.current && (\n <StateMonitor\n port={channelRef.current.port2}\n initialConfig={\n waveformConfigRef.current || {\n barCount: 10,\n updateInterval: 20,\n }\n }\n />\n )}\n </LiveKitRoom>\n );\n }\n rootRef.current.render(livekitComponentRef.current);\n } else {\n livekitComponentRef.current = null;\n rootRef.current.render(<></>);\n }\n }, [connectionDetail, disconnect, options.onError]);\n\n return {\n connect,\n disconnect,\n state,\n messages,\n send,\n audioWaveform,\n toggleMic,\n setVolume,\n };\n}\n\n/**\n * Component that monitors LiveKit state and communicates back to the main hook\n */\nfunction StateMonitor({\n port,\n initialConfig,\n}: {\n port: MessagePort | undefined;\n initialConfig: {\n speaker?: \"agent\" | \"user\";\n barCount: number;\n updateInterval: number;\n };\n}) {\n const { agent, state } = useVoiceAssistant();\n const { send: sendChat } = useChat();\n\n // Initialize waveform config with the passed initial values, defaulting to \"agent\" if not specified\n const [waveformConfig, setWaveformConfig] = useState({\n speaker: initialConfig.speaker || \"agent\",\n barCount: initialConfig.barCount,\n updateInterval: initialConfig.updateInterval,\n });\n\n // Agent transcriptions\n const agentAudioTrack = useParticipantTracks(\n [Track.Source.Microphone],\n agent?.identity\n )[0];\n const agentTranscription = useTrackTranscription(agentAudioTrack);\n\n // Use the current config for the waveform, applying different settings based on speaker\n const agentAudioWaveform = useAudioWaveform(agentAudioTrack, {\n barCount:\n waveformConfig.speaker === \"agent\" ? waveformConfig.barCount : 120, // default if not the selected speaker\n updateInterval:\n waveformConfig.speaker === \"agent\" ? waveformConfig.updateInterval : 20,\n });\n\n // User transcriptions\n const localParticipant = useLocalParticipant();\n const localMessages = useTrackTranscription({\n publication: localParticipant.microphoneTrack,\n source: Track.Source.Microphone,\n participant: localParticipant.localParticipant,\n });\n const localAudioTrack = useParticipantTracks(\n [Track.Source.Microphone],\n localParticipant.localParticipant.identity\n )[0];\n const userAudioWaveform = useAudioWaveform(localAudioTrack, {\n barCount: waveformConfig.speaker === \"user\" ? waveformConfig.barCount : 120, // default if not the selected speaker\n updateInterval:\n waveformConfig.speaker === \"user\" ? waveformConfig.updateInterval : 20,\n });\n\n // Add separate effects to send agent and user waveform data\n useEffect(() => {\n if (!port || !agentAudioWaveform || !agentAudioWaveform.bars) return;\n\n // Send the agent waveform data\n port.postMessage({\n type: \"waveform_update\",\n waveformData: agentAudioWaveform.bars,\n speaker: \"agent\",\n });\n }, [port, agentAudioWaveform]);\n\n useEffect(() => {\n if (!port || !userAudioWaveform || !userAudioWaveform.bars) return;\n\n // Send the user waveform data\n port.postMessage({\n type: \"waveform_update\",\n waveformData: userAudioWaveform.bars,\n speaker: \"user\",\n });\n }, [port, userAudioWaveform]);\n\n // Listen for messages including config updates and mic toggle commands\n useEffect(() => {\n if (!port) return;\n\n const handleMessage = (event: MessageEvent) => {\n const data = event.data;\n\n if (data.type === \"waveform_config\" && data.config) {\n // Verify we have both required properties before updating\n if (\n typeof data.config.barCount === \"number\" &&\n typeof data.config.updateInterval === \"number\"\n ) {\n setWaveformConfig(data.config);\n }\n } else if (data.type === \"send_text\") {\n if (sendChat) {\n sendChat(data.text);\n } else {\n console.error(\"sendChat function is not available\");\n }\n } else if (data.type === \"send_dtmf\") {\n if (localParticipant.localParticipant) {\n // Use standard DTMF code (RFC 4733)\n const standardDtmfCode = 101; // Standard DTMF payload type\n localParticipant.localParticipant.publishDtmf(\n standardDtmfCode,\n data.digit\n );\n } else {\n console.error(\"Local participant is not available for DTMF\");\n }\n } else if (\n data.type === \"toggle_mic\" &&\n typeof data.enabled === \"boolean\"\n ) {\n // Handle microphone toggle\n if (localParticipant.localParticipant) {\n localParticipant.localParticipant\n .setMicrophoneEnabled(data.enabled)\n .catch((error) => {\n console.error(\"Failed to toggle microphone:\", error);\n });\n } else {\n console.error(\"Local participant is not available for mic toggle\");\n }\n } else if (\n data.type === \"set_volume\" &&\n typeof data.volume === \"number\"\n ) {\n // Handle volume control\n if (agent) {\n // The agent is a RemoteParticipant, so we can call setVolume directly\n try {\n agent.setVolume(data.volume);\n console.log(`Set agent volume to ${data.volume}`);\n } catch (error) {\n console.error(\"Failed to set agent volume:\", error);\n }\n } else {\n console.error(\"Agent is not available for volume control\");\n }\n }\n };\n\n // Make sure we start the port\n port.start();\n\n port.addEventListener(\"message\", handleMessage);\n\n return () => {\n port.removeEventListener(\"message\", handleMessage);\n };\n }, [port, sendChat, localParticipant, agent]);\n\n // Send agent state updates\n useEffect(() => {\n if (port) {\n port.postMessage({ type: \"state_update\", state });\n }\n }, [state, port]);\n\n // Send agent transcriptions\n useEffect(() => {\n if (port && agentTranscription.segments.length > 0) {\n const transcriptions = agentTranscription.segments.map((segment) => ({\n id: segment.id,\n text: segment.text,\n isFinal: segment.final,\n timestamp: Date.now(),\n speaker: \"agent\" as const,\n }));\n\n port.postMessage({\n type: \"transcription_update\",\n transcriptions,\n });\n }\n }, [agentTranscription.segments, port]);\n\n // Send user transcriptions\n useEffect(() => {\n if (port && localMessages.segments.length > 0) {\n const transcriptions = localMessages.segments.map((segment) => ({\n id: segment.id,\n text: segment.text,\n isFinal: segment.final,\n timestamp: Date.now(),\n speaker: \"user\" as const,\n }));\n\n port.postMessage({\n type: \"transcription_update\",\n transcriptions,\n });\n }\n }, [localMessages.segments, port]);\n\n // Add data channel hook for function calls\n const { message: functionToolsExecuted } = useDataChannel(\n \"function_tools_executed\",\n (msg) => {\n if (!port) return;\n\n const textDecoder = new TextDecoder();\n const messageString =\n msg.payload instanceof Uint8Array\n ? textDecoder.decode(msg.payload)\n : String(msg.payload);\n\n let tool: FunctionToolsExecuted;\n try {\n tool = JSON.parse(messageString);\n\n // Send function calls to main hook via the port\n port.postMessage({\n type: \"function_tools_executed\",\n tool: tool,\n });\n } catch (e) {\n console.error(\"Failed to parse function call log:\", e);\n }\n }\n );\n\n return null;\n}\n","// export const HTTPS_API_ORIGIN = \"https://frontend-dev.tryvox.co/api/agent/sdk\";\nexport const HTTPS_API_ORIGIN = \"https://www.tryvox.co/api/agent/sdk\";\n// export const HTTPS_API_ORIGIN = \"http://localhost:3000/api/agent/sdk\";\nexport const SDK_VERSION = \"0.3.0\";\n"],"names":["useVoxAI","options","connectionDetail","setConnectionDetail","useState","state","setState","sessionTimestampRef","useRef","Date","now","transcriptMap","setTranscriptMap","Map","messages","setMessages","prevMessagesRef","processedMessageIdsRef","Set","portalRootRef","rootRef","channelRef","livekitComponentRef","waveformDataMap","setWaveformDataMap","agent","user","waveformConfigRef","isMicEnabled","setIsMicEnabled","useEffect","allMessages","Array","from","values","sort","a","b","timestamp","messagesString","JSON","stringify","current","onMessage","filter","msg","isFinal","id","has","forEach","add","channel","MessageChannel","port1","onmessage","e","data","type","handleTranscriptionUpdate","transcriptions","speaker","prevMap","_extends","waveformData","tool","functionCallsId","newMap","set","name","start","_channelRef$current","_channelRef$current2","close","port2","useCallback","t","_prevMap$get","messageType","existingTimestamp","get","message","text","div","document","createElement","style","display","body","appendChild","createRoot","unmount","removeChild","connect","async","agentId","apiKey","dynamicVariables","metadata","errorMessage","console","warn","onError","Error","Promise","reject","response","fetch","method","headers","Authorization","agent_id","runtime_context","source","version","call_web","dynamic_variables","ok","errorText","status","json","onConnect","err","error","String","disconnect","onDisconnect","send","digit","messageId","postMessage","undefined","audioWaveform","barCount","updateInterval","config","speakerData","length","slice","fill","toggleMic","value","enabled","setVolume","volume","validVolume","Math","min","max","_jsxs","LiveKitRoom","serverUrl","token","participantToken","audio","video","onDisconnected","children","_jsx","RoomAudioRenderer","StateMonitor","port","initialConfig","render","_Fragment","useVoiceAssistant","sendChat","useChat","waveformConfig","setWaveformConfig","agentAudioTrack","useParticipantTracks","Track","Source","Microphone","identity","agentTranscription","useTrackTranscription","agentAudioWaveform","useAudioWaveform","localParticipant","useLocalParticipant","localMessages","publication","microphoneTrack","participant","localAudioTrack","userAudioWaveform","bars","handleMessage","event","publishDtmf","setMicrophoneEnabled","catch","log","addEventListener","removeEventListener","segments","map","segment","final","useDataChannel","textDecoder","TextDecoder","messageString","payload","Uint8Array","decode","parse"],"mappings":"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"}
1
+ {"version":3,"file":"lib.modern.js","sources":["../src/hooks/useVoxAI.tsx","../src/utils/constants.ts"],"sourcesContent":["import {\n LiveKitRoom,\n RoomAudioRenderer,\n useAudioWaveform,\n useChat,\n useDataChannel,\n useLocalParticipant,\n useParticipantTracks,\n useTrackTranscription,\n useVoiceAssistant,\n} from \"@livekit/components-react\";\nimport { Track } from \"livekit-client\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { createRoot, Root } from \"react-dom/client\";\nimport { HTTPS_API_ORIGIN, SDK_VERSION } from \"../utils/constants\";\n\ntype VoxConnectionDetail = {\n serverUrl: string;\n roomName: string;\n participantName: string;\n participantToken: string;\n};\n\n/**\n * 음성 AI 에이전트의 현재 상태를 나타냅니다.\n *\n * @remarks\n * 에이전트의 상태는 다음과 같이 변화합니다:\n * `disconnected` → `connecting` → `initializing` → `listening` ⇄ `thinking` ⇄ `speaking`\n *\n * @example\n * ```tsx\n * const { state } = useVoxAI();\n *\n * if (state === 'listening') {\n * console.log('에이전트가 사용자의 말을 듣고 있습니다');\n * }\n * ```\n */\nexport type VoxAgentState =\n /** 연결되지 않은 상태 */\n | \"disconnected\"\n /** Vox.ai 서버에 연결 중 */\n | \"connecting\"\n /** LiveKit 세션을 초기화하는 중 */\n | \"initializing\"\n /** 에이전트가 사용자의 음성을 듣고 있는 상태 */\n | \"listening\"\n /** 에이전트가 응답을 생각하고 있는 상태 */\n | \"thinking\"\n /** 에이전트가 응답을 말하고 있는 상태 */\n | \"speaking\";\n\n/**\n * 에이전트가 실행한 함수 도구들의 정보를 담고 있는 타입입니다.\n *\n * @remarks\n * 에이전트가 외부 API를 호출하거나 특정 작업을 수행할 때 이 타입의 데이터가 생성됩니다.\n */\nexport interface FunctionToolsExecuted {\n /** 이벤트 타입 */\n type: \"function_tools_executed\";\n /** 실행된 함수 호출 정보 배열 */\n function_calls: FunctionCallInfo[];\n /** 함수 호출 결과 배열 */\n function_call_outputs: FunctionCallResult[];\n}\n\n/**\n * 에이전트가 호출한 함수의 정보를 담고 있는 타입입니다.\n */\nexport interface FunctionCallInfo {\n /** 함수 호출 고유 ID */\n id: string;\n /** 함수 타입 */\n type: string;\n /** 함수 호출 ID */\n call_id: string;\n /** 함수에 전달된 인자들 */\n arguments: Record<string, any>;\n /** 호출된 함수의 이름 */\n name: string;\n}\n\n/**\n * 함수 호출의 결과를 담고 있는 타입입니다.\n */\nexport interface FunctionCallResult {\n /** 결과 고유 ID */\n id: string;\n /** 호출된 함수의 이름 */\n name: string;\n /** 결과 타입 */\n type: string;\n /** 함수 호출 ID */\n call_id: string;\n /** 함수 실행 결과 (문자열 형태) */\n output: string;\n /** 에러 발생 여부 */\n is_error: boolean;\n}\n\n/**\n * 에이전트와 사용자 간의 대화 메시지를 나타내는 타입입니다.\n *\n * @remarks\n * - `name`이 \"agent\"인 경우: AI 에이전트가 말한 내용\n * - `name`이 \"user\"인 경우: 사용자가 말한 내용 (음성 또는 텍스트)\n * - `name`이 \"tool\"인 경우: 에이전트가 실행한 함수 도구 정보\n *\n * @example\n * ```tsx\n * const { messages } = useVoxAI();\n *\n * messages.forEach(msg => {\n * if (msg.name === 'user' && msg.isFinal) {\n * console.log('사용자:', msg.message);\n * }\n * });\n * ```\n */\nexport type VoxMessage = {\n /** 메시지 고유 ID */\n id?: string;\n /** 메시지 발신자 타입 */\n name: \"agent\" | \"user\" | \"tool\";\n /** 메시지 내용 (음성 전사 텍스트 또는 사용자가 보낸 텍스트) */\n message?: string;\n /** 메시지 생성 시각 (Unix timestamp) */\n timestamp: number;\n /** 최종 확정된 메시지인지 여부 (false인 경우 음성 인식 중간 결과) */\n isFinal?: boolean;\n /** 함수 도구 실행 정보 (name이 \"tool\"인 경우에만 존재) */\n tool?: FunctionToolsExecuted;\n};\n\n/**\n * useVoxAI 훅의 콜백 함수들을 설정하는 옵션입니다.\n *\n * @example\n * ```tsx\n * const vox = useVoxAI({\n * onConnect: () => {\n * console.log('음성 AI에 연결되었습니다');\n * },\n * onDisconnect: () => {\n * console.log('연결이 종료되었습니다');\n * },\n * onError: (error) => {\n * console.error('오류 발생:', error.message);\n * },\n * onMessage: (message) => {\n * if (message.isFinal) {\n * console.log(`${message.name}: ${message.message}`);\n * }\n * }\n * });\n * ```\n */\nexport interface VoxAIOptions {\n /** 음성 AI 연결이 성공했을 때 호출되는 콜백 */\n onConnect?: () => void;\n /** 음성 AI 연결이 종료되었을 때 호출되는 콜백 */\n onDisconnect?: () => void;\n /** 오류가 발생했을 때 호출되는 콜백 */\n onError?: (error: Error) => void;\n /** 새로운 최종 메시지가 수신되었을 때 호출되는 콜백 (isFinal이 true인 메시지만 전달됨) */\n onMessage?: (message: VoxMessage) => void;\n}\n\n// Message channel event types\ntype MessageChannelEvent =\n | { type: \"state_update\"; state: VoxAgentState }\n | { type: \"transcription_update\"; transcriptions: TranscriptionSegment[] }\n | {\n type: \"waveform_update\";\n waveformData: number[];\n speaker: \"agent\" | \"user\";\n }\n | { type: \"function_tools_executed\"; tool: FunctionToolsExecuted };\n\ntype TranscriptionSegment = {\n id: string;\n text: string;\n isFinal: boolean;\n timestamp: number;\n speaker: \"agent\" | \"user\";\n};\n\n/**\n * Vox.ai 음성 AI에 연결하기 위한 매개변수입니다.\n *\n * @example\n * ```tsx\n * // 기본 연결 (current 버전)\n * connect({\n * agentId: 'my-agent-id',\n * apiKey: 'my-api-key'\n * });\n *\n * // 특정 버전으로 연결\n * connect({\n * agentId: 'my-agent-id',\n * agentVersion: 'v5',\n * apiKey: 'my-api-key'\n * });\n *\n * // 동적 변수와 함께 연결\n * connect({\n * agentId: 'my-agent-id',\n * apiKey: 'my-api-key',\n * dynamicVariables: {\n * userName: '홍길동',\n * userId: 'user123'\n * },\n * metadata: {\n * sessionId: 'sess_abc123'\n * }\n * });\n * ```\n */\nexport interface ConnectParams {\n /**\n * 연결할 에이전트의 ID\n * @remarks Vox.ai 대시보드에서 확인할 수 있습니다.\n */\n agentId: string;\n\n /**\n * 사용할 에이전트 버전\n * @remarks\n * - `'v1'`, `'v2'`, `'v12'` 등: 특정 버전 번호 (v + 숫자 형식)\n * - `'current'`: 현재 편집중인 버전 (기본값)\n * - `'production'`: 프로덕션으로 지정된 버전\n * - `undefined` 또는 미지정: 'current' 버전 사용\n */\n agentVersion?: string;\n\n /**\n * Vox.ai API 키\n * @remarks Vox.ai 대시보드에서 발급받을 수 있습니다.\n */\n apiKey: string;\n\n /**\n * 에이전트 대화에 전달할 동적 변수\n * @remarks\n * 에이전트 프롬프트에서 이 변수들을 참조하여 개인화된 대화를 만들 수 있습니다.\n * @example\n * ```tsx\n * dynamicVariables: {\n * userName: '홍길동',\n * userType: 'premium',\n * accountBalance: 50000\n * }\n * ```\n */\n dynamicVariables?: Record<string, any>;\n\n /**\n * 통화 메타데이터\n * @remarks\n * 이 메타데이터는 아웃바운드 웹훅과 통화 기록에 포함되어,\n * 외부 시스템과의 연동이나 분석에 활용할 수 있습니다.\n * @example\n * ```tsx\n * metadata: {\n * source: 'mobile-app',\n * campaignId: 'spring-2024',\n * customerId: 'cust_123'\n * }\n * ```\n */\n metadata?: Record<string, any>;\n}\n\n/**\n * Vox.ai 음성 AI를 React 애플리케이션에 통합하기 위한 훅입니다.\n *\n * @param options - 연결 이벤트에 대한 콜백 함수들을 설정하는 옵션 객체\n *\n * @returns 음성 AI를 제어하기 위한 메서드와 상태를 포함한 객체\n * - `connect`: Vox.ai 서버에 연결하는 함수\n * - `disconnect`: 연결을 종료하는 함수\n * - `state`: 에이전트의 현재 상태\n * - `messages`: 대화 메시지 배열\n * - `send`: 텍스트 메시지 또는 DTMF 숫자를 전송하는 함수\n * - `audioWaveform`: 실시간 오디오 파형 데이터를 가져오는 함수\n * - `toggleMic`: 마이크를 켜거나 끄는 함수\n * - `setVolume`: 에이전트 음성의 볼륨을 조절하는 함수\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const {\n * connect,\n * disconnect,\n * state,\n * messages,\n * send,\n * audioWaveform,\n * toggleMic,\n * setVolume\n * } = useVoxAI({\n * onConnect: () => console.log(\"연결됨\"),\n * onDisconnect: () => console.log(\"연결 종료\"),\n * onError: (error) => console.error(\"오류:\", error),\n * onMessage: (message) => console.log(\"새 메시지:\", message)\n * });\n *\n * const handleConnect = () => {\n * connect({\n * agentId: 'my-agent-id',\n * apiKey: 'my-api-key'\n * });\n * };\n *\n * return (\n * <div>\n * <button onClick={handleConnect}>연결</button>\n * <button onClick={disconnect}>연결 해제</button>\n * <p>상태: {state}</p>\n * </div>\n * );\n * }\n * ```\n */\nexport function useVoxAI(options: VoxAIOptions = {}) {\n // Connection state\n const [connectionDetail, setConnectionDetail] =\n useState<VoxConnectionDetail | null>(null);\n const [state, setState] = useState<VoxAgentState>(\"disconnected\");\n\n // Session timestamp to filter out stale asynchronous events\n const sessionTimestampRef = useRef<number>(Date.now());\n\n // Message handling\n const [transcriptMap, setTranscriptMap] = useState<Map<string, VoxMessage>>(\n new Map()\n );\n const [messages, setMessages] = useState<VoxMessage[]>([]);\n const prevMessagesRef = useRef<string>(\"\");\n\n // Track which messages we've already sent to the onMessage callback\n const processedMessageIdsRef = useRef<Set<string>>(new Set());\n\n // DOM manipulation for LiveKit portal\n const portalRootRef = useRef<HTMLDivElement | null>(null);\n const rootRef = useRef<Root | null>(null);\n\n // Communication channel\n const channelRef = useRef<MessageChannel | null>(null);\n\n // Add this near the start of your useVoxAI hook\n const livekitComponentRef = useRef<React.ReactNode>(null);\n\n // Replace the single waveform state with a map for multiple speakers\n const [waveformDataMap, setWaveformDataMap] = useState<\n Record<string, number[]>\n >({\n agent: [],\n user: [],\n });\n\n // Add back the waveform config reference\n const waveformConfigRef = useRef<{\n speaker?: \"agent\" | \"user\";\n barCount: number;\n updateInterval: number;\n } | null>(null);\n\n // Add a new state to track microphone status\n const [isMicEnabled, setIsMicEnabled] = useState<boolean>(true);\n\n // Update messages whenever transcriptMap changes\n useEffect(() => {\n const allMessages = Array.from(transcriptMap.values()).sort(\n (a, b) => a.timestamp - b.timestamp\n );\n\n // Only update if the messages have actually changed\n const messagesString = JSON.stringify(allMessages);\n if (messagesString !== prevMessagesRef.current) {\n prevMessagesRef.current = messagesString;\n setMessages(allMessages);\n\n // Only trigger onMessage for new final messages that haven't been processed yet\n if (options.onMessage) {\n allMessages\n .filter(\n (msg) =>\n msg.isFinal &&\n msg.id &&\n !processedMessageIdsRef.current.has(msg.id)\n )\n .forEach((msg) => {\n if (msg.id) {\n // Mark this message as processed\n processedMessageIdsRef.current.add(msg.id);\n // Call the callback\n options.onMessage?.(msg);\n }\n });\n }\n }\n }, [transcriptMap, options.onMessage]);\n\n // Initialize message channel - ensure ports are properly connected\n useEffect(() => {\n const channel = new MessageChannel();\n\n channel.port1.onmessage = (e) => {\n const data = e.data as MessageChannelEvent;\n\n if (data.type === \"state_update\") {\n setState(data.state);\n } else if (data.type === \"transcription_update\") {\n handleTranscriptionUpdate(data.transcriptions);\n } else if (data.type === \"waveform_update\" && data.speaker) {\n // Store the waveform data for the specific speaker\n setWaveformDataMap((prevMap) => ({\n ...prevMap,\n [data.speaker]: data.waveformData,\n }));\n } else if (data.type === \"function_tools_executed\" && data.tool) {\n // Handle function calls\n const functionCallsId = `function-calls-${Date.now()}`;\n setTranscriptMap((prevMap) => {\n const newMap = new Map(prevMap);\n newMap.set(functionCallsId, {\n id: functionCallsId,\n name: \"tool\",\n tool: data.tool,\n timestamp: Date.now(),\n isFinal: true,\n });\n return newMap;\n });\n }\n };\n\n // Start the port\n channel.port1.start();\n\n // Store the channel reference\n channelRef.current = channel;\n\n return () => {\n channelRef.current?.port1.close();\n channelRef.current?.port2.close();\n channelRef.current = null;\n };\n }, []);\n\n // Process incoming transcriptions and filter out stale events\n const handleTranscriptionUpdate = useCallback(\n (transcriptions: TranscriptionSegment[]) => {\n setTranscriptMap((prevMap) => {\n const newMap = new Map(prevMap);\n\n transcriptions.forEach((t) => {\n // Only process transcriptions generated after the current session timestamp\n if (t.timestamp < sessionTimestampRef.current) {\n return;\n }\n const messageType = t.speaker === \"agent\" ? \"agent\" : \"user\";\n // Use existing timestamp if we already have this segment\n const existingTimestamp = prevMap.get(t.id)?.timestamp || t.timestamp;\n\n newMap.set(t.id, {\n id: t.id,\n name: messageType,\n message: t.text,\n timestamp: existingTimestamp,\n isFinal: t.isFinal,\n });\n });\n\n return newMap;\n });\n },\n []\n );\n\n // Set up DOM portal for LiveKit\n useEffect(() => {\n const div = document.createElement(\"div\");\n div.style.display = \"none\";\n document.body.appendChild(div);\n portalRootRef.current = div;\n rootRef.current = createRoot(div);\n\n return () => {\n if (rootRef.current) {\n rootRef.current.unmount();\n }\n if (portalRootRef.current) {\n document.body.removeChild(portalRootRef.current);\n }\n };\n }, []);\n\n /**\n * Vox.ai 음성 AI 서버에 연결합니다.\n *\n * @param params - 연결에 필요한 매개변수 ({@link ConnectParams} 참조)\n *\n * @remarks\n * - 이미 연결된 상태에서 호출하면 오류가 발생합니다.\n * - 연결에 성공하면 `onConnect` 콜백이 호출됩니다.\n * - 연결에 실패하면 `onError` 콜백이 호출됩니다.\n * - 연결 성공 후 상태가 `connecting` → `initializing` → `listening`으로 변화합니다.\n *\n * @throws {Error} 이미 연결된 상태이거나 인증에 실패한 경우\n *\n * @example\n * ```tsx\n * const { connect } = useVoxAI();\n *\n * // 기본 연결\n * await connect({\n * agentId: 'agent_abc123',\n * apiKey: 'key_xyz789'\n * });\n *\n * // 특정 버전과 동적 변수로 연결\n * await connect({\n * agentId: 'agent_abc123',\n * agentVersion: 'v5',\n * apiKey: 'key_xyz789',\n * dynamicVariables: {\n * userName: '홍길동',\n * userId: 'user_123'\n * }\n * });\n * ```\n */\n const connect = useCallback(\n async ({\n agentId,\n agentVersion,\n apiKey,\n dynamicVariables,\n metadata,\n }: ConnectParams) => {\n try {\n // Prevent connecting if already in a connection state\n if (state !== \"disconnected\") {\n const errorMessage = `Connection attempt rejected: Already in a connection state (${state})`;\n console.warn(errorMessage);\n\n if (options.onError) {\n options.onError(new Error(errorMessage));\n }\n return Promise.reject(new Error(errorMessage));\n }\n\n // Update session timestamp for new connection\n sessionTimestampRef.current = Date.now();\n setState(\"connecting\");\n\n const requestBody: any = {\n agent_id: agentId,\n agent_version: agentVersion || \"current\",\n metadata: {\n runtime_context: {\n source: {\n type: \"react-sdk\",\n version: SDK_VERSION,\n },\n },\n call_web: {\n dynamic_variables: dynamicVariables || {},\n metadata: metadata || {},\n },\n },\n };\n\n const response = await fetch(HTTPS_API_ORIGIN, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(requestBody),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Connection failed (${response.status}): ${errorText}`\n );\n }\n\n const data = await response.json();\n setConnectionDetail(data);\n\n if (options.onConnect) {\n options.onConnect();\n }\n } catch (err) {\n setConnectionDetail(null);\n setTranscriptMap(new Map());\n setMessages([]);\n setState(\"disconnected\");\n\n const error = err instanceof Error ? err : new Error(String(err));\n\n if (options.onError) {\n options.onError(error);\n }\n }\n },\n [options, state]\n );\n\n /**\n * 음성 AI 연결을 종료합니다.\n *\n * @remarks\n * - 연결이 종료되면 `onDisconnect` 콜백이 호출됩니다.\n * - 모든 메시지와 상태가 초기화됩니다.\n * - 상태가 `disconnected`로 변경됩니다.\n * - 연결되지 않은 상태에서 호출해도 안전합니다.\n *\n * @example\n * ```tsx\n * const { disconnect } = useVoxAI();\n *\n * // 연결 종료\n * disconnect();\n * ```\n */\n const disconnect = useCallback(() => {\n // Update session timestamp on disconnect\n sessionTimestampRef.current = Date.now();\n setConnectionDetail(null);\n setTranscriptMap(new Map());\n setMessages([]);\n setState(\"disconnected\");\n\n if (options.onDisconnect) {\n options.onDisconnect();\n }\n }, [options]);\n\n /**\n * 에이전트에게 텍스트 메시지를 전송하거나 DTMF 숫자를 입력합니다.\n *\n * @param params - 전송할 메시지 또는 DTMF 숫자\n * @param params.message - 전송할 텍스트 메시지 (음성 대신 텍스트로 입력)\n * @param params.digit - 전송할 DTMF 숫자 (0-9, *, #에 해당하는 숫자)\n *\n * @remarks\n * - 연결되지 않은 상태에서 호출하면 경고 메시지가 출력되고 무시됩니다.\n * - `message`와 `digit`을 동시에 전달할 수 있습니다.\n * - 텍스트 메시지는 음성 입력 대신 사용할 수 있습니다.\n * - DTMF는 전화번호 입력 등에 활용됩니다.\n *\n * @example\n * ```tsx\n * const { send } = useVoxAI();\n *\n * // 텍스트 메시지 전송\n * send({ message: '안녕하세요' });\n *\n * // DTMF 숫자 전송\n * send({ digit: 1 });\n *\n * // 둘 다 전송\n * send({ message: '1번을 선택합니다', digit: 1 });\n * ```\n */\n const send = useCallback(\n ({ message, digit }: { message?: string; digit?: number }) => {\n if (state === \"disconnected\") {\n console.warn(\"Cannot send message: Not connected to a conversation\");\n return;\n }\n\n if (message) {\n const messageId = `user-text-${Date.now()}`;\n setTranscriptMap((prevMap) => {\n const newMap = new Map(prevMap);\n newMap.set(messageId, {\n id: messageId,\n name: \"user\",\n message: message,\n timestamp: Date.now(),\n isFinal: true,\n });\n return newMap;\n });\n\n if (channelRef.current) {\n channelRef.current.port1.postMessage({\n type: \"send_text\",\n text: message,\n });\n } else {\n console.error(\"No message channel available to send message\");\n }\n }\n\n if (digit !== undefined) {\n if (channelRef.current) {\n channelRef.current.port1.postMessage({\n type: \"send_dtmf\",\n digit: digit,\n });\n } else {\n console.error(\"No message channel available to send DTMF\");\n }\n }\n },\n [state]\n );\n\n /**\n * 실시간 오디오 파형 데이터를 가져옵니다.\n *\n * @param params - 파형 설정 옵션\n * @param params.speaker - 파형을 가져올 대상 (`\"agent\"` 또는 `\"user\"`, 기본값: `\"agent\"`)\n * @param params.barCount - 반환할 파형 막대 개수 (기본값: 10)\n * @param params.updateInterval - 파형 업데이트 간격 (밀리초, 기본값: 20)\n *\n * @returns 0~1 사이의 값을 가진 숫자 배열 (길이는 `barCount`와 동일)\n *\n * @remarks\n * - 각 값은 해당 주파수 대역의 음량을 나타냅니다 (0: 무음, 1: 최대 음량).\n * - 음성 시각화 UI를 만들 때 유용합니다.\n * - 연결되지 않은 상태에서는 모두 0으로 채워진 배열을 반환합니다.\n *\n * @example\n * ```tsx\n * const { audioWaveform, state } = useVoxAI();\n *\n * // 렌더링 루프에서 사용\n * useEffect(() => {\n * const interval = setInterval(() => {\n * // 에이전트의 파형 데이터 (20개 막대)\n * const agentWaveform = audioWaveform({\n * speaker: 'agent',\n * barCount: 20\n * });\n *\n * // 사용자의 파형 데이터\n * const userWaveform = audioWaveform({\n * speaker: 'user',\n * barCount: 20\n * });\n *\n * // 시각화 업데이트\n * updateVisualization(agentWaveform, userWaveform);\n * }, 50);\n *\n * return () => clearInterval(interval);\n * }, []);\n * ```\n */\n const audioWaveform = useCallback(\n ({\n speaker = \"agent\",\n barCount = 10,\n updateInterval = 20,\n }: {\n speaker?: \"agent\" | \"user\";\n barCount?: number;\n updateInterval?: number;\n }): number[] => {\n waveformConfigRef.current = { speaker, barCount, updateInterval };\n\n if (channelRef.current) {\n channelRef.current.port1.postMessage({\n type: \"waveform_config\",\n config: { speaker, barCount, updateInterval },\n });\n }\n\n const speakerData = waveformDataMap[speaker] || [];\n return speakerData.length > 0\n ? speakerData.slice(0, barCount)\n : Array(barCount).fill(0);\n },\n [waveformDataMap]\n );\n\n /**\n * 사용자의 마이크를 켜거나 끕니다.\n *\n * @param value - `true`면 마이크 켜기, `false`면 마이크 끄기\n *\n * @remarks\n * - 마이크를 끄면 에이전트가 사용자의 음성을 듣지 못합니다.\n * - 음성 인식도 중단됩니다.\n * - 프라이버시나 소음 차단이 필요할 때 유용합니다.\n *\n * @example\n * ```tsx\n * const { toggleMic } = useVoxAI();\n *\n * // 마이크 끄기\n * toggleMic(false);\n *\n * // 마이크 켜기\n * toggleMic(true);\n *\n * // 토글 버튼 예제\n * const [isMuted, setIsMuted] = useState(false);\n * const handleToggle = () => {\n * setIsMuted(!isMuted);\n * toggleMic(!isMuted);\n * };\n * ```\n */\n const toggleMic = useCallback((value: boolean) => {\n setIsMicEnabled(value);\n if (channelRef.current) {\n channelRef.current.port1.postMessage({\n type: \"toggle_mic\",\n enabled: value,\n });\n } else {\n console.error(\"No message channel available to toggle microphone\");\n }\n }, []);\n\n /**\n * 에이전트 음성의 볼륨을 설정합니다.\n *\n * @param volume - 볼륨 크기 (0.0 ~ 1.0 사이의 값, 0: 무음, 1: 최대 음량)\n *\n * @remarks\n * - 범위를 벗어난 값은 자동으로 0~1 사이로 조정됩니다.\n * - 예: `-0.5` → `0`, `1.5` → `1`\n * - 사용자의 환경에 따라 적절한 볼륨을 설정할 수 있습니다.\n *\n * @example\n * ```tsx\n * const { setVolume } = useVoxAI();\n *\n * // 볼륨을 50%로 설정\n * setVolume(0.5);\n *\n * // 볼륨을 최대로 설정\n * setVolume(1.0);\n *\n * // 볼륨 슬라이더 예제\n * const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n * const newVolume = parseFloat(e.target.value);\n * setVolume(newVolume);\n * };\n *\n * <input\n * type=\"range\"\n * min=\"0\"\n * max=\"1\"\n * step=\"0.1\"\n * onChange={handleVolumeChange}\n * />\n * ```\n */\n const setVolume = useCallback((volume: number) => {\n const validVolume = Math.min(Math.max(volume, 0), 1);\n if (channelRef.current) {\n channelRef.current.port1.postMessage({\n type: \"set_volume\",\n volume: validVolume,\n });\n } else {\n console.error(\"No message channel available to set volume\");\n }\n }, []);\n\n // Modify the useEffect hook that renders the LiveKit component\n useEffect(() => {\n if (!rootRef.current) return;\n\n if (connectionDetail) {\n if (!livekitComponentRef.current) {\n if (channelRef.current) {\n channelRef.current.port2.start();\n }\n\n livekitComponentRef.current = (\n <LiveKitRoom\n serverUrl={connectionDetail.serverUrl}\n token={connectionDetail.participantToken}\n audio={true}\n video={false}\n connect={true}\n onDisconnected={disconnect}\n onError={(error) => {\n console.error(\"LiveKit connection error:\", error);\n disconnect();\n if (options.onError) {\n options.onError(\n new Error(`LiveKit connection error: ${error.message}`)\n );\n }\n }}\n >\n <RoomAudioRenderer />\n {channelRef.current && (\n <StateMonitor\n port={channelRef.current.port2}\n initialConfig={\n waveformConfigRef.current || {\n barCount: 10,\n updateInterval: 20,\n }\n }\n />\n )}\n </LiveKitRoom>\n );\n }\n rootRef.current.render(livekitComponentRef.current);\n } else {\n livekitComponentRef.current = null;\n rootRef.current.render(<></>);\n }\n }, [connectionDetail, disconnect, options.onError]);\n\n return {\n connect,\n disconnect,\n state,\n messages,\n send,\n audioWaveform,\n toggleMic,\n setVolume,\n };\n}\n\n/**\n * Component that monitors LiveKit state and communicates back to the main hook\n */\nfunction StateMonitor({\n port,\n initialConfig,\n}: {\n port: MessagePort | undefined;\n initialConfig: {\n speaker?: \"agent\" | \"user\";\n barCount: number;\n updateInterval: number;\n };\n}) {\n const { agent, state } = useVoiceAssistant();\n const { send: sendChat } = useChat();\n\n // Initialize waveform config with the passed initial values, defaulting to \"agent\" if not specified\n const [waveformConfig, setWaveformConfig] = useState({\n speaker: initialConfig.speaker || \"agent\",\n barCount: initialConfig.barCount,\n updateInterval: initialConfig.updateInterval,\n });\n\n // Agent transcriptions\n const agentAudioTrack = useParticipantTracks(\n [Track.Source.Microphone],\n agent?.identity\n )[0];\n const agentTranscription = useTrackTranscription(agentAudioTrack);\n\n // Use the current config for the waveform, applying different settings based on speaker\n const agentAudioWaveform = useAudioWaveform(agentAudioTrack, {\n barCount:\n waveformConfig.speaker === \"agent\" ? waveformConfig.barCount : 120, // default if not the selected speaker\n updateInterval:\n waveformConfig.speaker === \"agent\" ? waveformConfig.updateInterval : 20,\n });\n\n // User transcriptions\n const localParticipant = useLocalParticipant();\n const localMessages = useTrackTranscription({\n publication: localParticipant.microphoneTrack,\n source: Track.Source.Microphone,\n participant: localParticipant.localParticipant,\n });\n const localAudioTrack = useParticipantTracks(\n [Track.Source.Microphone],\n localParticipant.localParticipant.identity\n )[0];\n const userAudioWaveform = useAudioWaveform(localAudioTrack, {\n barCount: waveformConfig.speaker === \"user\" ? waveformConfig.barCount : 120, // default if not the selected speaker\n updateInterval:\n waveformConfig.speaker === \"user\" ? waveformConfig.updateInterval : 20,\n });\n\n // Add separate effects to send agent and user waveform data\n useEffect(() => {\n if (!port || !agentAudioWaveform || !agentAudioWaveform.bars) return;\n\n // Send the agent waveform data\n port.postMessage({\n type: \"waveform_update\",\n waveformData: agentAudioWaveform.bars,\n speaker: \"agent\",\n });\n }, [port, agentAudioWaveform]);\n\n useEffect(() => {\n if (!port || !userAudioWaveform || !userAudioWaveform.bars) return;\n\n // Send the user waveform data\n port.postMessage({\n type: \"waveform_update\",\n waveformData: userAudioWaveform.bars,\n speaker: \"user\",\n });\n }, [port, userAudioWaveform]);\n\n // Listen for messages including config updates and mic toggle commands\n useEffect(() => {\n if (!port) return;\n\n const handleMessage = (event: MessageEvent) => {\n const data = event.data;\n\n if (data.type === \"waveform_config\" && data.config) {\n // Verify we have both required properties before updating\n if (\n typeof data.config.barCount === \"number\" &&\n typeof data.config.updateInterval === \"number\"\n ) {\n setWaveformConfig(data.config);\n }\n } else if (data.type === \"send_text\") {\n if (sendChat) {\n sendChat(data.text);\n } else {\n console.error(\"sendChat function is not available\");\n }\n } else if (data.type === \"send_dtmf\") {\n if (localParticipant.localParticipant) {\n // Use standard DTMF code (RFC 4733)\n const standardDtmfCode = 101; // Standard DTMF payload type\n localParticipant.localParticipant.publishDtmf(\n standardDtmfCode,\n data.digit\n );\n } else {\n console.error(\"Local participant is not available for DTMF\");\n }\n } else if (\n data.type === \"toggle_mic\" &&\n typeof data.enabled === \"boolean\"\n ) {\n // Handle microphone toggle\n if (localParticipant.localParticipant) {\n localParticipant.localParticipant\n .setMicrophoneEnabled(data.enabled)\n .catch((error) => {\n console.error(\"Failed to toggle microphone:\", error);\n });\n } else {\n console.error(\"Local participant is not available for mic toggle\");\n }\n } else if (\n data.type === \"set_volume\" &&\n typeof data.volume === \"number\"\n ) {\n // Handle volume control\n if (agent) {\n // The agent is a RemoteParticipant, so we can call setVolume directly\n try {\n agent.setVolume(data.volume);\n console.log(`Set agent volume to ${data.volume}`);\n } catch (error) {\n console.error(\"Failed to set agent volume:\", error);\n }\n } else {\n console.error(\"Agent is not available for volume control\");\n }\n }\n };\n\n // Make sure we start the port\n port.start();\n\n port.addEventListener(\"message\", handleMessage);\n\n return () => {\n port.removeEventListener(\"message\", handleMessage);\n };\n }, [port, sendChat, localParticipant, agent]);\n\n // Send agent state updates\n useEffect(() => {\n if (port) {\n port.postMessage({ type: \"state_update\", state });\n }\n }, [state, port]);\n\n // Send agent transcriptions\n useEffect(() => {\n if (port && agentTranscription.segments.length > 0) {\n const transcriptions = agentTranscription.segments.map((segment) => ({\n id: segment.id,\n text: segment.text,\n isFinal: segment.final,\n timestamp: Date.now(),\n speaker: \"agent\" as const,\n }));\n\n port.postMessage({\n type: \"transcription_update\",\n transcriptions,\n });\n }\n }, [agentTranscription.segments, port]);\n\n // Send user transcriptions\n useEffect(() => {\n if (port && localMessages.segments.length > 0) {\n const transcriptions = localMessages.segments.map((segment) => ({\n id: segment.id,\n text: segment.text,\n isFinal: segment.final,\n timestamp: Date.now(),\n speaker: \"user\" as const,\n }));\n\n port.postMessage({\n type: \"transcription_update\",\n transcriptions,\n });\n }\n }, [localMessages.segments, port]);\n\n // Add data channel hook for function calls\n const { message: functionToolsExecuted } = useDataChannel(\n \"function_tools_executed\",\n (msg) => {\n if (!port) return;\n\n const textDecoder = new TextDecoder();\n const messageString =\n msg.payload instanceof Uint8Array\n ? textDecoder.decode(msg.payload)\n : String(msg.payload);\n\n let tool: FunctionToolsExecuted;\n try {\n tool = JSON.parse(messageString);\n\n // Send function calls to main hook via the port\n port.postMessage({\n type: \"function_tools_executed\",\n tool: tool,\n });\n } catch (e) {\n console.error(\"Failed to parse function call log:\", e);\n }\n }\n );\n\n return null;\n}\n","// export const HTTPS_API_ORIGIN = \"https://frontend-dev.tryvox.co/api/agent/sdk\";\nexport const HTTPS_API_ORIGIN = \"https://www.tryvox.co/api/agent/sdk\";\n// export const HTTPS_API_ORIGIN = \"http://localhost:3000/api/agent/sdk\";\nexport const SDK_VERSION = \"0.5.0\";\n"],"names":["useVoxAI","options","connectionDetail","setConnectionDetail","useState","state","setState","sessionTimestampRef","useRef","Date","now","transcriptMap","setTranscriptMap","Map","messages","setMessages","prevMessagesRef","processedMessageIdsRef","Set","portalRootRef","rootRef","channelRef","livekitComponentRef","waveformDataMap","setWaveformDataMap","agent","user","waveformConfigRef","isMicEnabled","setIsMicEnabled","useEffect","allMessages","Array","from","values","sort","a","b","timestamp","messagesString","JSON","stringify","current","onMessage","filter","msg","isFinal","id","has","forEach","add","channel","MessageChannel","port1","onmessage","e","data","type","handleTranscriptionUpdate","transcriptions","speaker","prevMap","_extends","waveformData","tool","functionCallsId","newMap","set","name","start","_channelRef$current","_channelRef$current2","close","port2","useCallback","t","_prevMap$get","messageType","existingTimestamp","get","message","text","div","document","createElement","style","display","body","appendChild","createRoot","unmount","removeChild","connect","async","agentId","agentVersion","apiKey","dynamicVariables","metadata","errorMessage","console","warn","onError","Error","Promise","reject","requestBody","agent_id","agent_version","runtime_context","source","version","call_web","dynamic_variables","response","fetch","method","headers","Authorization","ok","errorText","status","json","onConnect","err","error","String","disconnect","onDisconnect","send","digit","messageId","postMessage","undefined","audioWaveform","barCount","updateInterval","config","speakerData","length","slice","fill","toggleMic","value","enabled","setVolume","volume","validVolume","Math","min","max","_jsxs","LiveKitRoom","serverUrl","token","participantToken","audio","video","onDisconnected","children","_jsx","RoomAudioRenderer","StateMonitor","port","initialConfig","render","_Fragment","useVoiceAssistant","sendChat","useChat","waveformConfig","setWaveformConfig","agentAudioTrack","useParticipantTracks","Track","Source","Microphone","identity","agentTranscription","useTrackTranscription","agentAudioWaveform","useAudioWaveform","localParticipant","useLocalParticipant","localMessages","publication","microphoneTrack","participant","localAudioTrack","userAudioWaveform","bars","handleMessage","event","publishDtmf","setMicrophoneEnabled","catch","log","addEventListener","removeEventListener","segments","map","segment","final","useDataChannel","textDecoder","TextDecoder","messageString","payload","Uint8Array","decode","parse"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{jsxs as e,jsx as t,Fragment as n}from"react/jsx-runtime";import{LiveKitRoom as r,RoomAudioRenderer as o,useVoiceAssistant as a,useChat as s,useParticipantTracks as i,useTrackTranscription as c,useAudioWaveform as l,useLocalParticipant as u,useDataChannel as p}from"@livekit/components-react";import{Track as d}from"livekit-client";import{useState as m,useRef as g,useEffect as f,useCallback as v}from"react";import{createRoot as y}from"react-dom/client";function h(a){void 0===a&&(a={});const[s,i]=m(null),[c,l]=m("disconnected"),u=g(Date.now()),[p,d]=m(new Map),[h,w]=m([]),M=g(""),_=g(new Set),k=g(null),C=g(null),x=g(null),E=g(null),[D,P]=m({agent:[],user:[]}),F=g(null),[I,S]=m(!0);f(()=>{const e=Array.from(p.values()).sort((e,t)=>e.timestamp-t.timestamp),t=JSON.stringify(e);t!==M.current&&(M.current=t,w(e),a.onMessage&&e.filter(e=>e.isFinal&&e.id&&!_.current.has(e.id)).forEach(e=>{e.id&&(_.current.add(e.id),null==a.onMessage||a.onMessage(e))}))},[p,a.onMessage]),f(()=>{const e=new MessageChannel;return e.port1.onmessage=e=>{const t=e.data;if("state_update"===t.type)l(t.state);else if("transcription_update"===t.type)N(t.transcriptions);else if("waveform_update"===t.type&&t.speaker)P(e=>({...e,[t.speaker]:t.waveformData}));else if("function_tools_executed"===t.type&&t.tool){const e="function-calls-"+Date.now();d(n=>{const r=new Map(n);return r.set(e,{id:e,name:"tool",tool:t.tool,timestamp:Date.now(),isFinal:!0}),r})}},e.port1.start(),x.current=e,()=>{var e,t;null==(e=x.current)||e.port1.close(),null==(t=x.current)||t.port2.close(),x.current=null}},[]);const N=v(e=>{d(t=>{const n=new Map(t);return e.forEach(e=>{var r;if(e.timestamp<u.current)return;const o="agent"===e.speaker?"agent":"user",a=(null==(r=t.get(e.id))?void 0:r.timestamp)||e.timestamp;n.set(e.id,{id:e.id,name:o,message:e.text,timestamp:a,isFinal:e.isFinal})}),n})},[]);f(()=>{const e=document.createElement("div");return e.style.display="none",document.body.appendChild(e),k.current=e,C.current=y(e),()=>{C.current&&C.current.unmount(),k.current&&document.body.removeChild(k.current)}},[]);const T=v(function(e){let{agentId:t,apiKey:n,dynamicVariables:r,metadata:o}=e;try{return Promise.resolve(function(e,s){try{var p=function(){if("disconnected"!==c){const e="Connection attempt rejected: Already in a connection state ("+c+")";return console.warn(e),a.onError&&a.onError(new Error(e)),Promise.reject(new Error(e))}return u.current=Date.now(),l("connecting"),Promise.resolve(fetch("https://www.tryvox.co/api/agent/sdk",{method:"POST",headers:{Authorization:"Bearer "+n,"Content-Type":"application/json"},body:JSON.stringify({agent_id:t,metadata:{runtime_context:{source:{type:"react-sdk",version:"0.3.0"}},call_web:{dynamic_variables:r||{},metadata:o||{}}}})})).then(function(e){function t(t){return Promise.resolve(e.json()).then(function(e){i(e),a.onConnect&&a.onConnect()})}const n=function(){if(!e.ok)return Promise.resolve(e.text()).then(function(t){throw new Error("Connection failed ("+e.status+"): "+t)})}();return n&&n.then?n.then(t):t()})}()}catch(e){return s(e)}return p&&p.then?p.then(void 0,s):p}(0,function(e){i(null),d(new Map),w([]),l("disconnected");const t=e instanceof Error?e:new Error(String(e));a.onError&&a.onError(t)}))}catch(e){return Promise.reject(e)}},[a,c]),j=v(()=>{u.current=Date.now(),i(null),d(new Map),w([]),l("disconnected"),a.onDisconnect&&a.onDisconnect()},[a]),A=v(e=>{let{message:t,digit:n}=e;if("disconnected"!==c){if(t){const e="user-text-"+Date.now();d(n=>{const r=new Map(n);return r.set(e,{id:e,name:"user",message:t,timestamp:Date.now(),isFinal:!0}),r}),x.current?x.current.port1.postMessage({type:"send_text",text:t}):console.error("No message channel available to send message")}void 0!==n&&(x.current?x.current.port1.postMessage({type:"send_dtmf",digit:n}):console.error("No message channel available to send DTMF"))}else console.warn("Cannot send message: Not connected to a conversation")},[c]),L=v(e=>{let{speaker:t="agent",barCount:n=10,updateInterval:r=20}=e;F.current={speaker:t,barCount:n,updateInterval:r},x.current&&x.current.port1.postMessage({type:"waveform_config",config:{speaker:t,barCount:n,updateInterval:r}});const o=D[t]||[];return o.length>0?o.slice(0,n):Array(n).fill(0)},[D]),O=v(e=>{S(e),x.current?x.current.port1.postMessage({type:"toggle_mic",enabled:e}):console.error("No message channel available to toggle microphone")},[]),J=v(e=>{const t=Math.min(Math.max(e,0),1);x.current?x.current.port1.postMessage({type:"set_volume",volume:t}):console.error("No message channel available to set volume")},[]);return f(()=>{C.current&&(s?(E.current||(x.current&&x.current.port2.start(),E.current=e(r,{serverUrl:s.serverUrl,token:s.participantToken,audio:!0,video:!1,connect:!0,onDisconnected:j,onError:e=>{console.error("LiveKit connection error:",e),j(),a.onError&&a.onError(new Error("LiveKit connection error: "+e.message))},children:[t(o,{}),x.current&&t(b,{port:x.current.port2,initialConfig:F.current||{barCount:10,updateInterval:20}})]})),C.current.render(E.current)):(E.current=null,C.current.render(t(n,{}))))},[s,j,a.onError]),{connect:T,disconnect:j,state:c,messages:h,send:A,audioWaveform:L,toggleMic:O,setVolume:J}}function b(e){let{port:t,initialConfig:n}=e;const{agent:r,state:o}=a(),{send:g}=s(),[v,y]=m({speaker:n.speaker||"agent",barCount:n.barCount,updateInterval:n.updateInterval}),h=i([d.Source.Microphone],null==r?void 0:r.identity)[0],b=c(h),w=l(h,{barCount:"agent"===v.speaker?v.barCount:120,updateInterval:"agent"===v.speaker?v.updateInterval:20}),M=u(),_=c({publication:M.microphoneTrack,source:d.Source.Microphone,participant:M.localParticipant}),k=i([d.Source.Microphone],M.localParticipant.identity)[0],C=l(k,{barCount:"user"===v.speaker?v.barCount:120,updateInterval:"user"===v.speaker?v.updateInterval:20});return f(()=>{t&&w&&w.bars&&t.postMessage({type:"waveform_update",waveformData:w.bars,speaker:"agent"})},[t,w]),f(()=>{t&&C&&C.bars&&t.postMessage({type:"waveform_update",waveformData:C.bars,speaker:"user"})},[t,C]),f(()=>{if(!t)return;const e=e=>{const t=e.data;if("waveform_config"===t.type&&t.config)"number"==typeof t.config.barCount&&"number"==typeof t.config.updateInterval&&y(t.config);else if("send_text"===t.type)g?g(t.text):console.error("sendChat function is not available");else if("send_dtmf"===t.type)M.localParticipant?M.localParticipant.publishDtmf(101,t.digit):console.error("Local participant is not available for DTMF");else if("toggle_mic"===t.type&&"boolean"==typeof t.enabled)M.localParticipant?M.localParticipant.setMicrophoneEnabled(t.enabled).catch(e=>{console.error("Failed to toggle microphone:",e)}):console.error("Local participant is not available for mic toggle");else if("set_volume"===t.type&&"number"==typeof t.volume)if(r)try{r.setVolume(t.volume),console.log("Set agent volume to "+t.volume)}catch(e){console.error("Failed to set agent volume:",e)}else console.error("Agent is not available for volume control")};return t.start(),t.addEventListener("message",e),()=>{t.removeEventListener("message",e)}},[t,g,M,r]),f(()=>{t&&t.postMessage({type:"state_update",state:o})},[o,t]),f(()=>{if(t&&b.segments.length>0){const e=b.segments.map(e=>({id:e.id,text:e.text,isFinal:e.final,timestamp:Date.now(),speaker:"agent"}));t.postMessage({type:"transcription_update",transcriptions:e})}},[b.segments,t]),f(()=>{if(t&&_.segments.length>0){const e=_.segments.map(e=>({id:e.id,text:e.text,isFinal:e.final,timestamp:Date.now(),speaker:"user"}));t.postMessage({type:"transcription_update",transcriptions:e})}},[_.segments,t]),p("function_tools_executed",e=>{if(!t)return;const n=new TextDecoder,r=e.payload instanceof Uint8Array?n.decode(e.payload):String(e.payload);let o;try{o=JSON.parse(r),t.postMessage({type:"function_tools_executed",tool:o})}catch(e){console.error("Failed to parse function call log:",e)}}),null}export{h as useVoxAI};
1
+ import{jsxs as e,jsx as t,Fragment as n}from"react/jsx-runtime";import{LiveKitRoom as r,RoomAudioRenderer as o,useVoiceAssistant as a,useChat as s,useParticipantTracks as i,useTrackTranscription as c,useAudioWaveform as l,useLocalParticipant as u,useDataChannel as p}from"@livekit/components-react";import{Track as d}from"livekit-client";import{useState as m,useRef as g,useEffect as f,useCallback as v}from"react";import{createRoot as y}from"react-dom/client";function h(a){void 0===a&&(a={});const[s,i]=m(null),[c,l]=m("disconnected"),u=g(Date.now()),[p,d]=m(new Map),[h,w]=m([]),M=g(""),_=g(new Set),k=g(null),C=g(null),x=g(null),E=g(null),[D,P]=m({agent:[],user:[]}),F=g(null),[I,S]=m(!0);f(()=>{const e=Array.from(p.values()).sort((e,t)=>e.timestamp-t.timestamp),t=JSON.stringify(e);t!==M.current&&(M.current=t,w(e),a.onMessage&&e.filter(e=>e.isFinal&&e.id&&!_.current.has(e.id)).forEach(e=>{e.id&&(_.current.add(e.id),null==a.onMessage||a.onMessage(e))}))},[p,a.onMessage]),f(()=>{const e=new MessageChannel;return e.port1.onmessage=e=>{const t=e.data;if("state_update"===t.type)l(t.state);else if("transcription_update"===t.type)N(t.transcriptions);else if("waveform_update"===t.type&&t.speaker)P(e=>({...e,[t.speaker]:t.waveformData}));else if("function_tools_executed"===t.type&&t.tool){const e="function-calls-"+Date.now();d(n=>{const r=new Map(n);return r.set(e,{id:e,name:"tool",tool:t.tool,timestamp:Date.now(),isFinal:!0}),r})}},e.port1.start(),x.current=e,()=>{var e,t;null==(e=x.current)||e.port1.close(),null==(t=x.current)||t.port2.close(),x.current=null}},[]);const N=v(e=>{d(t=>{const n=new Map(t);return e.forEach(e=>{var r;if(e.timestamp<u.current)return;const o="agent"===e.speaker?"agent":"user",a=(null==(r=t.get(e.id))?void 0:r.timestamp)||e.timestamp;n.set(e.id,{id:e.id,name:o,message:e.text,timestamp:a,isFinal:e.isFinal})}),n})},[]);f(()=>{const e=document.createElement("div");return e.style.display="none",document.body.appendChild(e),k.current=e,C.current=y(e),()=>{C.current&&C.current.unmount(),k.current&&document.body.removeChild(k.current)}},[]);const T=v(function(e){let{agentId:t,agentVersion:n,apiKey:r,dynamicVariables:o,metadata:s}=e;try{return Promise.resolve(function(e,p){try{var d=function(){if("disconnected"!==c){const e="Connection attempt rejected: Already in a connection state ("+c+")";return console.warn(e),a.onError&&a.onError(new Error(e)),Promise.reject(new Error(e))}return u.current=Date.now(),l("connecting"),Promise.resolve(fetch("https://www.tryvox.co/api/agent/sdk",{method:"POST",headers:{Authorization:"Bearer "+r,"Content-Type":"application/json"},body:JSON.stringify({agent_id:t,agent_version:n||"current",metadata:{runtime_context:{source:{type:"react-sdk",version:"0.5.0"}},call_web:{dynamic_variables:o||{},metadata:s||{}}}})})).then(function(e){function t(t){return Promise.resolve(e.json()).then(function(e){i(e),a.onConnect&&a.onConnect()})}const n=function(){if(!e.ok)return Promise.resolve(e.text()).then(function(t){throw new Error("Connection failed ("+e.status+"): "+t)})}();return n&&n.then?n.then(t):t()})}()}catch(e){return p(e)}return d&&d.then?d.then(void 0,p):d}(0,function(e){i(null),d(new Map),w([]),l("disconnected");const t=e instanceof Error?e:new Error(String(e));a.onError&&a.onError(t)}))}catch(e){return Promise.reject(e)}},[a,c]),j=v(()=>{u.current=Date.now(),i(null),d(new Map),w([]),l("disconnected"),a.onDisconnect&&a.onDisconnect()},[a]),A=v(e=>{let{message:t,digit:n}=e;if("disconnected"!==c){if(t){const e="user-text-"+Date.now();d(n=>{const r=new Map(n);return r.set(e,{id:e,name:"user",message:t,timestamp:Date.now(),isFinal:!0}),r}),x.current?x.current.port1.postMessage({type:"send_text",text:t}):console.error("No message channel available to send message")}void 0!==n&&(x.current?x.current.port1.postMessage({type:"send_dtmf",digit:n}):console.error("No message channel available to send DTMF"))}else console.warn("Cannot send message: Not connected to a conversation")},[c]),L=v(e=>{let{speaker:t="agent",barCount:n=10,updateInterval:r=20}=e;F.current={speaker:t,barCount:n,updateInterval:r},x.current&&x.current.port1.postMessage({type:"waveform_config",config:{speaker:t,barCount:n,updateInterval:r}});const o=D[t]||[];return o.length>0?o.slice(0,n):Array(n).fill(0)},[D]),O=v(e=>{S(e),x.current?x.current.port1.postMessage({type:"toggle_mic",enabled:e}):console.error("No message channel available to toggle microphone")},[]),V=v(e=>{const t=Math.min(Math.max(e,0),1);x.current?x.current.port1.postMessage({type:"set_volume",volume:t}):console.error("No message channel available to set volume")},[]);return f(()=>{C.current&&(s?(E.current||(x.current&&x.current.port2.start(),E.current=e(r,{serverUrl:s.serverUrl,token:s.participantToken,audio:!0,video:!1,connect:!0,onDisconnected:j,onError:e=>{console.error("LiveKit connection error:",e),j(),a.onError&&a.onError(new Error("LiveKit connection error: "+e.message))},children:[t(o,{}),x.current&&t(b,{port:x.current.port2,initialConfig:F.current||{barCount:10,updateInterval:20}})]})),C.current.render(E.current)):(E.current=null,C.current.render(t(n,{}))))},[s,j,a.onError]),{connect:T,disconnect:j,state:c,messages:h,send:A,audioWaveform:L,toggleMic:O,setVolume:V}}function b(e){let{port:t,initialConfig:n}=e;const{agent:r,state:o}=a(),{send:g}=s(),[v,y]=m({speaker:n.speaker||"agent",barCount:n.barCount,updateInterval:n.updateInterval}),h=i([d.Source.Microphone],null==r?void 0:r.identity)[0],b=c(h),w=l(h,{barCount:"agent"===v.speaker?v.barCount:120,updateInterval:"agent"===v.speaker?v.updateInterval:20}),M=u(),_=c({publication:M.microphoneTrack,source:d.Source.Microphone,participant:M.localParticipant}),k=i([d.Source.Microphone],M.localParticipant.identity)[0],C=l(k,{barCount:"user"===v.speaker?v.barCount:120,updateInterval:"user"===v.speaker?v.updateInterval:20});return f(()=>{t&&w&&w.bars&&t.postMessage({type:"waveform_update",waveformData:w.bars,speaker:"agent"})},[t,w]),f(()=>{t&&C&&C.bars&&t.postMessage({type:"waveform_update",waveformData:C.bars,speaker:"user"})},[t,C]),f(()=>{if(!t)return;const e=e=>{const t=e.data;if("waveform_config"===t.type&&t.config)"number"==typeof t.config.barCount&&"number"==typeof t.config.updateInterval&&y(t.config);else if("send_text"===t.type)g?g(t.text):console.error("sendChat function is not available");else if("send_dtmf"===t.type)M.localParticipant?M.localParticipant.publishDtmf(101,t.digit):console.error("Local participant is not available for DTMF");else if("toggle_mic"===t.type&&"boolean"==typeof t.enabled)M.localParticipant?M.localParticipant.setMicrophoneEnabled(t.enabled).catch(e=>{console.error("Failed to toggle microphone:",e)}):console.error("Local participant is not available for mic toggle");else if("set_volume"===t.type&&"number"==typeof t.volume)if(r)try{r.setVolume(t.volume),console.log("Set agent volume to "+t.volume)}catch(e){console.error("Failed to set agent volume:",e)}else console.error("Agent is not available for volume control")};return t.start(),t.addEventListener("message",e),()=>{t.removeEventListener("message",e)}},[t,g,M,r]),f(()=>{t&&t.postMessage({type:"state_update",state:o})},[o,t]),f(()=>{if(t&&b.segments.length>0){const e=b.segments.map(e=>({id:e.id,text:e.text,isFinal:e.final,timestamp:Date.now(),speaker:"agent"}));t.postMessage({type:"transcription_update",transcriptions:e})}},[b.segments,t]),f(()=>{if(t&&_.segments.length>0){const e=_.segments.map(e=>({id:e.id,text:e.text,isFinal:e.final,timestamp:Date.now(),speaker:"user"}));t.postMessage({type:"transcription_update",transcriptions:e})}},[_.segments,t]),p("function_tools_executed",e=>{if(!t)return;const n=new TextDecoder,r=e.payload instanceof Uint8Array?n.decode(e.payload):String(e.payload);let o;try{o=JSON.parse(r),t.postMessage({type:"function_tools_executed",tool:o})}catch(e){console.error("Failed to parse function call log:",e)}}),null}export{h as useVoxAI};
2
2
  //# sourceMappingURL=lib.module.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"lib.module.js","sources":["../src/hooks/useVoxAI.tsx","../src/utils/constants.ts"],"sourcesContent":["import {\n LiveKitRoom,\n RoomAudioRenderer,\n useAudioWaveform,\n useChat,\n useDataChannel,\n useLocalParticipant,\n useParticipantTracks,\n useTrackTranscription,\n useVoiceAssistant,\n} from \"@livekit/components-react\";\nimport { Track } from \"livekit-client\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { createRoot, Root } from \"react-dom/client\";\nimport { HTTPS_API_ORIGIN, SDK_VERSION } from \"../utils/constants\";\n\ntype VoxConnectionDetail = {\n serverUrl: string;\n roomName: string;\n participantName: string;\n participantToken: string;\n};\n\n/**\n * VoxAgentState\n * @description The state of the agent\n */\nexport type VoxAgentState =\n | \"disconnected\"\n | \"connecting\"\n | \"initializing\"\n | \"listening\"\n | \"thinking\"\n | \"speaking\";\n\n/**\n * Function call related types\n */\nexport interface FunctionToolsExecuted {\n type: \"function_tools_executed\";\n function_calls: FunctionCallInfo[];\n function_call_outputs: FunctionCallResult[];\n}\n\nexport interface FunctionCallInfo {\n id: string;\n type: string;\n call_id: string;\n arguments: string;\n name: string;\n}\n\nexport interface FunctionCallResult {\n id: string;\n name: string;\n type: string;\n call_id: string;\n output: string;\n is_error: boolean;\n}\n\n/**\n * VoxMessage\n * @description The message type between the agent and the user\n */\nexport type VoxMessage = {\n id?: string;\n name: \"agent\" | \"user\" | \"tool\";\n message?: string;\n timestamp: number;\n isFinal?: boolean;\n tool?: FunctionToolsExecuted;\n};\n\n/**\n * VoxAIOptions\n * @description The callback functions for the useVoxAI hook\n */\nexport interface VoxAIOptions {\n onConnect?: () => void;\n onDisconnect?: () => void;\n onError?: (error: Error) => void;\n onMessage?: (message: VoxMessage) => void;\n}\n\n// Message channel event types\ntype MessageChannelEvent =\n | { type: \"state_update\"; state: VoxAgentState }\n | { type: \"transcription_update\"; transcriptions: TranscriptionSegment[] }\n | {\n type: \"waveform_update\";\n waveformData: number[];\n speaker: \"agent\" | \"user\";\n }\n | { type: \"function_tools_executed\"; tool: FunctionToolsExecuted };\n\ntype TranscriptionSegment = {\n id: string;\n text: string;\n isFinal: boolean;\n timestamp: number;\n speaker: \"agent\" | \"user\";\n};\n\n/**\n * ConnectParams\n * @param agentId - The agent ID\n * @param apiKey - The API key\n * @param dynamicVariables - The dynamic variables\n * @param metadata - 이 메타데이터는 아웃바운드 웹훅, 통화 기록에 포함됩니다.\n */\nexport interface ConnectParams {\n agentId: string;\n apiKey: string;\n dynamicVariables?: Record<string, any>;\n metadata?: Record<string, any>;\n}\n\n/**\n * useVoxAI\n * @description The hook for integrating with vox.ai voice assistant\n * @param options - The options for the useVoxAI hook\n * @returns The useVoxAI hook\n * @example\n * const { connect, disconnect, state, messages, send } = useVoxAI({\n * onConnect: () => console.log(\"Connected\"),\n * onDisconnect: () => console.log(\"Disconnected\"),\n * onError: (error) => console.error(\"Error:\", error),\n * onMessage: (message) => console.log(\"Message:\", message),\n * });\n */\nexport function useVoxAI(options: VoxAIOptions = {}) {\n // Connection state\n const [connectionDetail, setConnectionDetail] =\n useState<VoxConnectionDetail | null>(null);\n const [state, setState] = useState<VoxAgentState>(\"disconnected\");\n\n // Session timestamp to filter out stale asynchronous events\n const sessionTimestampRef = useRef<number>(Date.now());\n\n // Message handling\n const [transcriptMap, setTranscriptMap] = useState<Map<string, VoxMessage>>(\n new Map()\n );\n const [messages, setMessages] = useState<VoxMessage[]>([]);\n const prevMessagesRef = useRef<string>(\"\");\n\n // Track which messages we've already sent to the onMessage callback\n const processedMessageIdsRef = useRef<Set<string>>(new Set());\n\n // DOM manipulation for LiveKit portal\n const portalRootRef = useRef<HTMLDivElement | null>(null);\n const rootRef = useRef<Root | null>(null);\n\n // Communication channel\n const channelRef = useRef<MessageChannel | null>(null);\n\n // Add this near the start of your useVoxAI hook\n const livekitComponentRef = useRef<React.ReactNode>(null);\n\n // Replace the single waveform state with a map for multiple speakers\n const [waveformDataMap, setWaveformDataMap] = useState<\n Record<string, number[]>\n >({\n agent: [],\n user: [],\n });\n\n // Add back the waveform config reference\n const waveformConfigRef = useRef<{\n speaker?: \"agent\" | \"user\";\n barCount: number;\n updateInterval: number;\n } | null>(null);\n\n // Add a new state to track microphone status\n const [isMicEnabled, setIsMicEnabled] = useState<boolean>(true);\n\n // Update messages whenever transcriptMap changes\n useEffect(() => {\n const allMessages = Array.from(transcriptMap.values()).sort(\n (a, b) => a.timestamp - b.timestamp\n );\n\n // Only update if the messages have actually changed\n const messagesString = JSON.stringify(allMessages);\n if (messagesString !== prevMessagesRef.current) {\n prevMessagesRef.current = messagesString;\n setMessages(allMessages);\n\n // Only trigger onMessage for new final messages that haven't been processed yet\n if (options.onMessage) {\n allMessages\n .filter(\n (msg) =>\n msg.isFinal &&\n msg.id &&\n !processedMessageIdsRef.current.has(msg.id)\n )\n .forEach((msg) => {\n if (msg.id) {\n // Mark this message as processed\n processedMessageIdsRef.current.add(msg.id);\n // Call the callback\n options.onMessage?.(msg);\n }\n });\n }\n }\n }, [transcriptMap, options.onMessage]);\n\n // Initialize message channel - ensure ports are properly connected\n useEffect(() => {\n const channel = new MessageChannel();\n\n channel.port1.onmessage = (e) => {\n const data = e.data as MessageChannelEvent;\n\n if (data.type === \"state_update\") {\n setState(data.state);\n } else if (data.type === \"transcription_update\") {\n handleTranscriptionUpdate(data.transcriptions);\n } else if (data.type === \"waveform_update\" && data.speaker) {\n // Store the waveform data for the specific speaker\n setWaveformDataMap((prevMap) => ({\n ...prevMap,\n [data.speaker]: data.waveformData,\n }));\n } else if (data.type === \"function_tools_executed\" && data.tool) {\n // Handle function calls\n const functionCallsId = `function-calls-${Date.now()}`;\n setTranscriptMap((prevMap) => {\n const newMap = new Map(prevMap);\n newMap.set(functionCallsId, {\n id: functionCallsId,\n name: \"tool\",\n tool: data.tool,\n timestamp: Date.now(),\n isFinal: true,\n });\n return newMap;\n });\n }\n };\n\n // Start the port\n channel.port1.start();\n\n // Store the channel reference\n channelRef.current = channel;\n\n return () => {\n channelRef.current?.port1.close();\n channelRef.current?.port2.close();\n channelRef.current = null;\n };\n }, []);\n\n // Process incoming transcriptions and filter out stale events\n const handleTranscriptionUpdate = useCallback(\n (transcriptions: TranscriptionSegment[]) => {\n setTranscriptMap((prevMap) => {\n const newMap = new Map(prevMap);\n\n transcriptions.forEach((t) => {\n // Only process transcriptions generated after the current session timestamp\n if (t.timestamp < sessionTimestampRef.current) {\n return;\n }\n const messageType = t.speaker === \"agent\" ? \"agent\" : \"user\";\n // Use existing timestamp if we already have this segment\n const existingTimestamp = prevMap.get(t.id)?.timestamp || t.timestamp;\n\n newMap.set(t.id, {\n id: t.id,\n name: messageType,\n message: t.text,\n timestamp: existingTimestamp,\n isFinal: t.isFinal,\n });\n });\n\n return newMap;\n });\n },\n []\n );\n\n // Set up DOM portal for LiveKit\n useEffect(() => {\n const div = document.createElement(\"div\");\n div.style.display = \"none\";\n document.body.appendChild(div);\n portalRootRef.current = div;\n rootRef.current = createRoot(div);\n\n return () => {\n if (rootRef.current) {\n rootRef.current.unmount();\n }\n if (portalRootRef.current) {\n document.body.removeChild(portalRootRef.current);\n }\n };\n }, []);\n\n // Connect to VoxAI service - updated to include dynamicVariables\n const connect = useCallback(\n async ({ agentId, apiKey, dynamicVariables, metadata }: ConnectParams) => {\n try {\n // Prevent connecting if already in a connection state\n if (state !== \"disconnected\") {\n const errorMessage = `Connection attempt rejected: Already in a connection state (${state})`;\n console.warn(errorMessage);\n\n if (options.onError) {\n options.onError(new Error(errorMessage));\n }\n return Promise.reject(new Error(errorMessage));\n }\n\n // Update session timestamp for new connection\n sessionTimestampRef.current = Date.now();\n setState(\"connecting\");\n\n const response = await fetch(HTTPS_API_ORIGIN, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n agent_id: agentId,\n metadata: {\n runtime_context: {\n source: {\n type: \"react-sdk\",\n version: SDK_VERSION,\n },\n },\n call_web: {\n dynamic_variables: dynamicVariables || {},\n metadata: metadata || {},\n },\n },\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Connection failed (${response.status}): ${errorText}`\n );\n }\n\n const data = await response.json();\n setConnectionDetail(data);\n\n if (options.onConnect) {\n options.onConnect();\n }\n } catch (err) {\n setConnectionDetail(null);\n setTranscriptMap(new Map());\n setMessages([]);\n setState(\"disconnected\");\n\n const error = err instanceof Error ? err : new Error(String(err));\n\n if (options.onError) {\n options.onError(error);\n }\n }\n },\n [options, state]\n );\n\n // Disconnect from VoxAI service, updating the session timestamp to ignore stale events\n const disconnect = useCallback(() => {\n // Update session timestamp on disconnect\n sessionTimestampRef.current = Date.now();\n setConnectionDetail(null);\n setTranscriptMap(new Map());\n setMessages([]);\n setState(\"disconnected\");\n\n if (options.onDisconnect) {\n options.onDisconnect();\n }\n }, [options]);\n\n // Update the send function with debugging and error checking\n const send = useCallback(\n ({ message, digit }: { message?: string; digit?: number }) => {\n if (state === \"disconnected\") {\n console.warn(\"Cannot send message: Not connected to a conversation\");\n return;\n }\n\n if (message) {\n const messageId = `user-text-${Date.now()}`;\n setTranscriptMap((prevMap) => {\n const newMap = new Map(prevMap);\n newMap.set(messageId, {\n id: messageId,\n name: \"user\",\n message: message,\n timestamp: Date.now(),\n isFinal: true,\n });\n return newMap;\n });\n\n if (channelRef.current) {\n channelRef.current.port1.postMessage({\n type: \"send_text\",\n text: message,\n });\n } else {\n console.error(\"No message channel available to send message\");\n }\n }\n\n if (digit !== undefined) {\n if (channelRef.current) {\n channelRef.current.port1.postMessage({\n type: \"send_dtmf\",\n digit: digit,\n });\n } else {\n console.error(\"No message channel available to send DTMF\");\n }\n }\n },\n [state]\n );\n\n // Update the audioWaveform function to return data for the requested speaker\n const audioWaveform = useCallback(\n ({\n speaker = \"agent\",\n barCount = 10,\n updateInterval = 20,\n }: {\n speaker?: \"agent\" | \"user\";\n barCount?: number;\n updateInterval?: number;\n }): number[] => {\n waveformConfigRef.current = { speaker, barCount, updateInterval };\n\n if (channelRef.current) {\n channelRef.current.port1.postMessage({\n type: \"waveform_config\",\n config: { speaker, barCount, updateInterval },\n });\n }\n\n const speakerData = waveformDataMap[speaker] || [];\n return speakerData.length > 0\n ? speakerData.slice(0, barCount)\n : Array(barCount).fill(0);\n },\n [waveformDataMap]\n );\n\n // Add toggleMic function that will be exposed in the hook's return value\n const toggleMic = useCallback((value: boolean) => {\n setIsMicEnabled(value);\n if (channelRef.current) {\n channelRef.current.port1.postMessage({\n type: \"toggle_mic\",\n enabled: value,\n });\n } else {\n console.error(\"No message channel available to toggle microphone\");\n }\n }, []);\n\n // Add setVolume function that will be exposed in the hook's return value\n const setVolume = useCallback((volume: number) => {\n const validVolume = Math.min(Math.max(volume, 0), 1);\n if (channelRef.current) {\n channelRef.current.port1.postMessage({\n type: \"set_volume\",\n volume: validVolume,\n });\n } else {\n console.error(\"No message channel available to set volume\");\n }\n }, []);\n\n // Modify the useEffect hook that renders the LiveKit component\n useEffect(() => {\n if (!rootRef.current) return;\n\n if (connectionDetail) {\n if (!livekitComponentRef.current) {\n if (channelRef.current) {\n channelRef.current.port2.start();\n }\n\n livekitComponentRef.current = (\n <LiveKitRoom\n serverUrl={connectionDetail.serverUrl}\n token={connectionDetail.participantToken}\n audio={true}\n video={false}\n connect={true}\n onDisconnected={disconnect}\n onError={(error) => {\n console.error(\"LiveKit connection error:\", error);\n disconnect();\n if (options.onError) {\n options.onError(\n new Error(`LiveKit connection error: ${error.message}`)\n );\n }\n }}\n >\n <RoomAudioRenderer />\n {channelRef.current && (\n <StateMonitor\n port={channelRef.current.port2}\n initialConfig={\n waveformConfigRef.current || {\n barCount: 10,\n updateInterval: 20,\n }\n }\n />\n )}\n </LiveKitRoom>\n );\n }\n rootRef.current.render(livekitComponentRef.current);\n } else {\n livekitComponentRef.current = null;\n rootRef.current.render(<></>);\n }\n }, [connectionDetail, disconnect, options.onError]);\n\n return {\n connect,\n disconnect,\n state,\n messages,\n send,\n audioWaveform,\n toggleMic,\n setVolume,\n };\n}\n\n/**\n * Component that monitors LiveKit state and communicates back to the main hook\n */\nfunction StateMonitor({\n port,\n initialConfig,\n}: {\n port: MessagePort | undefined;\n initialConfig: {\n speaker?: \"agent\" | \"user\";\n barCount: number;\n updateInterval: number;\n };\n}) {\n const { agent, state } = useVoiceAssistant();\n const { send: sendChat } = useChat();\n\n // Initialize waveform config with the passed initial values, defaulting to \"agent\" if not specified\n const [waveformConfig, setWaveformConfig] = useState({\n speaker: initialConfig.speaker || \"agent\",\n barCount: initialConfig.barCount,\n updateInterval: initialConfig.updateInterval,\n });\n\n // Agent transcriptions\n const agentAudioTrack = useParticipantTracks(\n [Track.Source.Microphone],\n agent?.identity\n )[0];\n const agentTranscription = useTrackTranscription(agentAudioTrack);\n\n // Use the current config for the waveform, applying different settings based on speaker\n const agentAudioWaveform = useAudioWaveform(agentAudioTrack, {\n barCount:\n waveformConfig.speaker === \"agent\" ? waveformConfig.barCount : 120, // default if not the selected speaker\n updateInterval:\n waveformConfig.speaker === \"agent\" ? waveformConfig.updateInterval : 20,\n });\n\n // User transcriptions\n const localParticipant = useLocalParticipant();\n const localMessages = useTrackTranscription({\n publication: localParticipant.microphoneTrack,\n source: Track.Source.Microphone,\n participant: localParticipant.localParticipant,\n });\n const localAudioTrack = useParticipantTracks(\n [Track.Source.Microphone],\n localParticipant.localParticipant.identity\n )[0];\n const userAudioWaveform = useAudioWaveform(localAudioTrack, {\n barCount: waveformConfig.speaker === \"user\" ? waveformConfig.barCount : 120, // default if not the selected speaker\n updateInterval:\n waveformConfig.speaker === \"user\" ? waveformConfig.updateInterval : 20,\n });\n\n // Add separate effects to send agent and user waveform data\n useEffect(() => {\n if (!port || !agentAudioWaveform || !agentAudioWaveform.bars) return;\n\n // Send the agent waveform data\n port.postMessage({\n type: \"waveform_update\",\n waveformData: agentAudioWaveform.bars,\n speaker: \"agent\",\n });\n }, [port, agentAudioWaveform]);\n\n useEffect(() => {\n if (!port || !userAudioWaveform || !userAudioWaveform.bars) return;\n\n // Send the user waveform data\n port.postMessage({\n type: \"waveform_update\",\n waveformData: userAudioWaveform.bars,\n speaker: \"user\",\n });\n }, [port, userAudioWaveform]);\n\n // Listen for messages including config updates and mic toggle commands\n useEffect(() => {\n if (!port) return;\n\n const handleMessage = (event: MessageEvent) => {\n const data = event.data;\n\n if (data.type === \"waveform_config\" && data.config) {\n // Verify we have both required properties before updating\n if (\n typeof data.config.barCount === \"number\" &&\n typeof data.config.updateInterval === \"number\"\n ) {\n setWaveformConfig(data.config);\n }\n } else if (data.type === \"send_text\") {\n if (sendChat) {\n sendChat(data.text);\n } else {\n console.error(\"sendChat function is not available\");\n }\n } else if (data.type === \"send_dtmf\") {\n if (localParticipant.localParticipant) {\n // Use standard DTMF code (RFC 4733)\n const standardDtmfCode = 101; // Standard DTMF payload type\n localParticipant.localParticipant.publishDtmf(\n standardDtmfCode,\n data.digit\n );\n } else {\n console.error(\"Local participant is not available for DTMF\");\n }\n } else if (\n data.type === \"toggle_mic\" &&\n typeof data.enabled === \"boolean\"\n ) {\n // Handle microphone toggle\n if (localParticipant.localParticipant) {\n localParticipant.localParticipant\n .setMicrophoneEnabled(data.enabled)\n .catch((error) => {\n console.error(\"Failed to toggle microphone:\", error);\n });\n } else {\n console.error(\"Local participant is not available for mic toggle\");\n }\n } else if (\n data.type === \"set_volume\" &&\n typeof data.volume === \"number\"\n ) {\n // Handle volume control\n if (agent) {\n // The agent is a RemoteParticipant, so we can call setVolume directly\n try {\n agent.setVolume(data.volume);\n console.log(`Set agent volume to ${data.volume}`);\n } catch (error) {\n console.error(\"Failed to set agent volume:\", error);\n }\n } else {\n console.error(\"Agent is not available for volume control\");\n }\n }\n };\n\n // Make sure we start the port\n port.start();\n\n port.addEventListener(\"message\", handleMessage);\n\n return () => {\n port.removeEventListener(\"message\", handleMessage);\n };\n }, [port, sendChat, localParticipant, agent]);\n\n // Send agent state updates\n useEffect(() => {\n if (port) {\n port.postMessage({ type: \"state_update\", state });\n }\n }, [state, port]);\n\n // Send agent transcriptions\n useEffect(() => {\n if (port && agentTranscription.segments.length > 0) {\n const transcriptions = agentTranscription.segments.map((segment) => ({\n id: segment.id,\n text: segment.text,\n isFinal: segment.final,\n timestamp: Date.now(),\n speaker: \"agent\" as const,\n }));\n\n port.postMessage({\n type: \"transcription_update\",\n transcriptions,\n });\n }\n }, [agentTranscription.segments, port]);\n\n // Send user transcriptions\n useEffect(() => {\n if (port && localMessages.segments.length > 0) {\n const transcriptions = localMessages.segments.map((segment) => ({\n id: segment.id,\n text: segment.text,\n isFinal: segment.final,\n timestamp: Date.now(),\n speaker: \"user\" as const,\n }));\n\n port.postMessage({\n type: \"transcription_update\",\n transcriptions,\n });\n }\n }, [localMessages.segments, port]);\n\n // Add data channel hook for function calls\n const { message: functionToolsExecuted } = useDataChannel(\n \"function_tools_executed\",\n (msg) => {\n if (!port) return;\n\n const textDecoder = new TextDecoder();\n const messageString =\n msg.payload instanceof Uint8Array\n ? textDecoder.decode(msg.payload)\n : String(msg.payload);\n\n let tool: FunctionToolsExecuted;\n try {\n tool = JSON.parse(messageString);\n\n // Send function calls to main hook via the port\n port.postMessage({\n type: \"function_tools_executed\",\n tool: tool,\n });\n } catch (e) {\n console.error(\"Failed to parse function call log:\", e);\n }\n }\n );\n\n return null;\n}\n","// export const HTTPS_API_ORIGIN = \"https://frontend-dev.tryvox.co/api/agent/sdk\";\nexport const HTTPS_API_ORIGIN = \"https://www.tryvox.co/api/agent/sdk\";\n// export const HTTPS_API_ORIGIN = \"http://localhost:3000/api/agent/sdk\";\nexport const SDK_VERSION = \"0.3.0\";\n"],"names":["useVoxAI","options","connectionDetail","setConnectionDetail","useState","state","setState","sessionTimestampRef","useRef","Date","now","transcriptMap","setTranscriptMap","Map","messages","setMessages","prevMessagesRef","processedMessageIdsRef","Set","portalRootRef","rootRef","channelRef","livekitComponentRef","waveformDataMap","setWaveformDataMap","agent","user","waveformConfigRef","isMicEnabled","setIsMicEnabled","useEffect","allMessages","Array","from","values","sort","a","b","timestamp","messagesString","JSON","stringify","current","onMessage","filter","msg","isFinal","id","has","forEach","add","channel","MessageChannel","port1","onmessage","e","data","type","handleTranscriptionUpdate","transcriptions","speaker","prevMap","waveformData","tool","functionCallsId","newMap","set","name","start","_channelRef$current","_channelRef$current2","close","port2","useCallback","t","_prevMap$get","messageType","existingTimestamp","get","message","text","div","document","createElement","style","display","body","appendChild","createRoot","unmount","removeChild","connect","_ref","agentId","apiKey","dynamicVariables","metadata","Promise","resolve","errorMessage","console","warn","onError","Error","reject","fetch","method","headers","Authorization","agent_id","runtime_context","source","version","call_web","dynamic_variables","then","response","_temp2","_result2","_exit","json","onConnect","_temp","ok","errorText","status","_catch","err","error","String","disconnect","onDisconnect","send","_ref2","digit","messageId","postMessage","undefined","audioWaveform","_ref3","barCount","updateInterval","config","speakerData","length","slice","fill","toggleMic","value","enabled","setVolume","volume","validVolume","Math","min","max","_jsxs","LiveKitRoom","serverUrl","token","participantToken","audio","video","onDisconnected","children","_jsx","RoomAudioRenderer","StateMonitor","port","initialConfig","render","_Fragment","_ref4","useVoiceAssistant","sendChat","useChat","waveformConfig","setWaveformConfig","agentAudioTrack","useParticipantTracks","Track","Source","Microphone","identity","agentTranscription","useTrackTranscription","agentAudioWaveform","useAudioWaveform","localParticipant","useLocalParticipant","localMessages","publication","microphoneTrack","participant","localAudioTrack","userAudioWaveform","bars","handleMessage","event","publishDtmf","setMicrophoneEnabled","catch","log","addEventListener","removeEventListener","segments","map","segment","final","useDataChannel","textDecoder","TextDecoder","messageString","payload","Uint8Array","decode","parse"],"mappings":"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"}
1
+ {"version":3,"file":"lib.module.js","sources":["../src/hooks/useVoxAI.tsx","../src/utils/constants.ts"],"sourcesContent":["import {\n LiveKitRoom,\n RoomAudioRenderer,\n useAudioWaveform,\n useChat,\n useDataChannel,\n useLocalParticipant,\n useParticipantTracks,\n useTrackTranscription,\n useVoiceAssistant,\n} from \"@livekit/components-react\";\nimport { Track } from \"livekit-client\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { createRoot, Root } from \"react-dom/client\";\nimport { HTTPS_API_ORIGIN, SDK_VERSION } from \"../utils/constants\";\n\ntype VoxConnectionDetail = {\n serverUrl: string;\n roomName: string;\n participantName: string;\n participantToken: string;\n};\n\n/**\n * 음성 AI 에이전트의 현재 상태를 나타냅니다.\n *\n * @remarks\n * 에이전트의 상태는 다음과 같이 변화합니다:\n * `disconnected` → `connecting` → `initializing` → `listening` ⇄ `thinking` ⇄ `speaking`\n *\n * @example\n * ```tsx\n * const { state } = useVoxAI();\n *\n * if (state === 'listening') {\n * console.log('에이전트가 사용자의 말을 듣고 있습니다');\n * }\n * ```\n */\nexport type VoxAgentState =\n /** 연결되지 않은 상태 */\n | \"disconnected\"\n /** Vox.ai 서버에 연결 중 */\n | \"connecting\"\n /** LiveKit 세션을 초기화하는 중 */\n | \"initializing\"\n /** 에이전트가 사용자의 음성을 듣고 있는 상태 */\n | \"listening\"\n /** 에이전트가 응답을 생각하고 있는 상태 */\n | \"thinking\"\n /** 에이전트가 응답을 말하고 있는 상태 */\n | \"speaking\";\n\n/**\n * 에이전트가 실행한 함수 도구들의 정보를 담고 있는 타입입니다.\n *\n * @remarks\n * 에이전트가 외부 API를 호출하거나 특정 작업을 수행할 때 이 타입의 데이터가 생성됩니다.\n */\nexport interface FunctionToolsExecuted {\n /** 이벤트 타입 */\n type: \"function_tools_executed\";\n /** 실행된 함수 호출 정보 배열 */\n function_calls: FunctionCallInfo[];\n /** 함수 호출 결과 배열 */\n function_call_outputs: FunctionCallResult[];\n}\n\n/**\n * 에이전트가 호출한 함수의 정보를 담고 있는 타입입니다.\n */\nexport interface FunctionCallInfo {\n /** 함수 호출 고유 ID */\n id: string;\n /** 함수 타입 */\n type: string;\n /** 함수 호출 ID */\n call_id: string;\n /** 함수에 전달된 인자들 */\n arguments: Record<string, any>;\n /** 호출된 함수의 이름 */\n name: string;\n}\n\n/**\n * 함수 호출의 결과를 담고 있는 타입입니다.\n */\nexport interface FunctionCallResult {\n /** 결과 고유 ID */\n id: string;\n /** 호출된 함수의 이름 */\n name: string;\n /** 결과 타입 */\n type: string;\n /** 함수 호출 ID */\n call_id: string;\n /** 함수 실행 결과 (문자열 형태) */\n output: string;\n /** 에러 발생 여부 */\n is_error: boolean;\n}\n\n/**\n * 에이전트와 사용자 간의 대화 메시지를 나타내는 타입입니다.\n *\n * @remarks\n * - `name`이 \"agent\"인 경우: AI 에이전트가 말한 내용\n * - `name`이 \"user\"인 경우: 사용자가 말한 내용 (음성 또는 텍스트)\n * - `name`이 \"tool\"인 경우: 에이전트가 실행한 함수 도구 정보\n *\n * @example\n * ```tsx\n * const { messages } = useVoxAI();\n *\n * messages.forEach(msg => {\n * if (msg.name === 'user' && msg.isFinal) {\n * console.log('사용자:', msg.message);\n * }\n * });\n * ```\n */\nexport type VoxMessage = {\n /** 메시지 고유 ID */\n id?: string;\n /** 메시지 발신자 타입 */\n name: \"agent\" | \"user\" | \"tool\";\n /** 메시지 내용 (음성 전사 텍스트 또는 사용자가 보낸 텍스트) */\n message?: string;\n /** 메시지 생성 시각 (Unix timestamp) */\n timestamp: number;\n /** 최종 확정된 메시지인지 여부 (false인 경우 음성 인식 중간 결과) */\n isFinal?: boolean;\n /** 함수 도구 실행 정보 (name이 \"tool\"인 경우에만 존재) */\n tool?: FunctionToolsExecuted;\n};\n\n/**\n * useVoxAI 훅의 콜백 함수들을 설정하는 옵션입니다.\n *\n * @example\n * ```tsx\n * const vox = useVoxAI({\n * onConnect: () => {\n * console.log('음성 AI에 연결되었습니다');\n * },\n * onDisconnect: () => {\n * console.log('연결이 종료되었습니다');\n * },\n * onError: (error) => {\n * console.error('오류 발생:', error.message);\n * },\n * onMessage: (message) => {\n * if (message.isFinal) {\n * console.log(`${message.name}: ${message.message}`);\n * }\n * }\n * });\n * ```\n */\nexport interface VoxAIOptions {\n /** 음성 AI 연결이 성공했을 때 호출되는 콜백 */\n onConnect?: () => void;\n /** 음성 AI 연결이 종료되었을 때 호출되는 콜백 */\n onDisconnect?: () => void;\n /** 오류가 발생했을 때 호출되는 콜백 */\n onError?: (error: Error) => void;\n /** 새로운 최종 메시지가 수신되었을 때 호출되는 콜백 (isFinal이 true인 메시지만 전달됨) */\n onMessage?: (message: VoxMessage) => void;\n}\n\n// Message channel event types\ntype MessageChannelEvent =\n | { type: \"state_update\"; state: VoxAgentState }\n | { type: \"transcription_update\"; transcriptions: TranscriptionSegment[] }\n | {\n type: \"waveform_update\";\n waveformData: number[];\n speaker: \"agent\" | \"user\";\n }\n | { type: \"function_tools_executed\"; tool: FunctionToolsExecuted };\n\ntype TranscriptionSegment = {\n id: string;\n text: string;\n isFinal: boolean;\n timestamp: number;\n speaker: \"agent\" | \"user\";\n};\n\n/**\n * Vox.ai 음성 AI에 연결하기 위한 매개변수입니다.\n *\n * @example\n * ```tsx\n * // 기본 연결 (current 버전)\n * connect({\n * agentId: 'my-agent-id',\n * apiKey: 'my-api-key'\n * });\n *\n * // 특정 버전으로 연결\n * connect({\n * agentId: 'my-agent-id',\n * agentVersion: 'v5',\n * apiKey: 'my-api-key'\n * });\n *\n * // 동적 변수와 함께 연결\n * connect({\n * agentId: 'my-agent-id',\n * apiKey: 'my-api-key',\n * dynamicVariables: {\n * userName: '홍길동',\n * userId: 'user123'\n * },\n * metadata: {\n * sessionId: 'sess_abc123'\n * }\n * });\n * ```\n */\nexport interface ConnectParams {\n /**\n * 연결할 에이전트의 ID\n * @remarks Vox.ai 대시보드에서 확인할 수 있습니다.\n */\n agentId: string;\n\n /**\n * 사용할 에이전트 버전\n * @remarks\n * - `'v1'`, `'v2'`, `'v12'` 등: 특정 버전 번호 (v + 숫자 형식)\n * - `'current'`: 현재 편집중인 버전 (기본값)\n * - `'production'`: 프로덕션으로 지정된 버전\n * - `undefined` 또는 미지정: 'current' 버전 사용\n */\n agentVersion?: string;\n\n /**\n * Vox.ai API 키\n * @remarks Vox.ai 대시보드에서 발급받을 수 있습니다.\n */\n apiKey: string;\n\n /**\n * 에이전트 대화에 전달할 동적 변수\n * @remarks\n * 에이전트 프롬프트에서 이 변수들을 참조하여 개인화된 대화를 만들 수 있습니다.\n * @example\n * ```tsx\n * dynamicVariables: {\n * userName: '홍길동',\n * userType: 'premium',\n * accountBalance: 50000\n * }\n * ```\n */\n dynamicVariables?: Record<string, any>;\n\n /**\n * 통화 메타데이터\n * @remarks\n * 이 메타데이터는 아웃바운드 웹훅과 통화 기록에 포함되어,\n * 외부 시스템과의 연동이나 분석에 활용할 수 있습니다.\n * @example\n * ```tsx\n * metadata: {\n * source: 'mobile-app',\n * campaignId: 'spring-2024',\n * customerId: 'cust_123'\n * }\n * ```\n */\n metadata?: Record<string, any>;\n}\n\n/**\n * Vox.ai 음성 AI를 React 애플리케이션에 통합하기 위한 훅입니다.\n *\n * @param options - 연결 이벤트에 대한 콜백 함수들을 설정하는 옵션 객체\n *\n * @returns 음성 AI를 제어하기 위한 메서드와 상태를 포함한 객체\n * - `connect`: Vox.ai 서버에 연결하는 함수\n * - `disconnect`: 연결을 종료하는 함수\n * - `state`: 에이전트의 현재 상태\n * - `messages`: 대화 메시지 배열\n * - `send`: 텍스트 메시지 또는 DTMF 숫자를 전송하는 함수\n * - `audioWaveform`: 실시간 오디오 파형 데이터를 가져오는 함수\n * - `toggleMic`: 마이크를 켜거나 끄는 함수\n * - `setVolume`: 에이전트 음성의 볼륨을 조절하는 함수\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const {\n * connect,\n * disconnect,\n * state,\n * messages,\n * send,\n * audioWaveform,\n * toggleMic,\n * setVolume\n * } = useVoxAI({\n * onConnect: () => console.log(\"연결됨\"),\n * onDisconnect: () => console.log(\"연결 종료\"),\n * onError: (error) => console.error(\"오류:\", error),\n * onMessage: (message) => console.log(\"새 메시지:\", message)\n * });\n *\n * const handleConnect = () => {\n * connect({\n * agentId: 'my-agent-id',\n * apiKey: 'my-api-key'\n * });\n * };\n *\n * return (\n * <div>\n * <button onClick={handleConnect}>연결</button>\n * <button onClick={disconnect}>연결 해제</button>\n * <p>상태: {state}</p>\n * </div>\n * );\n * }\n * ```\n */\nexport function useVoxAI(options: VoxAIOptions = {}) {\n // Connection state\n const [connectionDetail, setConnectionDetail] =\n useState<VoxConnectionDetail | null>(null);\n const [state, setState] = useState<VoxAgentState>(\"disconnected\");\n\n // Session timestamp to filter out stale asynchronous events\n const sessionTimestampRef = useRef<number>(Date.now());\n\n // Message handling\n const [transcriptMap, setTranscriptMap] = useState<Map<string, VoxMessage>>(\n new Map()\n );\n const [messages, setMessages] = useState<VoxMessage[]>([]);\n const prevMessagesRef = useRef<string>(\"\");\n\n // Track which messages we've already sent to the onMessage callback\n const processedMessageIdsRef = useRef<Set<string>>(new Set());\n\n // DOM manipulation for LiveKit portal\n const portalRootRef = useRef<HTMLDivElement | null>(null);\n const rootRef = useRef<Root | null>(null);\n\n // Communication channel\n const channelRef = useRef<MessageChannel | null>(null);\n\n // Add this near the start of your useVoxAI hook\n const livekitComponentRef = useRef<React.ReactNode>(null);\n\n // Replace the single waveform state with a map for multiple speakers\n const [waveformDataMap, setWaveformDataMap] = useState<\n Record<string, number[]>\n >({\n agent: [],\n user: [],\n });\n\n // Add back the waveform config reference\n const waveformConfigRef = useRef<{\n speaker?: \"agent\" | \"user\";\n barCount: number;\n updateInterval: number;\n } | null>(null);\n\n // Add a new state to track microphone status\n const [isMicEnabled, setIsMicEnabled] = useState<boolean>(true);\n\n // Update messages whenever transcriptMap changes\n useEffect(() => {\n const allMessages = Array.from(transcriptMap.values()).sort(\n (a, b) => a.timestamp - b.timestamp\n );\n\n // Only update if the messages have actually changed\n const messagesString = JSON.stringify(allMessages);\n if (messagesString !== prevMessagesRef.current) {\n prevMessagesRef.current = messagesString;\n setMessages(allMessages);\n\n // Only trigger onMessage for new final messages that haven't been processed yet\n if (options.onMessage) {\n allMessages\n .filter(\n (msg) =>\n msg.isFinal &&\n msg.id &&\n !processedMessageIdsRef.current.has(msg.id)\n )\n .forEach((msg) => {\n if (msg.id) {\n // Mark this message as processed\n processedMessageIdsRef.current.add(msg.id);\n // Call the callback\n options.onMessage?.(msg);\n }\n });\n }\n }\n }, [transcriptMap, options.onMessage]);\n\n // Initialize message channel - ensure ports are properly connected\n useEffect(() => {\n const channel = new MessageChannel();\n\n channel.port1.onmessage = (e) => {\n const data = e.data as MessageChannelEvent;\n\n if (data.type === \"state_update\") {\n setState(data.state);\n } else if (data.type === \"transcription_update\") {\n handleTranscriptionUpdate(data.transcriptions);\n } else if (data.type === \"waveform_update\" && data.speaker) {\n // Store the waveform data for the specific speaker\n setWaveformDataMap((prevMap) => ({\n ...prevMap,\n [data.speaker]: data.waveformData,\n }));\n } else if (data.type === \"function_tools_executed\" && data.tool) {\n // Handle function calls\n const functionCallsId = `function-calls-${Date.now()}`;\n setTranscriptMap((prevMap) => {\n const newMap = new Map(prevMap);\n newMap.set(functionCallsId, {\n id: functionCallsId,\n name: \"tool\",\n tool: data.tool,\n timestamp: Date.now(),\n isFinal: true,\n });\n return newMap;\n });\n }\n };\n\n // Start the port\n channel.port1.start();\n\n // Store the channel reference\n channelRef.current = channel;\n\n return () => {\n channelRef.current?.port1.close();\n channelRef.current?.port2.close();\n channelRef.current = null;\n };\n }, []);\n\n // Process incoming transcriptions and filter out stale events\n const handleTranscriptionUpdate = useCallback(\n (transcriptions: TranscriptionSegment[]) => {\n setTranscriptMap((prevMap) => {\n const newMap = new Map(prevMap);\n\n transcriptions.forEach((t) => {\n // Only process transcriptions generated after the current session timestamp\n if (t.timestamp < sessionTimestampRef.current) {\n return;\n }\n const messageType = t.speaker === \"agent\" ? \"agent\" : \"user\";\n // Use existing timestamp if we already have this segment\n const existingTimestamp = prevMap.get(t.id)?.timestamp || t.timestamp;\n\n newMap.set(t.id, {\n id: t.id,\n name: messageType,\n message: t.text,\n timestamp: existingTimestamp,\n isFinal: t.isFinal,\n });\n });\n\n return newMap;\n });\n },\n []\n );\n\n // Set up DOM portal for LiveKit\n useEffect(() => {\n const div = document.createElement(\"div\");\n div.style.display = \"none\";\n document.body.appendChild(div);\n portalRootRef.current = div;\n rootRef.current = createRoot(div);\n\n return () => {\n if (rootRef.current) {\n rootRef.current.unmount();\n }\n if (portalRootRef.current) {\n document.body.removeChild(portalRootRef.current);\n }\n };\n }, []);\n\n /**\n * Vox.ai 음성 AI 서버에 연결합니다.\n *\n * @param params - 연결에 필요한 매개변수 ({@link ConnectParams} 참조)\n *\n * @remarks\n * - 이미 연결된 상태에서 호출하면 오류가 발생합니다.\n * - 연결에 성공하면 `onConnect` 콜백이 호출됩니다.\n * - 연결에 실패하면 `onError` 콜백이 호출됩니다.\n * - 연결 성공 후 상태가 `connecting` → `initializing` → `listening`으로 변화합니다.\n *\n * @throws {Error} 이미 연결된 상태이거나 인증에 실패한 경우\n *\n * @example\n * ```tsx\n * const { connect } = useVoxAI();\n *\n * // 기본 연결\n * await connect({\n * agentId: 'agent_abc123',\n * apiKey: 'key_xyz789'\n * });\n *\n * // 특정 버전과 동적 변수로 연결\n * await connect({\n * agentId: 'agent_abc123',\n * agentVersion: 'v5',\n * apiKey: 'key_xyz789',\n * dynamicVariables: {\n * userName: '홍길동',\n * userId: 'user_123'\n * }\n * });\n * ```\n */\n const connect = useCallback(\n async ({\n agentId,\n agentVersion,\n apiKey,\n dynamicVariables,\n metadata,\n }: ConnectParams) => {\n try {\n // Prevent connecting if already in a connection state\n if (state !== \"disconnected\") {\n const errorMessage = `Connection attempt rejected: Already in a connection state (${state})`;\n console.warn(errorMessage);\n\n if (options.onError) {\n options.onError(new Error(errorMessage));\n }\n return Promise.reject(new Error(errorMessage));\n }\n\n // Update session timestamp for new connection\n sessionTimestampRef.current = Date.now();\n setState(\"connecting\");\n\n const requestBody: any = {\n agent_id: agentId,\n agent_version: agentVersion || \"current\",\n metadata: {\n runtime_context: {\n source: {\n type: \"react-sdk\",\n version: SDK_VERSION,\n },\n },\n call_web: {\n dynamic_variables: dynamicVariables || {},\n metadata: metadata || {},\n },\n },\n };\n\n const response = await fetch(HTTPS_API_ORIGIN, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(requestBody),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Connection failed (${response.status}): ${errorText}`\n );\n }\n\n const data = await response.json();\n setConnectionDetail(data);\n\n if (options.onConnect) {\n options.onConnect();\n }\n } catch (err) {\n setConnectionDetail(null);\n setTranscriptMap(new Map());\n setMessages([]);\n setState(\"disconnected\");\n\n const error = err instanceof Error ? err : new Error(String(err));\n\n if (options.onError) {\n options.onError(error);\n }\n }\n },\n [options, state]\n );\n\n /**\n * 음성 AI 연결을 종료합니다.\n *\n * @remarks\n * - 연결이 종료되면 `onDisconnect` 콜백이 호출됩니다.\n * - 모든 메시지와 상태가 초기화됩니다.\n * - 상태가 `disconnected`로 변경됩니다.\n * - 연결되지 않은 상태에서 호출해도 안전합니다.\n *\n * @example\n * ```tsx\n * const { disconnect } = useVoxAI();\n *\n * // 연결 종료\n * disconnect();\n * ```\n */\n const disconnect = useCallback(() => {\n // Update session timestamp on disconnect\n sessionTimestampRef.current = Date.now();\n setConnectionDetail(null);\n setTranscriptMap(new Map());\n setMessages([]);\n setState(\"disconnected\");\n\n if (options.onDisconnect) {\n options.onDisconnect();\n }\n }, [options]);\n\n /**\n * 에이전트에게 텍스트 메시지를 전송하거나 DTMF 숫자를 입력합니다.\n *\n * @param params - 전송할 메시지 또는 DTMF 숫자\n * @param params.message - 전송할 텍스트 메시지 (음성 대신 텍스트로 입력)\n * @param params.digit - 전송할 DTMF 숫자 (0-9, *, #에 해당하는 숫자)\n *\n * @remarks\n * - 연결되지 않은 상태에서 호출하면 경고 메시지가 출력되고 무시됩니다.\n * - `message`와 `digit`을 동시에 전달할 수 있습니다.\n * - 텍스트 메시지는 음성 입력 대신 사용할 수 있습니다.\n * - DTMF는 전화번호 입력 등에 활용됩니다.\n *\n * @example\n * ```tsx\n * const { send } = useVoxAI();\n *\n * // 텍스트 메시지 전송\n * send({ message: '안녕하세요' });\n *\n * // DTMF 숫자 전송\n * send({ digit: 1 });\n *\n * // 둘 다 전송\n * send({ message: '1번을 선택합니다', digit: 1 });\n * ```\n */\n const send = useCallback(\n ({ message, digit }: { message?: string; digit?: number }) => {\n if (state === \"disconnected\") {\n console.warn(\"Cannot send message: Not connected to a conversation\");\n return;\n }\n\n if (message) {\n const messageId = `user-text-${Date.now()}`;\n setTranscriptMap((prevMap) => {\n const newMap = new Map(prevMap);\n newMap.set(messageId, {\n id: messageId,\n name: \"user\",\n message: message,\n timestamp: Date.now(),\n isFinal: true,\n });\n return newMap;\n });\n\n if (channelRef.current) {\n channelRef.current.port1.postMessage({\n type: \"send_text\",\n text: message,\n });\n } else {\n console.error(\"No message channel available to send message\");\n }\n }\n\n if (digit !== undefined) {\n if (channelRef.current) {\n channelRef.current.port1.postMessage({\n type: \"send_dtmf\",\n digit: digit,\n });\n } else {\n console.error(\"No message channel available to send DTMF\");\n }\n }\n },\n [state]\n );\n\n /**\n * 실시간 오디오 파형 데이터를 가져옵니다.\n *\n * @param params - 파형 설정 옵션\n * @param params.speaker - 파형을 가져올 대상 (`\"agent\"` 또는 `\"user\"`, 기본값: `\"agent\"`)\n * @param params.barCount - 반환할 파형 막대 개수 (기본값: 10)\n * @param params.updateInterval - 파형 업데이트 간격 (밀리초, 기본값: 20)\n *\n * @returns 0~1 사이의 값을 가진 숫자 배열 (길이는 `barCount`와 동일)\n *\n * @remarks\n * - 각 값은 해당 주파수 대역의 음량을 나타냅니다 (0: 무음, 1: 최대 음량).\n * - 음성 시각화 UI를 만들 때 유용합니다.\n * - 연결되지 않은 상태에서는 모두 0으로 채워진 배열을 반환합니다.\n *\n * @example\n * ```tsx\n * const { audioWaveform, state } = useVoxAI();\n *\n * // 렌더링 루프에서 사용\n * useEffect(() => {\n * const interval = setInterval(() => {\n * // 에이전트의 파형 데이터 (20개 막대)\n * const agentWaveform = audioWaveform({\n * speaker: 'agent',\n * barCount: 20\n * });\n *\n * // 사용자의 파형 데이터\n * const userWaveform = audioWaveform({\n * speaker: 'user',\n * barCount: 20\n * });\n *\n * // 시각화 업데이트\n * updateVisualization(agentWaveform, userWaveform);\n * }, 50);\n *\n * return () => clearInterval(interval);\n * }, []);\n * ```\n */\n const audioWaveform = useCallback(\n ({\n speaker = \"agent\",\n barCount = 10,\n updateInterval = 20,\n }: {\n speaker?: \"agent\" | \"user\";\n barCount?: number;\n updateInterval?: number;\n }): number[] => {\n waveformConfigRef.current = { speaker, barCount, updateInterval };\n\n if (channelRef.current) {\n channelRef.current.port1.postMessage({\n type: \"waveform_config\",\n config: { speaker, barCount, updateInterval },\n });\n }\n\n const speakerData = waveformDataMap[speaker] || [];\n return speakerData.length > 0\n ? speakerData.slice(0, barCount)\n : Array(barCount).fill(0);\n },\n [waveformDataMap]\n );\n\n /**\n * 사용자의 마이크를 켜거나 끕니다.\n *\n * @param value - `true`면 마이크 켜기, `false`면 마이크 끄기\n *\n * @remarks\n * - 마이크를 끄면 에이전트가 사용자의 음성을 듣지 못합니다.\n * - 음성 인식도 중단됩니다.\n * - 프라이버시나 소음 차단이 필요할 때 유용합니다.\n *\n * @example\n * ```tsx\n * const { toggleMic } = useVoxAI();\n *\n * // 마이크 끄기\n * toggleMic(false);\n *\n * // 마이크 켜기\n * toggleMic(true);\n *\n * // 토글 버튼 예제\n * const [isMuted, setIsMuted] = useState(false);\n * const handleToggle = () => {\n * setIsMuted(!isMuted);\n * toggleMic(!isMuted);\n * };\n * ```\n */\n const toggleMic = useCallback((value: boolean) => {\n setIsMicEnabled(value);\n if (channelRef.current) {\n channelRef.current.port1.postMessage({\n type: \"toggle_mic\",\n enabled: value,\n });\n } else {\n console.error(\"No message channel available to toggle microphone\");\n }\n }, []);\n\n /**\n * 에이전트 음성의 볼륨을 설정합니다.\n *\n * @param volume - 볼륨 크기 (0.0 ~ 1.0 사이의 값, 0: 무음, 1: 최대 음량)\n *\n * @remarks\n * - 범위를 벗어난 값은 자동으로 0~1 사이로 조정됩니다.\n * - 예: `-0.5` → `0`, `1.5` → `1`\n * - 사용자의 환경에 따라 적절한 볼륨을 설정할 수 있습니다.\n *\n * @example\n * ```tsx\n * const { setVolume } = useVoxAI();\n *\n * // 볼륨을 50%로 설정\n * setVolume(0.5);\n *\n * // 볼륨을 최대로 설정\n * setVolume(1.0);\n *\n * // 볼륨 슬라이더 예제\n * const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n * const newVolume = parseFloat(e.target.value);\n * setVolume(newVolume);\n * };\n *\n * <input\n * type=\"range\"\n * min=\"0\"\n * max=\"1\"\n * step=\"0.1\"\n * onChange={handleVolumeChange}\n * />\n * ```\n */\n const setVolume = useCallback((volume: number) => {\n const validVolume = Math.min(Math.max(volume, 0), 1);\n if (channelRef.current) {\n channelRef.current.port1.postMessage({\n type: \"set_volume\",\n volume: validVolume,\n });\n } else {\n console.error(\"No message channel available to set volume\");\n }\n }, []);\n\n // Modify the useEffect hook that renders the LiveKit component\n useEffect(() => {\n if (!rootRef.current) return;\n\n if (connectionDetail) {\n if (!livekitComponentRef.current) {\n if (channelRef.current) {\n channelRef.current.port2.start();\n }\n\n livekitComponentRef.current = (\n <LiveKitRoom\n serverUrl={connectionDetail.serverUrl}\n token={connectionDetail.participantToken}\n audio={true}\n video={false}\n connect={true}\n onDisconnected={disconnect}\n onError={(error) => {\n console.error(\"LiveKit connection error:\", error);\n disconnect();\n if (options.onError) {\n options.onError(\n new Error(`LiveKit connection error: ${error.message}`)\n );\n }\n }}\n >\n <RoomAudioRenderer />\n {channelRef.current && (\n <StateMonitor\n port={channelRef.current.port2}\n initialConfig={\n waveformConfigRef.current || {\n barCount: 10,\n updateInterval: 20,\n }\n }\n />\n )}\n </LiveKitRoom>\n );\n }\n rootRef.current.render(livekitComponentRef.current);\n } else {\n livekitComponentRef.current = null;\n rootRef.current.render(<></>);\n }\n }, [connectionDetail, disconnect, options.onError]);\n\n return {\n connect,\n disconnect,\n state,\n messages,\n send,\n audioWaveform,\n toggleMic,\n setVolume,\n };\n}\n\n/**\n * Component that monitors LiveKit state and communicates back to the main hook\n */\nfunction StateMonitor({\n port,\n initialConfig,\n}: {\n port: MessagePort | undefined;\n initialConfig: {\n speaker?: \"agent\" | \"user\";\n barCount: number;\n updateInterval: number;\n };\n}) {\n const { agent, state } = useVoiceAssistant();\n const { send: sendChat } = useChat();\n\n // Initialize waveform config with the passed initial values, defaulting to \"agent\" if not specified\n const [waveformConfig, setWaveformConfig] = useState({\n speaker: initialConfig.speaker || \"agent\",\n barCount: initialConfig.barCount,\n updateInterval: initialConfig.updateInterval,\n });\n\n // Agent transcriptions\n const agentAudioTrack = useParticipantTracks(\n [Track.Source.Microphone],\n agent?.identity\n )[0];\n const agentTranscription = useTrackTranscription(agentAudioTrack);\n\n // Use the current config for the waveform, applying different settings based on speaker\n const agentAudioWaveform = useAudioWaveform(agentAudioTrack, {\n barCount:\n waveformConfig.speaker === \"agent\" ? waveformConfig.barCount : 120, // default if not the selected speaker\n updateInterval:\n waveformConfig.speaker === \"agent\" ? waveformConfig.updateInterval : 20,\n });\n\n // User transcriptions\n const localParticipant = useLocalParticipant();\n const localMessages = useTrackTranscription({\n publication: localParticipant.microphoneTrack,\n source: Track.Source.Microphone,\n participant: localParticipant.localParticipant,\n });\n const localAudioTrack = useParticipantTracks(\n [Track.Source.Microphone],\n localParticipant.localParticipant.identity\n )[0];\n const userAudioWaveform = useAudioWaveform(localAudioTrack, {\n barCount: waveformConfig.speaker === \"user\" ? waveformConfig.barCount : 120, // default if not the selected speaker\n updateInterval:\n waveformConfig.speaker === \"user\" ? waveformConfig.updateInterval : 20,\n });\n\n // Add separate effects to send agent and user waveform data\n useEffect(() => {\n if (!port || !agentAudioWaveform || !agentAudioWaveform.bars) return;\n\n // Send the agent waveform data\n port.postMessage({\n type: \"waveform_update\",\n waveformData: agentAudioWaveform.bars,\n speaker: \"agent\",\n });\n }, [port, agentAudioWaveform]);\n\n useEffect(() => {\n if (!port || !userAudioWaveform || !userAudioWaveform.bars) return;\n\n // Send the user waveform data\n port.postMessage({\n type: \"waveform_update\",\n waveformData: userAudioWaveform.bars,\n speaker: \"user\",\n });\n }, [port, userAudioWaveform]);\n\n // Listen for messages including config updates and mic toggle commands\n useEffect(() => {\n if (!port) return;\n\n const handleMessage = (event: MessageEvent) => {\n const data = event.data;\n\n if (data.type === \"waveform_config\" && data.config) {\n // Verify we have both required properties before updating\n if (\n typeof data.config.barCount === \"number\" &&\n typeof data.config.updateInterval === \"number\"\n ) {\n setWaveformConfig(data.config);\n }\n } else if (data.type === \"send_text\") {\n if (sendChat) {\n sendChat(data.text);\n } else {\n console.error(\"sendChat function is not available\");\n }\n } else if (data.type === \"send_dtmf\") {\n if (localParticipant.localParticipant) {\n // Use standard DTMF code (RFC 4733)\n const standardDtmfCode = 101; // Standard DTMF payload type\n localParticipant.localParticipant.publishDtmf(\n standardDtmfCode,\n data.digit\n );\n } else {\n console.error(\"Local participant is not available for DTMF\");\n }\n } else if (\n data.type === \"toggle_mic\" &&\n typeof data.enabled === \"boolean\"\n ) {\n // Handle microphone toggle\n if (localParticipant.localParticipant) {\n localParticipant.localParticipant\n .setMicrophoneEnabled(data.enabled)\n .catch((error) => {\n console.error(\"Failed to toggle microphone:\", error);\n });\n } else {\n console.error(\"Local participant is not available for mic toggle\");\n }\n } else if (\n data.type === \"set_volume\" &&\n typeof data.volume === \"number\"\n ) {\n // Handle volume control\n if (agent) {\n // The agent is a RemoteParticipant, so we can call setVolume directly\n try {\n agent.setVolume(data.volume);\n console.log(`Set agent volume to ${data.volume}`);\n } catch (error) {\n console.error(\"Failed to set agent volume:\", error);\n }\n } else {\n console.error(\"Agent is not available for volume control\");\n }\n }\n };\n\n // Make sure we start the port\n port.start();\n\n port.addEventListener(\"message\", handleMessage);\n\n return () => {\n port.removeEventListener(\"message\", handleMessage);\n };\n }, [port, sendChat, localParticipant, agent]);\n\n // Send agent state updates\n useEffect(() => {\n if (port) {\n port.postMessage({ type: \"state_update\", state });\n }\n }, [state, port]);\n\n // Send agent transcriptions\n useEffect(() => {\n if (port && agentTranscription.segments.length > 0) {\n const transcriptions = agentTranscription.segments.map((segment) => ({\n id: segment.id,\n text: segment.text,\n isFinal: segment.final,\n timestamp: Date.now(),\n speaker: \"agent\" as const,\n }));\n\n port.postMessage({\n type: \"transcription_update\",\n transcriptions,\n });\n }\n }, [agentTranscription.segments, port]);\n\n // Send user transcriptions\n useEffect(() => {\n if (port && localMessages.segments.length > 0) {\n const transcriptions = localMessages.segments.map((segment) => ({\n id: segment.id,\n text: segment.text,\n isFinal: segment.final,\n timestamp: Date.now(),\n speaker: \"user\" as const,\n }));\n\n port.postMessage({\n type: \"transcription_update\",\n transcriptions,\n });\n }\n }, [localMessages.segments, port]);\n\n // Add data channel hook for function calls\n const { message: functionToolsExecuted } = useDataChannel(\n \"function_tools_executed\",\n (msg) => {\n if (!port) return;\n\n const textDecoder = new TextDecoder();\n const messageString =\n msg.payload instanceof Uint8Array\n ? textDecoder.decode(msg.payload)\n : String(msg.payload);\n\n let tool: FunctionToolsExecuted;\n try {\n tool = JSON.parse(messageString);\n\n // Send function calls to main hook via the port\n port.postMessage({\n type: \"function_tools_executed\",\n tool: tool,\n });\n } catch (e) {\n console.error(\"Failed to parse function call log:\", e);\n }\n }\n );\n\n return null;\n}\n","// export const HTTPS_API_ORIGIN = \"https://frontend-dev.tryvox.co/api/agent/sdk\";\nexport const HTTPS_API_ORIGIN = \"https://www.tryvox.co/api/agent/sdk\";\n// export const HTTPS_API_ORIGIN = \"http://localhost:3000/api/agent/sdk\";\nexport const SDK_VERSION = \"0.5.0\";\n"],"names":["useVoxAI","options","connectionDetail","setConnectionDetail","useState","state","setState","sessionTimestampRef","useRef","Date","now","transcriptMap","setTranscriptMap","Map","messages","setMessages","prevMessagesRef","processedMessageIdsRef","Set","portalRootRef","rootRef","channelRef","livekitComponentRef","waveformDataMap","setWaveformDataMap","agent","user","waveformConfigRef","isMicEnabled","setIsMicEnabled","useEffect","allMessages","Array","from","values","sort","a","b","timestamp","messagesString","JSON","stringify","current","onMessage","filter","msg","isFinal","id","has","forEach","add","channel","MessageChannel","port1","onmessage","e","data","type","handleTranscriptionUpdate","transcriptions","speaker","prevMap","waveformData","tool","functionCallsId","newMap","set","name","start","_channelRef$current","_channelRef$current2","close","port2","useCallback","t","_prevMap$get","messageType","existingTimestamp","get","message","text","div","document","createElement","style","display","body","appendChild","createRoot","unmount","removeChild","connect","_ref","agentId","agentVersion","apiKey","dynamicVariables","metadata","Promise","resolve","recover","result","errorMessage","console","warn","onError","Error","reject","fetch","method","headers","Authorization","agent_id","agent_version","runtime_context","source","version","call_web","dynamic_variables","then","response","_temp2","_result2","json","onConnect","_temp","ok","errorText","status","_catch","err","error","String","disconnect","onDisconnect","send","_ref2","digit","messageId","postMessage","undefined","audioWaveform","_ref3","barCount","updateInterval","config","speakerData","length","slice","fill","toggleMic","value","enabled","setVolume","volume","validVolume","Math","min","max","_jsxs","LiveKitRoom","serverUrl","token","participantToken","audio","video","onDisconnected","children","_jsx","RoomAudioRenderer","StateMonitor","port","initialConfig","render","_Fragment","_ref4","useVoiceAssistant","sendChat","useChat","waveformConfig","setWaveformConfig","agentAudioTrack","useParticipantTracks","Track","Source","Microphone","identity","agentTranscription","useTrackTranscription","agentAudioWaveform","useAudioWaveform","localParticipant","useLocalParticipant","localMessages","publication","microphoneTrack","participant","localAudioTrack","userAudioWaveform","bars","handleMessage","event","publishDtmf","setMicrophoneEnabled","catch","log","addEventListener","removeEventListener","segments","map","segment","final","useDataChannel","textDecoder","TextDecoder","messageString","payload","Uint8Array","decode","parse"],"mappings":"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"}
package/dist/lib.umd.js CHANGED
@@ -1,2 +1,2 @@
1
- !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("react/jsx-runtime"),require("@livekit/components-react"),require("livekit-client"),require("react"),require("react-dom/client")):"function"==typeof define&&define.amd?define(["exports","react/jsx-runtime","@livekit/components-react","livekit-client","react","react-dom/client"],t):t((e||self).react={},e.jsxRuntime,e.componentsReact,e.livekitClient,e.react,e.client)}(this,function(e,t,n,r,o,a){function s(e){let{port:t,initialConfig:a}=e;const{agent:s,state:i}=n.useVoiceAssistant(),{send:c}=n.useChat(),[u,l]=o.useState({speaker:a.speaker||"agent",barCount:a.barCount,updateInterval:a.updateInterval}),p=n.useParticipantTracks([r.Track.Source.Microphone],null==s?void 0:s.identity)[0],d=n.useTrackTranscription(p),f=n.useAudioWaveform(p,{barCount:"agent"===u.speaker?u.barCount:120,updateInterval:"agent"===u.speaker?u.updateInterval:20}),m=n.useLocalParticipant(),g=n.useTrackTranscription({publication:m.microphoneTrack,source:r.Track.Source.Microphone,participant:m.localParticipant}),v=n.useParticipantTracks([r.Track.Source.Microphone],m.localParticipant.identity)[0],y=n.useAudioWaveform(v,{barCount:"user"===u.speaker?u.barCount:120,updateInterval:"user"===u.speaker?u.updateInterval:20});return o.useEffect(()=>{t&&f&&f.bars&&t.postMessage({type:"waveform_update",waveformData:f.bars,speaker:"agent"})},[t,f]),o.useEffect(()=>{t&&y&&y.bars&&t.postMessage({type:"waveform_update",waveformData:y.bars,speaker:"user"})},[t,y]),o.useEffect(()=>{if(!t)return;const e=e=>{const t=e.data;if("waveform_config"===t.type&&t.config)"number"==typeof t.config.barCount&&"number"==typeof t.config.updateInterval&&l(t.config);else if("send_text"===t.type)c?c(t.text):console.error("sendChat function is not available");else if("send_dtmf"===t.type)m.localParticipant?m.localParticipant.publishDtmf(101,t.digit):console.error("Local participant is not available for DTMF");else if("toggle_mic"===t.type&&"boolean"==typeof t.enabled)m.localParticipant?m.localParticipant.setMicrophoneEnabled(t.enabled).catch(e=>{console.error("Failed to toggle microphone:",e)}):console.error("Local participant is not available for mic toggle");else if("set_volume"===t.type&&"number"==typeof t.volume)if(s)try{s.setVolume(t.volume),console.log("Set agent volume to "+t.volume)}catch(e){console.error("Failed to set agent volume:",e)}else console.error("Agent is not available for volume control")};return t.start(),t.addEventListener("message",e),()=>{t.removeEventListener("message",e)}},[t,c,m,s]),o.useEffect(()=>{t&&t.postMessage({type:"state_update",state:i})},[i,t]),o.useEffect(()=>{if(t&&d.segments.length>0){const e=d.segments.map(e=>({id:e.id,text:e.text,isFinal:e.final,timestamp:Date.now(),speaker:"agent"}));t.postMessage({type:"transcription_update",transcriptions:e})}},[d.segments,t]),o.useEffect(()=>{if(t&&g.segments.length>0){const e=g.segments.map(e=>({id:e.id,text:e.text,isFinal:e.final,timestamp:Date.now(),speaker:"user"}));t.postMessage({type:"transcription_update",transcriptions:e})}},[g.segments,t]),n.useDataChannel("function_tools_executed",e=>{if(!t)return;const n=new TextDecoder,r=e.payload instanceof Uint8Array?n.decode(e.payload):String(e.payload);let o;try{o=JSON.parse(r),t.postMessage({type:"function_tools_executed",tool:o})}catch(e){console.error("Failed to parse function call log:",e)}}),null}e.useVoxAI=function(e){void 0===e&&(e={});const[r,i]=o.useState(null),[c,u]=o.useState("disconnected"),l=o.useRef(Date.now()),[p,d]=o.useState(new Map),[f,m]=o.useState([]),g=o.useRef(""),v=o.useRef(new Set),y=o.useRef(null),b=o.useRef(null),h=o.useRef(null),k=o.useRef(null),[w,C]=o.useState({agent:[],user:[]}),M=o.useRef(null),[E,x]=o.useState(!0);o.useEffect(()=>{const t=Array.from(p.values()).sort((e,t)=>e.timestamp-t.timestamp),n=JSON.stringify(t);n!==g.current&&(g.current=n,m(t),e.onMessage&&t.filter(e=>e.isFinal&&e.id&&!v.current.has(e.id)).forEach(t=>{t.id&&(v.current.add(t.id),null==e.onMessage||e.onMessage(t))}))},[p,e.onMessage]),o.useEffect(()=>{const e=new MessageChannel;return e.port1.onmessage=e=>{const t=e.data;if("state_update"===t.type)u(t.state);else if("transcription_update"===t.type)_(t.transcriptions);else if("waveform_update"===t.type&&t.speaker)C(e=>({...e,[t.speaker]:t.waveformData}));else if("function_tools_executed"===t.type&&t.tool){const e="function-calls-"+Date.now();d(n=>{const r=new Map(n);return r.set(e,{id:e,name:"tool",tool:t.tool,timestamp:Date.now(),isFinal:!0}),r})}},e.port1.start(),h.current=e,()=>{var e,t;null==(e=h.current)||e.port1.close(),null==(t=h.current)||t.port2.close(),h.current=null}},[]);const _=o.useCallback(e=>{d(t=>{const n=new Map(t);return e.forEach(e=>{var r;if(e.timestamp<l.current)return;const o="agent"===e.speaker?"agent":"user",a=(null==(r=t.get(e.id))?void 0:r.timestamp)||e.timestamp;n.set(e.id,{id:e.id,name:o,message:e.text,timestamp:a,isFinal:e.isFinal})}),n})},[]);o.useEffect(()=>{const e=document.createElement("div");return e.style.display="none",document.body.appendChild(e),y.current=e,b.current=a.createRoot(e),()=>{b.current&&b.current.unmount(),y.current&&document.body.removeChild(y.current)}},[]);const D=o.useCallback(function(t){let{agentId:n,apiKey:r,dynamicVariables:o,metadata:a}=t;try{return Promise.resolve(function(t,s){try{var p=function(){if("disconnected"!==c){const t="Connection attempt rejected: Already in a connection state ("+c+")";return console.warn(t),e.onError&&e.onError(new Error(t)),Promise.reject(new Error(t))}return l.current=Date.now(),u("connecting"),Promise.resolve(fetch("https://www.tryvox.co/api/agent/sdk",{method:"POST",headers:{Authorization:"Bearer "+r,"Content-Type":"application/json"},body:JSON.stringify({agent_id:n,metadata:{runtime_context:{source:{type:"react-sdk",version:"0.3.0"}},call_web:{dynamic_variables:o||{},metadata:a||{}}}})})).then(function(t){function n(n){return Promise.resolve(t.json()).then(function(t){i(t),e.onConnect&&e.onConnect()})}const r=function(){if(!t.ok)return Promise.resolve(t.text()).then(function(e){throw new Error("Connection failed ("+t.status+"): "+e)})}();return r&&r.then?r.then(n):n()})}()}catch(e){return s(e)}return p&&p.then?p.then(void 0,s):p}(0,function(t){i(null),d(new Map),m([]),u("disconnected");const n=t instanceof Error?t:new Error(String(t));e.onError&&e.onError(n)}))}catch(e){return Promise.reject(e)}},[e,c]),S=o.useCallback(()=>{l.current=Date.now(),i(null),d(new Map),m([]),u("disconnected"),e.onDisconnect&&e.onDisconnect()},[e]),T=o.useCallback(e=>{let{message:t,digit:n}=e;if("disconnected"!==c){if(t){const e="user-text-"+Date.now();d(n=>{const r=new Map(n);return r.set(e,{id:e,name:"user",message:t,timestamp:Date.now(),isFinal:!0}),r}),h.current?h.current.port1.postMessage({type:"send_text",text:t}):console.error("No message channel available to send message")}void 0!==n&&(h.current?h.current.port1.postMessage({type:"send_dtmf",digit:n}):console.error("No message channel available to send DTMF"))}else console.warn("Cannot send message: Not connected to a conversation")},[c]),P=o.useCallback(e=>{let{speaker:t="agent",barCount:n=10,updateInterval:r=20}=e;M.current={speaker:t,barCount:n,updateInterval:r},h.current&&h.current.port1.postMessage({type:"waveform_config",config:{speaker:t,barCount:n,updateInterval:r}});const o=w[t]||[];return o.length>0?o.slice(0,n):Array(n).fill(0)},[w]),R=o.useCallback(e=>{x(e),h.current?h.current.port1.postMessage({type:"toggle_mic",enabled:e}):console.error("No message channel available to toggle microphone")},[]),j=o.useCallback(e=>{const t=Math.min(Math.max(e,0),1);h.current?h.current.port1.postMessage({type:"set_volume",volume:t}):console.error("No message channel available to set volume")},[]);return o.useEffect(()=>{b.current&&(r?(k.current||(h.current&&h.current.port2.start(),k.current=t.jsxs(n.LiveKitRoom,{serverUrl:r.serverUrl,token:r.participantToken,audio:!0,video:!1,connect:!0,onDisconnected:S,onError:t=>{console.error("LiveKit connection error:",t),S(),e.onError&&e.onError(new Error("LiveKit connection error: "+t.message))},children:[t.jsx(n.RoomAudioRenderer,{}),h.current&&t.jsx(s,{port:h.current.port2,initialConfig:M.current||{barCount:10,updateInterval:20}})]})),b.current.render(k.current)):(k.current=null,b.current.render(t.jsx(t.Fragment,{}))))},[r,S,e.onError]),{connect:D,disconnect:S,state:c,messages:f,send:T,audioWaveform:P,toggleMic:R,setVolume:j}}});
1
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("react/jsx-runtime"),require("@livekit/components-react"),require("livekit-client"),require("react"),require("react-dom/client")):"function"==typeof define&&define.amd?define(["exports","react/jsx-runtime","@livekit/components-react","livekit-client","react","react-dom/client"],t):t((e||self).react={},e.jsxRuntime,e.componentsReact,e.livekitClient,e.react,e.client)}(this,function(e,t,n,r,o,a){function s(e){let{port:t,initialConfig:a}=e;const{agent:s,state:i}=n.useVoiceAssistant(),{send:c}=n.useChat(),[u,l]=o.useState({speaker:a.speaker||"agent",barCount:a.barCount,updateInterval:a.updateInterval}),p=n.useParticipantTracks([r.Track.Source.Microphone],null==s?void 0:s.identity)[0],d=n.useTrackTranscription(p),f=n.useAudioWaveform(p,{barCount:"agent"===u.speaker?u.barCount:120,updateInterval:"agent"===u.speaker?u.updateInterval:20}),m=n.useLocalParticipant(),g=n.useTrackTranscription({publication:m.microphoneTrack,source:r.Track.Source.Microphone,participant:m.localParticipant}),v=n.useParticipantTracks([r.Track.Source.Microphone],m.localParticipant.identity)[0],y=n.useAudioWaveform(v,{barCount:"user"===u.speaker?u.barCount:120,updateInterval:"user"===u.speaker?u.updateInterval:20});return o.useEffect(()=>{t&&f&&f.bars&&t.postMessage({type:"waveform_update",waveformData:f.bars,speaker:"agent"})},[t,f]),o.useEffect(()=>{t&&y&&y.bars&&t.postMessage({type:"waveform_update",waveformData:y.bars,speaker:"user"})},[t,y]),o.useEffect(()=>{if(!t)return;const e=e=>{const t=e.data;if("waveform_config"===t.type&&t.config)"number"==typeof t.config.barCount&&"number"==typeof t.config.updateInterval&&l(t.config);else if("send_text"===t.type)c?c(t.text):console.error("sendChat function is not available");else if("send_dtmf"===t.type)m.localParticipant?m.localParticipant.publishDtmf(101,t.digit):console.error("Local participant is not available for DTMF");else if("toggle_mic"===t.type&&"boolean"==typeof t.enabled)m.localParticipant?m.localParticipant.setMicrophoneEnabled(t.enabled).catch(e=>{console.error("Failed to toggle microphone:",e)}):console.error("Local participant is not available for mic toggle");else if("set_volume"===t.type&&"number"==typeof t.volume)if(s)try{s.setVolume(t.volume),console.log("Set agent volume to "+t.volume)}catch(e){console.error("Failed to set agent volume:",e)}else console.error("Agent is not available for volume control")};return t.start(),t.addEventListener("message",e),()=>{t.removeEventListener("message",e)}},[t,c,m,s]),o.useEffect(()=>{t&&t.postMessage({type:"state_update",state:i})},[i,t]),o.useEffect(()=>{if(t&&d.segments.length>0){const e=d.segments.map(e=>({id:e.id,text:e.text,isFinal:e.final,timestamp:Date.now(),speaker:"agent"}));t.postMessage({type:"transcription_update",transcriptions:e})}},[d.segments,t]),o.useEffect(()=>{if(t&&g.segments.length>0){const e=g.segments.map(e=>({id:e.id,text:e.text,isFinal:e.final,timestamp:Date.now(),speaker:"user"}));t.postMessage({type:"transcription_update",transcriptions:e})}},[g.segments,t]),n.useDataChannel("function_tools_executed",e=>{if(!t)return;const n=new TextDecoder,r=e.payload instanceof Uint8Array?n.decode(e.payload):String(e.payload);let o;try{o=JSON.parse(r),t.postMessage({type:"function_tools_executed",tool:o})}catch(e){console.error("Failed to parse function call log:",e)}}),null}e.useVoxAI=function(e){void 0===e&&(e={});const[r,i]=o.useState(null),[c,u]=o.useState("disconnected"),l=o.useRef(Date.now()),[p,d]=o.useState(new Map),[f,m]=o.useState([]),g=o.useRef(""),v=o.useRef(new Set),y=o.useRef(null),b=o.useRef(null),h=o.useRef(null),k=o.useRef(null),[w,C]=o.useState({agent:[],user:[]}),M=o.useRef(null),[E,x]=o.useState(!0);o.useEffect(()=>{const t=Array.from(p.values()).sort((e,t)=>e.timestamp-t.timestamp),n=JSON.stringify(t);n!==g.current&&(g.current=n,m(t),e.onMessage&&t.filter(e=>e.isFinal&&e.id&&!v.current.has(e.id)).forEach(t=>{t.id&&(v.current.add(t.id),null==e.onMessage||e.onMessage(t))}))},[p,e.onMessage]),o.useEffect(()=>{const e=new MessageChannel;return e.port1.onmessage=e=>{const t=e.data;if("state_update"===t.type)u(t.state);else if("transcription_update"===t.type)_(t.transcriptions);else if("waveform_update"===t.type&&t.speaker)C(e=>({...e,[t.speaker]:t.waveformData}));else if("function_tools_executed"===t.type&&t.tool){const e="function-calls-"+Date.now();d(n=>{const r=new Map(n);return r.set(e,{id:e,name:"tool",tool:t.tool,timestamp:Date.now(),isFinal:!0}),r})}},e.port1.start(),h.current=e,()=>{var e,t;null==(e=h.current)||e.port1.close(),null==(t=h.current)||t.port2.close(),h.current=null}},[]);const _=o.useCallback(e=>{d(t=>{const n=new Map(t);return e.forEach(e=>{var r;if(e.timestamp<l.current)return;const o="agent"===e.speaker?"agent":"user",a=(null==(r=t.get(e.id))?void 0:r.timestamp)||e.timestamp;n.set(e.id,{id:e.id,name:o,message:e.text,timestamp:a,isFinal:e.isFinal})}),n})},[]);o.useEffect(()=>{const e=document.createElement("div");return e.style.display="none",document.body.appendChild(e),y.current=e,b.current=a.createRoot(e),()=>{b.current&&b.current.unmount(),y.current&&document.body.removeChild(y.current)}},[]);const D=o.useCallback(function(t){let{agentId:n,agentVersion:r,apiKey:o,dynamicVariables:a,metadata:s}=t;try{return Promise.resolve(function(t,p){try{var d=function(){if("disconnected"!==c){const t="Connection attempt rejected: Already in a connection state ("+c+")";return console.warn(t),e.onError&&e.onError(new Error(t)),Promise.reject(new Error(t))}return l.current=Date.now(),u("connecting"),Promise.resolve(fetch("https://www.tryvox.co/api/agent/sdk",{method:"POST",headers:{Authorization:"Bearer "+o,"Content-Type":"application/json"},body:JSON.stringify({agent_id:n,agent_version:r||"current",metadata:{runtime_context:{source:{type:"react-sdk",version:"0.5.0"}},call_web:{dynamic_variables:a||{},metadata:s||{}}}})})).then(function(t){function n(n){return Promise.resolve(t.json()).then(function(t){i(t),e.onConnect&&e.onConnect()})}const r=function(){if(!t.ok)return Promise.resolve(t.text()).then(function(e){throw new Error("Connection failed ("+t.status+"): "+e)})}();return r&&r.then?r.then(n):n()})}()}catch(e){return p(e)}return d&&d.then?d.then(void 0,p):d}(0,function(t){i(null),d(new Map),m([]),u("disconnected");const n=t instanceof Error?t:new Error(String(t));e.onError&&e.onError(n)}))}catch(e){return Promise.reject(e)}},[e,c]),S=o.useCallback(()=>{l.current=Date.now(),i(null),d(new Map),m([]),u("disconnected"),e.onDisconnect&&e.onDisconnect()},[e]),T=o.useCallback(e=>{let{message:t,digit:n}=e;if("disconnected"!==c){if(t){const e="user-text-"+Date.now();d(n=>{const r=new Map(n);return r.set(e,{id:e,name:"user",message:t,timestamp:Date.now(),isFinal:!0}),r}),h.current?h.current.port1.postMessage({type:"send_text",text:t}):console.error("No message channel available to send message")}void 0!==n&&(h.current?h.current.port1.postMessage({type:"send_dtmf",digit:n}):console.error("No message channel available to send DTMF"))}else console.warn("Cannot send message: Not connected to a conversation")},[c]),P=o.useCallback(e=>{let{speaker:t="agent",barCount:n=10,updateInterval:r=20}=e;M.current={speaker:t,barCount:n,updateInterval:r},h.current&&h.current.port1.postMessage({type:"waveform_config",config:{speaker:t,barCount:n,updateInterval:r}});const o=w[t]||[];return o.length>0?o.slice(0,n):Array(n).fill(0)},[w]),R=o.useCallback(e=>{x(e),h.current?h.current.port1.postMessage({type:"toggle_mic",enabled:e}):console.error("No message channel available to toggle microphone")},[]),j=o.useCallback(e=>{const t=Math.min(Math.max(e,0),1);h.current?h.current.port1.postMessage({type:"set_volume",volume:t}):console.error("No message channel available to set volume")},[]);return o.useEffect(()=>{b.current&&(r?(k.current||(h.current&&h.current.port2.start(),k.current=t.jsxs(n.LiveKitRoom,{serverUrl:r.serverUrl,token:r.participantToken,audio:!0,video:!1,connect:!0,onDisconnected:S,onError:t=>{console.error("LiveKit connection error:",t),S(),e.onError&&e.onError(new Error("LiveKit connection error: "+t.message))},children:[t.jsx(n.RoomAudioRenderer,{}),h.current&&t.jsx(s,{port:h.current.port2,initialConfig:M.current||{barCount:10,updateInterval:20}})]})),b.current.render(k.current)):(k.current=null,b.current.render(t.jsx(t.Fragment,{}))))},[r,S,e.onError]),{connect:D,disconnect:S,state:c,messages:f,send:T,audioWaveform:P,toggleMic:R,setVolume:j}}});
2
2
  //# sourceMappingURL=lib.umd.js.map