mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-09 07:29:51 +00:00
feat: voice assistant
This commit is contained in:
@@ -15,6 +15,8 @@ import { createAttachmentAdapter } from '@/lib/attachmentAdapter'
|
||||
import { SessionHeader } from '@/components/SessionHeader'
|
||||
import { usePlatform } from '@/hooks/usePlatform'
|
||||
import { useSessionActions } from '@/hooks/mutations/useSessionActions'
|
||||
import { useVoiceOptional } from '@/lib/voice-context'
|
||||
import { RealtimeVoiceSession, registerSessionStore, registerVoiceHooksStore, voiceHooks } from '@/realtime'
|
||||
|
||||
export function SessionChat(props: {
|
||||
api: ApiClient
|
||||
@@ -49,6 +51,96 @@ export function SessionChat(props: {
|
||||
agentFlavor
|
||||
)
|
||||
|
||||
// Voice assistant integration
|
||||
const voice = useVoiceOptional()
|
||||
|
||||
// Register session store for voice client tools
|
||||
useEffect(() => {
|
||||
registerSessionStore({
|
||||
getSession: () => props.session as { agentState?: { requests?: Record<string, unknown> } } | null,
|
||||
sendMessage: (_sessionId: string, message: string) => props.onSend(message),
|
||||
approvePermission: async (_sessionId: string, requestId: string) => {
|
||||
await props.api.approvePermission(props.session.id, requestId)
|
||||
props.onRefresh()
|
||||
},
|
||||
denyPermission: async (_sessionId: string, requestId: string) => {
|
||||
await props.api.denyPermission(props.session.id, requestId)
|
||||
props.onRefresh()
|
||||
}
|
||||
})
|
||||
}, [props.session, props.api, props.onSend, props.onRefresh])
|
||||
|
||||
useEffect(() => {
|
||||
registerVoiceHooksStore(
|
||||
(sessionId) => (sessionId === props.session.id ? props.session : null),
|
||||
(sessionId) => (sessionId === props.session.id ? props.messages : [])
|
||||
)
|
||||
}, [props.session, props.messages])
|
||||
|
||||
// Track and report new messages to voice assistant
|
||||
// Note: voiceHooks internally checks isVoiceSessionStarted() so we don't need to check voice.status here
|
||||
const prevMessagesRef = useRef<DecryptedMessage[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const prevIds = new Set(prevMessagesRef.current.map(m => m.id))
|
||||
const newMessages = props.messages.filter(m => !prevIds.has(m.id))
|
||||
|
||||
if (newMessages.length > 0) {
|
||||
voiceHooks.onMessages(props.session.id, newMessages)
|
||||
}
|
||||
|
||||
prevMessagesRef.current = props.messages
|
||||
}, [props.messages, props.session.id])
|
||||
|
||||
// Report ready event when thinking stops
|
||||
// Note: voiceHooks internally checks isVoiceSessionStarted() so we don't need to check voice.status here
|
||||
const prevThinkingRef = useRef(props.session.thinking)
|
||||
|
||||
useEffect(() => {
|
||||
// Detect transition: thinking → not thinking
|
||||
if (prevThinkingRef.current && !props.session.thinking) {
|
||||
voiceHooks.onReady(props.session.id)
|
||||
}
|
||||
|
||||
prevThinkingRef.current = props.session.thinking
|
||||
}, [props.session.thinking, props.session.id])
|
||||
|
||||
// Report permission requests to voice assistant
|
||||
// Note: voiceHooks internally checks isVoiceSessionStarted() so we don't need to check voice.status here
|
||||
const prevRequestIdsRef = useRef<Set<string>>(new Set())
|
||||
|
||||
useEffect(() => {
|
||||
const requests = props.session.agentState?.requests ?? {}
|
||||
const currentIds = new Set(Object.keys(requests))
|
||||
|
||||
for (const [requestId, request] of Object.entries(requests)) {
|
||||
if (!prevRequestIdsRef.current.has(requestId)) {
|
||||
voiceHooks.onPermissionRequested(
|
||||
props.session.id,
|
||||
requestId,
|
||||
(request as { tool?: string }).tool ?? 'unknown',
|
||||
(request as { arguments?: unknown }).arguments
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
prevRequestIdsRef.current = currentIds
|
||||
}, [props.session.agentState?.requests, props.session.id])
|
||||
|
||||
const handleVoiceToggle = useCallback(async () => {
|
||||
if (!voice) return
|
||||
if (voice.status === 'connected' || voice.status === 'connecting') {
|
||||
await voice.stopVoice()
|
||||
} else {
|
||||
await voice.startVoice(props.session.id)
|
||||
}
|
||||
}, [voice, props.session.id])
|
||||
|
||||
const handleVoiceMicToggle = useCallback(() => {
|
||||
if (!voice) return
|
||||
voice.toggleMic()
|
||||
}, [voice])
|
||||
|
||||
useEffect(() => {
|
||||
normalizedCacheRef.current.clear()
|
||||
blocksByIdRef.current.clear()
|
||||
@@ -218,9 +310,22 @@ export function SessionChat(props: {
|
||||
onSwitchToRemote={handleSwitchToRemote}
|
||||
onTerminal={props.session.active ? handleViewTerminal : undefined}
|
||||
autocompleteSuggestions={props.autocompleteSuggestions}
|
||||
voiceStatus={voice?.status}
|
||||
voiceMicMuted={voice?.micMuted}
|
||||
onVoiceToggle={voice ? handleVoiceToggle : undefined}
|
||||
onVoiceMicToggle={voice ? handleVoiceMicToggle : undefined}
|
||||
/>
|
||||
</div>
|
||||
</AssistantRuntimeProvider>
|
||||
|
||||
{/* Voice session component - renders nothing but initializes ElevenLabs */}
|
||||
{voice && (
|
||||
<RealtimeVoiceSession
|
||||
api={props.api}
|
||||
micMuted={voice.micMuted}
|
||||
onStatusChange={voice.setStatus}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user