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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 40 additions & 10 deletions apps/mail/components/create/ai-chat.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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(/<thread id="([^"]+)" ?\/>/);
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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) {
Expand All @@ -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()),
Expand Down Expand Up @@ -284,7 +301,6 @@ export function AIChat({
Ask to do or show anything using natural language
</p>

{/* Example Thread */}
<ExampleQueries onQueryClick={handleQueryClick} />
</div>
) : (
Expand All @@ -293,14 +309,19 @@ export function AIChat({
const toolParts = message.parts.filter((part) => part.type === 'tool-invocation');

return (
<div key={`${message.id}-${index}`} className="mb-2 flex flex-col" data-message-role={message.role}>
<div
key={`${message.id}-${index}`}
className="mb-2 flex flex-col"
data-message-role={message.role}
>
{toolParts.map(
(part, index) =>
part.toolInvocation?.result && (
part.toolInvocation &&
'result' in part.toolInvocation && (
<ToolResponse
key={`${part.toolInvocation.toolName}-${index}`}
toolName={part.toolInvocation.toolName}
result={part.toolInvocation.result}
result={(part.toolInvocation as any).result}
args={part.toolInvocation.args}
/>
),
Expand Down Expand Up @@ -367,7 +388,6 @@ export function AIChat({
</div>
</div>

{/* Fixed input at bottom */}
<div className={cn('mb-4 shrink-0 px-4', isFullScreen ? 'px-0' : '')}>
<div className="bg-offsetLight relative rounded-lg p-2 dark:bg-[#202020]">
<div className="flex flex-col">
Expand All @@ -387,7 +407,17 @@ export function AIChat({
</div>
<div className="grid">
<div className="flex justify-end gap-1">
<VoiceProvider>
<VoiceProvider
onTranscriptComplete={(transcript) => {
setIsVoiceQuery(true);
editor.commands.setContent(transcript);
setInput(transcript);
onSubmit({ preventDefault: () => {} } as React.FormEvent<HTMLFormElement>);
}}
onResponseReady={(callback) => {
voiceResponseCallbackRef.current = callback;
}}
>
<VoiceButton />
</VoiceProvider>
<button
Expand Down
Loading
Loading