11import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from '../ui/select' ;
2+ import { Tooltip , TooltipContent , TooltipProvider , TooltipTrigger } from '../ui/tooltip' ;
23import { Avatar , AvatarFallback , AvatarImage } from '../ui/avatar' ;
34import { CurvedArrow , Puzzle , Stop } from '../icons/icons' ;
5+ import useComposeEditor from '@/hooks/use-compose-editor' ;
6+ import { InfoIcon , Mic , Mic2Icon } from 'lucide-react' ;
47import { useRef , useCallback , useEffect } from 'react' ;
58import { PricingDialog } from '../ui/pricing-dialog' ;
69import { Markdown } from '@react-email/components' ;
710import { useAIFullScreen } from '../ui/ai-sidebar' ;
811import { useBilling } from '@/hooks/use-billing' ;
912import { TextShimmer } from '../ui/text-shimmer' ;
1013import { useThread } from '@/hooks/use-threads' ;
14+ import { useConversation } from '@11labs/react' ;
1115import { MailLabels } from '../mail/mail-list' ;
1216import { cn , getEmailLogo } from '@/lib/utils' ;
17+ import { EditorContent } from '@tiptap/react' ;
18+ import { Tools } from '../../types/tools' ;
1319import { Button } from '../ui/button' ;
1420import { format } from 'date-fns-tz' ;
1521import { useQueryState } from 'nuqs' ;
@@ -26,7 +32,6 @@ const renderThread = (thread: { id: string; title: string; snippet: string }) =>
2632 const handleClick = ( ) => {
2733 setThreadId ( thread . id ) ;
2834 setAiSidebarOpen ( null ) ;
29- // Reset fullscreen state when clicking on a thread
3035 setIsFullScreen ( null ) ;
3136 } ;
3237
@@ -49,7 +54,7 @@ const renderThread = (thread: { id: string; title: string; snippet: string }) =>
4954 </ Avatar >
5055 < div className = "flex w-full flex-col gap-1.5" >
5156 < div className = "flex w-full items-center justify-between gap-2" >
52- < p className = "text-sm font-medium text-black dark:text-white" >
57+ < p className = "max-w-[20ch] truncate text-sm font-medium text-black dark:text-white" >
5358 { getThread . latest ?. sender ?. name }
5459 </ p >
5560 < span className = "max-w-[180px] truncate text-xs text-[#8C8C8C] dark:text-[#8C8C8C]" >
@@ -140,6 +145,7 @@ interface Message {
140145 result ?: {
141146 threads ?: Array < { id : string ; title : string ; snippet : string } > ;
142147 } ;
148+ args ?: any ;
143149 } ;
144150 } > ;
145151}
@@ -156,6 +162,92 @@ export interface AIChatProps {
156162 onModelChange ?: ( model : string ) => void ;
157163}
158164
165+ const ToolResponse = ( { toolName, result, args } : { toolName : string ; result : any ; args : any } ) => {
166+ const renderContent = ( ) => {
167+ switch ( toolName ) {
168+ case Tools . ListThreads :
169+ case Tools . AskZeroMailbox :
170+ return result ?. threads ? < RenderThreads threads = { result . threads } /> : null ;
171+
172+ case Tools . GetThread :
173+ return result ?. thread ? (
174+ < div className = "rounded-lg border border-gray-200 p-4 dark:border-gray-800" >
175+ < div className = "mb-2 flex items-center gap-2" >
176+ < Avatar className = "h-8 w-8" >
177+ < AvatarImage src = { getEmailLogo ( result . thread . sender ?. email ) } />
178+ < AvatarFallback > { result . thread . sender ?. name ?. [ 0 ] ?. toUpperCase ( ) } </ AvatarFallback >
179+ </ Avatar >
180+ < div >
181+ < p className = "font-medium" > { result . thread . sender ?. name } </ p >
182+ < p className = "text-sm text-gray-500" > { result . thread . subject } </ p >
183+ </ div >
184+ </ div >
185+ < div className = "prose dark:prose-invert max-w-none" >
186+ < Markdown > { result . thread . body } </ Markdown >
187+ </ div >
188+ </ div >
189+ ) : null ;
190+
191+ case Tools . GetUserLabels :
192+ return result ?. labels ? (
193+ < div className = "flex flex-wrap gap-2" >
194+ { result . labels . map ( ( label : any ) => (
195+ < MailLabels key = { label . id } labels = { [ label ] } />
196+ ) ) }
197+ </ div >
198+ ) : null ;
199+
200+ case Tools . WebSearch :
201+ return (
202+ < div className = "rounded-lg border border-gray-200 p-4 dark:border-gray-800" >
203+ < div className = "prose dark:prose-invert max-w-none" >
204+ < Markdown > { result } </ Markdown >
205+ </ div >
206+ </ div >
207+ ) ;
208+
209+ case Tools . ComposeEmail :
210+ return result ?. newBody ? (
211+ < div className = "rounded-lg border border-gray-200 p-4 dark:border-gray-800" >
212+ < div className = "prose dark:prose-invert max-w-none" >
213+ < Markdown > { result . newBody } </ Markdown >
214+ </ div >
215+ </ div >
216+ ) : null ;
217+
218+ default :
219+ if ( result ?. success ) {
220+ return (
221+ < div className = "text-sm text-green-600 dark:text-green-400" >
222+ Operation completed successfully
223+ </ div >
224+ ) ;
225+ }
226+ return null ;
227+ }
228+ } ;
229+
230+ const content = renderContent ( ) ;
231+ if ( ! content ) return null ;
232+
233+ return (
234+ < div className = "group relative" >
235+ < Tooltip >
236+ < TooltipTrigger asChild >
237+ < InfoIcon className = "fill-subtleWhite text-subtleBlack dark:fill-subtleBlack h-4 w-4 dark:text-[#373737]" />
238+ </ TooltipTrigger >
239+ < TooltipContent >
240+ < div className = "text-xs" >
241+ < p className = "mb-1 font-medium" > Tool Arguments:</ p >
242+ < pre className = "whitespace-pre-wrap break-words" > { JSON . stringify ( args , null , 2 ) } </ pre >
243+ </ div >
244+ </ TooltipContent >
245+ </ Tooltip >
246+ { content }
247+ </ div >
248+ ) ;
249+ } ;
250+
159251export function AIChat ( {
160252 messages,
161253 input,
@@ -178,6 +270,24 @@ export function AIChat({
178270 }
179271 } , [ ] ) ;
180272
273+ const editor = useComposeEditor ( {
274+ placeholder : 'Ask Zero to do anything...' ,
275+ onLengthChange : ( ) => setInput ( editor . getText ( ) ) ,
276+ onKeydown ( event ) {
277+ if ( event . key === 'Enter' && ! event . metaKey && ! event . shiftKey ) {
278+ event . preventDefault ( ) ;
279+ handleSubmit ( event as unknown as React . FormEvent < HTMLFormElement > ) ;
280+ editor . commands . clearContent ( true ) ;
281+ }
282+ } ,
283+ } ) ;
284+
285+ const onSubmit = ( e : React . FormEvent < HTMLFormElement > ) => {
286+ e . preventDefault ( ) ;
287+ handleSubmit ( e ) ;
288+ editor . commands . clearContent ( true ) ;
289+ } ;
290+
181291 useEffect ( ( ) => {
182292 scrollToBottom ( ) ;
183293 } , [ messages , scrollToBottom ] ) ;
@@ -219,21 +329,22 @@ export function AIChat({
219329 </ div >
220330 ) : (
221331 messages . map ( ( message , index ) => {
222- // Separate text and tool-invocation parts
223332 const textParts = message . parts . filter ( ( part ) => part . type === 'text' ) ;
224333 const toolParts = message . parts . filter ( ( part ) => part . type === 'tool-invocation' ) ;
225334 return (
226335 < div key = { `${ message . id } -${ index } ` } className = "flex flex-col gap-2" >
227336 { toolParts . map ( ( part , idx ) =>
228- part . toolInvocation &&
229- 'result' in part . toolInvocation &&
230- part . toolInvocation . result &&
231- 'threads' in part . toolInvocation . result ? (
232- < RenderThreads threads = { part . toolInvocation . result . threads ?? [ ] } key = { idx } />
337+ part . toolInvocation && part . toolInvocation . result ? (
338+ < ToolResponse
339+ key = { idx }
340+ toolName = { part . toolInvocation . toolName }
341+ result = { part . toolInvocation . result }
342+ args = { part . toolInvocation . args }
343+ />
233344 ) : null ,
234345 ) }
235346 { textParts . length > 0 && (
236- < div
347+ < p
237348 className = { cn (
238349 'flex w-fit flex-col gap-2 rounded-lg text-sm' ,
239350 message . role === 'user'
@@ -242,10 +353,9 @@ export function AIChat({
242353 ) }
243354 >
244355 { textParts . map (
245- ( part ) =>
246- part . text && < Markdown key = { part . text } > { part . text || ' ' } </ Markdown > ,
356+ ( part ) => part . text && < span key = { part . text } > { part . text || ' ' } </ span > ,
247357 ) }
248- </ div >
358+ </ p >
249359 ) }
250360 </ div >
251361 ) ;
@@ -270,45 +380,39 @@ export function AIChat({
270380
271381 { /* Fixed input at bottom */ }
272382 < div className = { cn ( 'mb-4 flex-shrink-0 px-4' , isFullScreen ? 'px-0' : '' ) } >
273- < div className = "bg-offsetLight relative rounded-lg dark:bg-[#141414 ]" >
383+ < div className = "bg-offsetLight relative rounded-lg p-2 dark:bg-[#202020 ]" >
274384 { showVoiceChat ? (
275385 < VoiceChat onClose = { ( ) => setShowVoiceChat ( false ) } />
276386 ) : (
277387 < div className = "flex flex-col" >
278388 < div className = "w-full" >
279- < form id = "ai-chat-form" onSubmit = { handleSubmit } className = "relative" >
280- < Input
281- ref = { inputRef }
282- readOnly = { ! chatMessages . enabled }
283- value = { input }
284- onChange = { ( e ) => setInput ( e . target . value ) }
285- placeholder = "Ask Zero to do anything..."
286- className = "placeholder:text-muted-foreground h-8 w-full resize-none rounded-lg border-none bg-white px-3 py-2 pr-10 text-sm ring-0 focus:ring-0 focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-[#141414]"
287- />
288- { status === 'ready' ? (
289- < button
290- form = "ai-chat-form"
291- type = "submit"
292- className = "absolute right-1 top-1/2 inline-flex h-6 -translate-y-1/2 cursor-pointer items-center justify-center gap-1.5 overflow-hidden rounded-lg"
293- disabled = { ! input . trim ( ) || ! chatMessages . enabled }
294- >
295- < div className = "dark:bg[#141414] flex h-5 items-center justify-center gap-1 rounded-sm bg-[#262626] px-1 pr-0.5" >
296- < CurvedArrow className = "mt-1.5 h-4 w-4 fill-white dark:fill-[#929292]" />
297- </ div >
298- </ button >
299- ) : (
300- < button
301- onClick = { stop }
302- type = "button"
303- className = "absolute right-1 top-1/2 inline-flex h-6 -translate-y-1/2 cursor-pointer items-center justify-center gap-1.5 overflow-hidden rounded-lg"
389+ < form id = "ai-chat-form" onSubmit = { onSubmit } className = "relative" >
390+ < div className = "grow self-stretch overflow-y-auto bg-[#FFFFFF] outline-white/5 dark:bg-[#202020]" >
391+ < div
392+ onClick = { ( ) => {
393+ editor . commands . focus ( ) ;
394+ } }
395+ className = { cn ( 'max-h-[100px] w-full' ) }
304396 >
305- < div className = "flex h-5 items-center justify-center gap-1 rounded-sm px-1" >
306- < Stop className = "h-4 w-4 fill-[#DE5555]" />
307- </ div >
308- </ button >
309- ) }
397+ < EditorContent editor = { editor } className = "h-full w-full" />
398+ </ div >
399+ </ div >
310400 </ form >
311401 </ div >
402+ < div className = "grid" >
403+ < div className = "flex justify-end" >
404+ < button
405+ form = "ai-chat-form"
406+ type = "submit"
407+ className = "inline-flex cursor-pointer gap-1.5 rounded-lg"
408+ disabled = { ! chatMessages . enabled }
409+ >
410+ < div className = "dark:bg[#141414] flex h-5 items-center justify-center gap-1 rounded-sm bg-[#262626] px-2 pr-1" >
411+ < CurvedArrow className = "mt-1.5 h-4 w-4 fill-white dark:fill-[#929292]" />
412+ </ div >
413+ </ button >
414+ </ div >
415+ </ div >
312416 </ div >
313417 ) }
314418 </ div >
0 commit comments