diff --git a/apps/mail/components/create/ai-chat.tsx b/apps/mail/components/create/ai-chat.tsx index c1e637e57e..81c01d0f56 100644 --- a/apps/mail/components/create/ai-chat.tsx +++ b/apps/mail/components/create/ai-chat.tsx @@ -1,8 +1,8 @@ import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'; import { useAIFullScreen, useAISidebar } from '../ui/ai-sidebar'; +import { useRef, useCallback, useEffect, useState } from 'react'; import { VoiceProvider } from '@/providers/voice-provider'; import useComposeEditor from '@/hooks/use-compose-editor'; -import { useRef, useCallback, useEffect } from 'react'; import type { useAgentChat } from 'agents/ai-react'; import { Markdown } from '@react-email/components'; import { useBilling } from '@/hooks/use-billing'; @@ -146,9 +146,7 @@ export interface AIChatProps { setMessages: (messages: AiMessage[]) => void; } -// Subcomponents for ToolResponse const GetThreadToolResponse = ({ result, args }: { result: any; args: any }) => { - // Extract threadId from result or args let threadId: string | null = null; if (typeof result === 'string') { const match = result.match(//); @@ -181,7 +179,6 @@ const ComposeEmailToolResponse = ({ result }: { result: any }) => { ); }; -// Main ToolResponse switcher const ToolResponse = ({ toolName, result, args }: { toolName: string; result: any; args: any }) => { switch (toolName) { case Tools.GetThread: @@ -209,6 +206,7 @@ export function AIChat({ const [, setPricingDialog] = useQueryState('pricingDialog'); const [aiSidebarOpen] = useQueryState('aiSidebar'); const { toggleOpen } = useAISidebar(); + const voiceResponseCallbackRef = useRef<((response: string) => void) | null>(null); const scrollToBottom = useCallback(() => { if (messagesEndRef.current) { @@ -222,6 +220,25 @@ export function AIChat({ } }, [status, scrollToBottom]); + const [isVoiceQuery, setIsVoiceQuery] = useState(false); + + useEffect(() => { + if (isVoiceQuery && messages.length > 0) { + const lastMessage = messages[messages.length - 1]; + if (lastMessage.role === 'assistant') { + const textContent = lastMessage.parts + .filter((part) => part.type === 'text' && 'text' in part) + .map((part) => (part as any).text) + .join(' '); + + if (textContent && voiceResponseCallbackRef.current) { + voiceResponseCallbackRef.current(textContent); + setIsVoiceQuery(false); + } + } + } + }, [messages, isVoiceQuery]); + const editor = useComposeEditor({ placeholder: 'Ask Zero to do anything...', onLengthChange: () => setInput(editor.getText()), @@ -284,7 +301,6 @@ export function AIChat({ Ask to do or show anything using natural language

- {/* Example Thread */} ) : ( @@ -293,14 +309,19 @@ export function AIChat({ const toolParts = message.parts.filter((part) => part.type === 'tool-invocation'); return ( -
+
{toolParts.map( (part, index) => - part.toolInvocation?.result && ( + part.toolInvocation && + 'result' in part.toolInvocation && ( ), @@ -367,7 +388,6 @@ export function AIChat({
- {/* Fixed input at bottom */}
@@ -387,7 +407,17 @@ export function AIChat({
- + { + setIsVoiceQuery(true); + editor.commands.setContent(transcript); + setInput(transcript); + onSubmit({ preventDefault: () => {} } as React.FormEvent); + }} + onResponseReady={(callback) => { + voiceResponseCallbackRef.current = callback; + }} + >