2025-12-19 16:42:58 +02:00
import {
2026-03-24 02:22:51 +05:30
AuiIf ,
2025-12-19 16:42:58 +02:00
ComposerPrimitive ,
MessagePrimitive ,
ThreadPrimitive ,
2026-03-24 02:22:51 +05:30
useAui ,
useAuiState ,
2026-03-29 02:45:59 -07:00
useThreadViewportStore ,
2025-12-19 16:42:58 +02:00
} from "@assistant-ui/react" ;
2025-12-23 16:04:39 +05:30
import { useAtom , useAtomValue , useSetAtom } from "jotai" ;
2025-12-19 16:42:58 +02:00
import {
2025-12-23 01:16:25 -08:00
AlertCircle ,
2025-12-19 16:42:58 +02:00
ArrowDownIcon ,
ArrowUpIcon ,
2026-03-29 02:54:48 +02:00
ChevronDown ,
ChevronUp ,
Clipboard ,
2026-04-03 17:28:12 +05:30
Dot ,
2026-03-17 01:09:15 +05:30
Globe ,
2026-03-15 16:27:33 +05:30
Plus ,
2026-03-15 16:39:56 +05:30
Settings2 ,
2025-12-19 16:42:58 +02:00
SquareIcon ,
2026-03-10 16:17:12 +05:30
Unplug ,
2026-03-15 16:27:33 +05:30
Upload ,
2026-03-21 11:38:42 +05:30
Wrench ,
2026-03-10 16:16:24 +05:30
X ,
2025-12-19 16:42:58 +02:00
} from "lucide-react" ;
2026-03-17 01:09:15 +05:30
import { AnimatePresence , motion } from "motion/react" ;
2026-03-21 11:38:42 +05:30
import Image from "next/image" ;
2025-12-23 16:04:39 +05:30
import { useParams } from "next/navigation" ;
2026-04-08 05:21:39 +05:30
import { type FC , useCallback , useEffect , useLayoutEffect , useMemo , useRef , useState } from "react" ;
2025-12-23 14:24:36 +05:30
import { createPortal } from "react-dom" ;
2026-03-11 12:30:20 +05:30
import {
agentToolsAtom ,
disabledToolsAtom ,
hydrateDisabledToolsAtom ,
toggleToolAtom ,
} from "@/atoms/agent-tools/agent-tools.atoms" ;
2026-01-22 16:43:08 -08:00
import { chatSessionStateAtom } from "@/atoms/chat/chat-session-state.atom" ;
2025-12-24 07:06:35 +02:00
import {
mentionedDocumentsAtom ,
2026-03-06 15:59:45 +05:30
sidebarSelectedDocumentsAtom ,
2025-12-24 07:06:35 +02:00
} from "@/atoms/chat/mentioned-documents.atom" ;
2026-03-10 16:16:24 +05:30
import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms" ;
2026-03-10 14:45:37 +05:30
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms" ;
2026-03-07 12:57:27 +05:30
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms" ;
2026-01-20 18:39:50 +02:00
import { membersAtom } from "@/atoms/members/members-query.atoms" ;
2025-12-23 01:16:25 -08:00
import {
globalNewLLMConfigsAtom ,
llmPreferencesAtom ,
newLLMConfigsAtom ,
} from "@/atoms/new-llm-config/new-llm-config-query.atoms" ;
import { currentUserAtom } from "@/atoms/user/user-query.atoms" ;
2026-01-16 15:09:51 +02:00
import { AssistantMessage } from "@/components/assistant-ui/assistant-message" ;
2026-01-20 18:39:50 +02:00
import { ChatSessionStatus } from "@/components/assistant-ui/chat-session-status" ;
2026-03-10 17:36:26 -07:00
import { ConnectorIndicator } from "@/components/assistant-ui/connector-popup" ;
2026-03-15 16:39:56 +05:30
import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup" ;
2025-12-25 13:44:18 +05:30
import {
InlineMentionEditor ,
type InlineMentionEditorRef ,
} from "@/components/assistant-ui/inline-mention-editor" ;
2025-12-19 16:42:58 +02:00
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button" ;
2026-01-15 00:05:53 -08:00
import { UserMessage } from "@/components/assistant-ui/user-message" ;
2026-03-10 16:17:12 +05:30
import { SLIDEOUT_PANEL_OPENED_EVENT } from "@/components/layout/ui/sidebar/SidebarSlideOutPanel" ;
2025-12-24 07:06:35 +02:00
import {
2025-12-26 00:41:14 +05:30
DocumentMentionPicker ,
type DocumentMentionPickerRef ,
2025-12-25 11:42:12 -08:00
} from "@/components/new-chat/document-mention-picker" ;
2026-03-30 01:50:41 +05:30
import { PromptPicker , type PromptPickerRef } from "@/components/new-chat/prompt-picker" ;
2026-03-10 16:16:24 +05:30
import { Avatar , AvatarFallback , AvatarGroup } from "@/components/ui/avatar" ;
2025-12-19 16:42:58 +02:00
import { Button } from "@/components/ui/button" ;
2026-03-15 16:39:56 +05:30
import { Drawer , DrawerContent , DrawerHandle , DrawerTitle } from "@/components/ui/drawer" ;
2026-03-15 16:27:33 +05:30
import {
DropdownMenu ,
DropdownMenuContent ,
DropdownMenuItem ,
DropdownMenuTrigger ,
} from "@/components/ui/dropdown-menu" ;
2026-03-15 16:39:56 +05:30
import { Popover , PopoverContent , PopoverTrigger } from "@/components/ui/popover" ;
2026-03-11 12:30:20 +05:30
import { Switch } from "@/components/ui/switch" ;
import { Tooltip , TooltipContent , TooltipTrigger } from "@/components/ui/tooltip" ;
2026-03-10 16:16:24 +05:30
import { getConnectorIcon } from "@/contracts/enums/connectorIcons" ;
2026-03-21 13:20:13 +05:30
import {
CONNECTOR_ICON_TO_TYPES ,
CONNECTOR_TOOL_ICON_PATHS ,
getToolIcon ,
} from "@/contracts/enums/toolIcons" ;
2025-12-23 16:04:39 +05:30
import type { Document } from "@/contracts/types/document.types" ;
2026-02-27 17:19:25 -08:00
import { useBatchCommentsPreload } from "@/hooks/use-comments" ;
2026-03-23 19:29:08 +02:00
import { useCommentsSync } from "@/hooks/use-comments-sync" ;
2026-03-11 12:04:22 +05:30
import { useMediaQuery } from "@/hooks/use-media-query" ;
2026-04-07 00:43:40 -07:00
import { useElectronAPI } from "@/hooks/use-platform" ;
2025-12-22 23:29:49 +02:00
import { cn } from "@/lib/utils" ;
2025-12-22 22:54:22 +05:30
2026-04-07 02:49:24 -07:00
const COMPOSER_PLACEHOLDER = "Ask anything · Type / for prompts · Type @ to mention docs" ;
2026-02-01 17:26:50 -05:00
2026-03-24 02:23:05 +05:30
export const Thread : FC = ( ) = > {
return < ThreadContent / > ;
2026-01-19 14:37:06 +02:00
} ;
2026-03-06 22:38:49 +05:30
const ThreadContent : FC = ( ) = > {
2026-01-19 14:37:06 +02:00
return (
< ThreadPrimitive.Root
2026-03-17 01:09:15 +05:30
className = "aui-root aui-thread-root @container flex h-full min-h-0 flex-col bg-main-panel"
2026-01-19 14:37:06 +02:00
style = { {
[ "--thread-max-width" as string ] : "44rem" ,
} }
>
< ThreadPrimitive.Viewport
turnAnchor = "top"
2026-03-29 02:45:59 -07:00
className = "aui-thread-viewport relative flex flex-1 min-h-0 flex-col overflow-y-auto px-4 pt-4"
style = { { scrollbarGutter : "stable" } }
2025-12-19 16:42:58 +02:00
>
2026-03-24 02:22:51 +05:30
< AuiIf condition = { ( { thread } ) = > thread . isEmpty } >
2026-01-19 14:37:06 +02:00
< ThreadWelcome / >
2026-03-24 02:22:51 +05:30
< / AuiIf >
2025-12-19 16:42:58 +02:00
2026-01-19 14:37:06 +02:00
< ThreadPrimitive.Messages
components = { {
UserMessage ,
EditComposer ,
AssistantMessage ,
} }
/ >
2025-12-19 16:42:58 +02:00
2026-02-06 19:43:14 +05:30
< ThreadPrimitive.ViewportFooter
2026-03-29 02:45:59 -07:00
className = "aui-thread-viewport-footer sticky bottom-0 z-10 mx-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-3xl bg-main-panel pb-4 md:pb-6"
2026-02-06 19:43:14 +05:30
style = { { paddingBottom : "max(1rem, env(safe-area-inset-bottom))" } }
>
2026-01-19 14:37:06 +02:00
< ThreadScrollToBottom / >
2026-03-24 02:22:51 +05:30
< AuiIf condition = { ( { thread } ) = > ! thread . isEmpty } >
2026-01-19 14:37:06 +02:00
< div className = "fade-in slide-in-from-bottom-4 animate-in duration-500 ease-out fill-mode-both" >
< Composer / >
< / div >
2026-03-24 02:22:51 +05:30
< / AuiIf >
2026-01-19 14:37:06 +02:00
< / ThreadPrimitive.ViewportFooter >
< / ThreadPrimitive.Viewport >
< / ThreadPrimitive.Root >
2025-12-19 16:42:58 +02:00
) ;
} ;
const ThreadScrollToBottom : FC = ( ) = > {
return (
< ThreadPrimitive.ScrollToBottom asChild >
< TooltipIconButton
tooltip = "Scroll to bottom"
variant = "outline"
2026-03-17 01:09:15 +05:30
className = "aui-thread-scroll-to-bottom -top-12 absolute z-10 self-center rounded-full p-4 disabled:invisible dark:bg-main-panel dark:hover:bg-accent"
2025-12-19 16:42:58 +02:00
>
< ArrowDownIcon / >
< / TooltipIconButton >
< / ThreadPrimitive.ScrollToBottom >
) ;
} ;
2026-01-16 11:32:06 -08:00
const getTimeBasedGreeting = ( user ? : { display_name? : string | null ; email? : string } ) : string = > {
2025-12-22 18:38:08 +05:30
const hour = new Date ( ) . getHours ( ) ;
2025-12-23 01:16:25 -08:00
2026-01-16 11:32:06 -08:00
// Extract first name: prefer display_name, fall back to email extraction
let firstName : string | null = null ;
if ( user ? . display_name ? . trim ( ) ) {
// Use display_name if available and not empty
// Extract first name from display_name (take first word)
const nameParts = user . display_name . trim ( ) . split ( /\s+/ ) ;
firstName = nameParts [ 0 ] . charAt ( 0 ) . toUpperCase ( ) + nameParts [ 0 ] . slice ( 1 ) . toLowerCase ( ) ;
} else if ( user ? . email ) {
// Fall back to email extraction if display_name is not available
firstName =
user . email . split ( "@" ) [ 0 ] . split ( "." ) [ 0 ] . charAt ( 0 ) . toUpperCase ( ) +
user . email . split ( "@" ) [ 0 ] . split ( "." ) [ 0 ] . slice ( 1 ) ;
}
2025-12-23 01:16:25 -08:00
2025-12-22 18:38:08 +05:30
// Array of greeting variations for each time period
2025-12-25 12:18:45 +05:30
const morningGreetings = [ "Good morning" , "Fresh start today" , "Morning" , "Hey there" ] ;
2025-12-23 01:16:25 -08:00
const afternoonGreetings = [ "Good afternoon" , "Afternoon" , "Hey there" , "Hi there" ] ;
const eveningGreetings = [ "Good evening" , "Evening" , "Hey there" , "Hi there" ] ;
const nightGreetings = [ "Good night" , "Evening" , "Hey there" , "Winding down" ] ;
2025-12-25 12:18:45 +05:30
const lateNightGreetings = [ "Still up" , "Night owl mode" , "Up past bedtime" , "Hi there" ] ;
2025-12-23 01:16:25 -08:00
2025-12-22 18:38:08 +05:30
// Select a random greeting based on time
let greeting : string ;
2025-12-22 23:57:16 +05:30
if ( hour < 5 ) {
// Late night: midnight to 5 AM
greeting = lateNightGreetings [ Math . floor ( Math . random ( ) * lateNightGreetings . length ) ] ;
} else if ( hour < 12 ) {
2025-12-22 18:38:08 +05:30
greeting = morningGreetings [ Math . floor ( Math . random ( ) * morningGreetings . length ) ] ;
2025-12-22 23:57:16 +05:30
} else if ( hour < 18 ) {
2025-12-22 18:38:08 +05:30
greeting = afternoonGreetings [ Math . floor ( Math . random ( ) * afternoonGreetings . length ) ] ;
2025-12-22 23:57:16 +05:30
} else if ( hour < 22 ) {
2025-12-22 18:38:08 +05:30
greeting = eveningGreetings [ Math . floor ( Math . random ( ) * eveningGreetings . length ) ] ;
} else {
2025-12-22 23:57:16 +05:30
// Night: 10 PM to midnight
2025-12-22 18:38:08 +05:30
greeting = nightGreetings [ Math . floor ( Math . random ( ) * nightGreetings . length ) ] ;
}
2025-12-23 01:16:25 -08:00
2025-12-22 18:38:08 +05:30
// Add personalization with first name if available
if ( firstName ) {
return ` ${ greeting } , ${ firstName } ! ` ;
}
2025-12-23 01:16:25 -08:00
2025-12-22 18:38:08 +05:30
return ` ${ greeting } ! ` ;
2025-12-19 16:42:58 +02:00
} ;
2025-12-22 18:38:08 +05:30
const ThreadWelcome : FC = ( ) = > {
const { data : user } = useAtomValue ( currentUserAtom ) ;
2025-12-24 07:06:35 +02:00
2025-12-23 14:24:36 +05:30
// Memoize greeting so it doesn't change on re-renders (only on user change)
2026-01-16 11:32:06 -08:00
const greeting = useMemo ( ( ) = > getTimeBasedGreeting ( user ) , [ user ] ) ;
2025-12-23 01:16:25 -08:00
2025-12-19 16:42:58 +02:00
return (
2025-12-22 18:38:08 +05:30
< div className = "aui-thread-welcome-root mx-auto flex w-full max-w-(--thread-max-width) grow flex-col items-center px-4 relative" >
2026-03-11 16:37:56 -07:00
{ /* Greeting positioned above the composer */ }
2025-12-23 19:10:58 -08:00
< div className = "aui-thread-welcome-message absolute bottom-[calc(50%+5rem)] left-0 right-0 flex flex-col items-center text-center" >
2026-03-17 04:40:46 +05:30
< h1 className = "aui-thread-welcome-message-inner text-3xl md:text-5xl select-none" >
{ greeting }
< / h1 >
2025-12-22 18:38:08 +05:30
< / div >
2025-12-23 02:21:41 +05:30
{ /* Composer - top edge fixed, expands downward only */ }
2026-03-11 16:37:56 -07:00
< div className = "w-full flex items-start justify-center absolute top-[calc(50%-3.5rem)] left-0 right-0" >
2025-12-22 18:38:08 +05:30
< Composer / >
< / div >
2025-12-19 16:42:58 +02:00
< / div >
) ;
} ;
2026-03-10 16:16:24 +05:30
const BANNER_CONNECTORS = [
{ type : "GOOGLE_DRIVE_CONNECTOR" , label : "Google Drive" } ,
{ type : "GOOGLE_GMAIL_CONNECTOR" , label : "Gmail" } ,
{ type : "NOTION_CONNECTOR" , label : "Notion" } ,
{ type : "YOUTUBE_CONNECTOR" , label : "YouTube" } ,
{ type : "SLACK_CONNECTOR" , label : "Slack" } ,
] as const ;
const BANNER_DISMISSED_KEY = "surfsense-connect-tools-banner-dismissed" ;
2026-03-21 21:03:23 +05:30
const ConnectToolsBanner : FC < { isThreadEmpty : boolean } > = ( { isThreadEmpty } ) = > {
2026-03-10 16:16:24 +05:30
const { data : connectors } = useAtomValue ( connectorsAtom ) ;
const setConnectorDialogOpen = useSetAtom ( connectorDialogOpenAtom ) ;
const [ dismissed , setDismissed ] = useState ( ( ) = > {
if ( typeof window === "undefined" ) return false ;
return localStorage . getItem ( BANNER_DISMISSED_KEY ) === "true" ;
} ) ;
const hasConnectors = ( connectors ? . length ? ? 0 ) > 0 ;
2026-03-21 21:03:23 +05:30
if ( dismissed || hasConnectors || ! isThreadEmpty ) return null ;
2026-03-10 16:16:24 +05:30
const handleDismiss = ( e : React.MouseEvent ) = > {
e . stopPropagation ( ) ;
setDismissed ( true ) ;
localStorage . setItem ( BANNER_DISMISSED_KEY , "true" ) ;
} ;
return (
2026-03-17 03:36:32 +05:30
< div className = "border-t border-border/50" >
< div className = "flex w-full items-center gap-2.5 px-4 py-2.5" >
< button
type = "button"
2026-04-08 04:48:16 +05:30
className = "flex flex-1 items-center gap-2.5 text-left cursor-pointer select-none"
2026-03-17 03:36:32 +05:30
onClick = { ( ) = > setConnectorDialogOpen ( true ) }
>
< Unplug className = "size-4 text-muted-foreground shrink-0" / >
< span className = "text-[13px] text-muted-foreground/80 flex-1" > Connect your tools < / span >
< AvatarGroup className = "shrink-0" >
{ BANNER_CONNECTORS . map ( ( { type } , i ) = > (
2026-03-17 04:40:46 +05:30
< Avatar
key = { type }
className = "size-6"
style = { { zIndex : BANNER_CONNECTORS.length - i } }
>
2026-03-17 03:36:32 +05:30
< AvatarFallback className = "bg-muted text-[10px]" >
{ getConnectorIcon ( type , "size-3.5" ) }
< / AvatarFallback >
< / Avatar >
) ) }
< / AvatarGroup >
< / button >
2026-03-15 16:27:33 +05:30
< button
type = "button"
2026-03-10 16:16:24 +05:30
onClick = { handleDismiss }
2026-03-17 03:36:32 +05:30
className = "shrink-0 ml-0.5 p-1.5 -mr-1 text-muted-foreground/40 hover:text-foreground transition-colors cursor-pointer"
2026-03-10 16:16:24 +05:30
aria - label = "Dismiss"
>
2026-03-17 03:36:32 +05:30
< X className = "size-3.5 text-muted-foreground" / >
2026-03-15 16:27:33 +05:30
< / button >
2026-03-17 03:36:32 +05:30
< / div >
2026-03-10 16:16:24 +05:30
< / div >
) ;
} ;
2026-03-29 02:54:48 +02:00
const ClipboardChip : FC < { text : string ; onDismiss : ( ) = > void } > = ( { text , onDismiss } ) = > {
const [ expanded , setExpanded ] = useState ( false ) ;
const isLong = text . length > 120 ;
const preview = isLong ? ` ${ text . slice ( 0 , 120 ) } … ` : text ;
return (
< div className = "mx-3 mt-2 rounded-lg border border-border/40 bg-background/60" >
< div className = "flex items-center gap-2 px-3 py-2" >
< Clipboard className = "size-4 shrink-0 text-muted-foreground" / >
< span className = "text-xs font-medium text-muted-foreground" > From clipboard < / span >
< div className = "flex-1" / >
{ isLong && (
< button
type = "button"
onClick = { ( ) = > setExpanded ( ( v ) = > ! v ) }
className = "flex items-center text-muted-foreground hover:text-foreground transition-colors"
>
{ expanded ? < ChevronUp className = "size-3.5" / > : < ChevronDown className = "size-3.5" / > }
< / button >
) }
< button
type = "button"
onClick = { onDismiss }
className = "flex items-center text-muted-foreground hover:text-foreground transition-colors"
>
< X className = "size-3.5" / >
< / button >
< / div >
< div className = "px-3 pb-2" >
< p className = "text-xs text-foreground/80 whitespace-pre-wrap wrap-break-word leading-relaxed" >
{ expanded ? text : preview }
< / p >
< / div >
< / div >
) ;
} ;
2025-12-19 16:42:58 +02:00
const Composer : FC = ( ) = > {
2026-01-18 20:13:51 +05:30
// Document mention state (atoms persist across component remounts)
2025-12-23 15:13:03 +05:30
const [ mentionedDocuments , setMentionedDocuments ] = useAtom ( mentionedDocumentsAtom ) ;
2026-03-06 23:33:51 +05:30
const setSidebarDocs = useSetAtom ( sidebarSelectedDocumentsAtom ) ;
2025-12-22 23:17:48 +02:00
const [ showDocumentPopover , setShowDocumentPopover ] = useState ( false ) ;
2026-03-29 00:07:08 +02:00
const [ showPromptPicker , setShowPromptPicker ] = useState ( false ) ;
2025-12-23 15:13:03 +05:30
const [ mentionQuery , setMentionQuery ] = useState ( "" ) ;
2026-03-28 23:20:10 +02:00
const [ actionQuery , setActionQuery ] = useState ( "" ) ;
2026-04-08 05:21:39 +05:30
const [ containerPos , setContainerPos ] = useState ( { bottom : "200px" , left : "50%" , top : "auto" } ) ;
2025-12-25 13:44:18 +05:30
const editorRef = useRef < InlineMentionEditorRef > ( null ) ;
const editorContainerRef = useRef < HTMLDivElement > ( null ) ;
2026-03-29 02:54:48 +02:00
const composerBoxRef = useRef < HTMLDivElement > ( null ) ;
2025-12-26 00:41:14 +05:30
const documentPickerRef = useRef < DocumentMentionPickerRef > ( null ) ;
2026-03-29 00:07:08 +02:00
const promptPickerRef = useRef < PromptPickerRef > ( null ) ;
2026-04-08 05:21:39 +05:30
const viewportRef = useRef < Element | null > ( null ) ;
2026-01-20 18:39:50 +02:00
const { search_space_id , chat_id } = useParams ( ) ;
2026-03-24 02:22:51 +05:30
const aui = useAui ( ) ;
2026-03-29 02:45:59 -07:00
const threadViewportStore = useThreadViewportStore ( ) ;
2025-12-25 14:19:22 +05:30
const hasAutoFocusedRef = useRef ( false ) ;
2026-03-29 02:45:59 -07:00
const submitCleanupRef = useRef < ( ( ) = > void ) | null > ( null ) ;
useEffect ( ( ) = > {
2026-03-30 01:50:41 +05:30
return ( ) = > {
submitCleanupRef . current ? . ( ) ;
} ;
2026-03-29 02:45:59 -07:00
} , [ ] ) ;
2025-12-25 14:19:22 +05:30
2026-04-08 05:21:39 +05:30
// Store viewport element reference on mount
useEffect ( ( ) = > {
viewportRef . current = document . querySelector ( ".aui-thread-viewport" ) ;
} , [ ] ) ;
// Compute picker positions using ResizeObserver to avoid layout reads during render
useLayoutEffect ( ( ) = > {
if ( ! editorContainerRef . current ) return ;
const updatePosition = ( ) = > {
if ( ! editorContainerRef . current ) return ;
const rect = editorContainerRef . current . getBoundingClientRect ( ) ;
const composerRect = composerBoxRef . current ? . getBoundingClientRect ( ) ;
setContainerPos ( {
bottom : ` ${ window . innerHeight - rect . top + 8 } px ` ,
left : ` ${ rect . left } px ` ,
top : composerRect ? ` ${ composerRect . bottom + 8 } px ` : "auto" ,
} ) ;
} ;
updatePosition ( ) ;
const ro = new ResizeObserver ( updatePosition ) ;
ro . observe ( editorContainerRef . current ) ;
if ( composerBoxRef . current ) {
ro . observe ( composerBoxRef . current ) ;
}
return ( ) = > ro . disconnect ( ) ;
} , [ ] ) ;
2026-04-07 00:43:40 -07:00
const electronAPI = useElectronAPI ( ) ;
2026-03-29 00:45:11 +02:00
const [ clipboardInitialText , setClipboardInitialText ] = useState < string | undefined > ( ) ;
const clipboardLoadedRef = useRef ( false ) ;
2026-03-24 19:28:41 +02:00
useEffect ( ( ) = > {
2026-04-07 00:43:40 -07:00
if ( ! electronAPI || clipboardLoadedRef . current ) return ;
2026-03-29 00:45:11 +02:00
clipboardLoadedRef . current = true ;
2026-04-07 00:43:40 -07:00
electronAPI . getQuickAskText ( ) . then ( ( text ) = > {
2026-03-27 20:07:55 +02:00
if ( text ) {
2026-03-29 00:45:11 +02:00
setClipboardInitialText ( text ) ;
2026-03-27 20:07:55 +02:00
}
2026-03-24 19:28:41 +02:00
} ) ;
2026-04-07 00:43:40 -07:00
} , [ electronAPI ] ) ;
2026-03-24 19:28:41 +02:00
2026-03-24 02:22:51 +05:30
const isThreadEmpty = useAuiState ( ( { thread } ) = > thread . isEmpty ) ;
const isThreadRunning = useAuiState ( ( { thread } ) = > thread . isRunning ) ;
2025-12-25 17:52:48 +05:30
2026-04-07 02:49:24 -07:00
const currentPlaceholder = COMPOSER_PLACEHOLDER ;
2026-02-01 17:26:50 -05:00
2026-01-20 19:48:28 +02:00
// Live collaboration state
2026-01-20 18:39:50 +02:00
const { data : currentUser } = useAtomValue ( currentUserAtom ) ;
const { data : members } = useAtomValue ( membersAtom ) ;
const threadId = useMemo ( ( ) = > {
if ( Array . isArray ( chat_id ) && chat_id . length > 0 ) {
return Number . parseInt ( chat_id [ 0 ] , 10 ) || null ;
}
return typeof chat_id === "string" ? Number . parseInt ( chat_id , 10 ) || null : null ;
} , [ chat_id ] ) ;
2026-01-22 19:04:23 +02:00
const sessionState = useAtomValue ( chatSessionStateAtom ) ;
const isAiResponding = sessionState ? . isAiResponding ? ? false ;
const respondingToUserId = sessionState ? . respondingToUserId ? ? null ;
2026-01-20 18:39:50 +02:00
const isBlockedByOtherUser = isAiResponding && respondingToUserId !== currentUser ? . id ;
2026-03-23 19:29:08 +02:00
// Sync comments for the entire thread via Zero (one subscription per thread)
useCommentsSync ( threadId ) ;
2026-01-22 17:57:20 +02:00
2026-02-27 17:19:25 -08:00
// Batch-prefetch comments for all assistant messages so individual useComments
// hooks never fire their own network requests (eliminates N+1 API calls).
// Return a primitive string from the selector so useSyncExternalStore can
// compare snapshots by value and avoid infinite re-render loops.
2026-03-24 02:22:51 +05:30
const assistantIdsKey = useAuiState ( ( { thread } ) = >
2026-02-27 17:19:25 -08:00
thread . messages
. filter ( ( m ) = > m . role === "assistant" && m . id ? . startsWith ( "msg-" ) )
2026-03-06 15:59:45 +05:30
. map ( ( m ) = > m . id ? . replace ( "msg-" , "" ) )
2026-02-27 17:19:25 -08:00
. join ( "," )
) ;
const assistantDbMessageIds = useMemo (
( ) = > ( assistantIdsKey ? assistantIdsKey . split ( "," ) . map ( Number ) : [ ] ) ,
[ assistantIdsKey ]
) ;
useBatchCommentsPreload ( assistantDbMessageIds ) ;
2026-01-18 20:13:51 +05:30
// Auto-focus editor on new chat page after mount
2025-12-25 14:19:22 +05:30
useEffect ( ( ) = > {
if ( isThreadEmpty && ! hasAutoFocusedRef . current && editorRef . current ) {
const timeoutId = setTimeout ( ( ) = > {
editorRef . current ? . focus ( ) ;
hasAutoFocusedRef . current = true ;
} , 100 ) ;
return ( ) = > clearTimeout ( timeoutId ) ;
}
} , [ isThreadEmpty ] ) ;
2026-03-10 15:40:17 +05:30
// Close document picker when a slide-out panel (inbox, shared/private chats) opens
useEffect ( ( ) = > {
const handler = ( ) = > {
setShowDocumentPopover ( false ) ;
setMentionQuery ( "" ) ;
} ;
window . addEventListener ( SLIDEOUT_PANEL_OPENED_EVENT , handler ) ;
return ( ) = > window . removeEventListener ( SLIDEOUT_PANEL_OPENED_EVENT , handler ) ;
} , [ ] ) ;
2026-01-18 20:13:51 +05:30
// Sync editor text with assistant-ui composer runtime
2025-12-25 13:44:18 +05:30
const handleEditorChange = useCallback (
( text : string ) = > {
2026-03-24 02:22:51 +05:30
aui . composer ( ) . setText ( text ) ;
2025-12-25 13:44:18 +05:30
} ,
2026-03-24 02:22:51 +05:30
[ aui ]
2025-12-25 13:44:18 +05:30
) ;
2025-12-23 15:13:03 +05:30
2026-01-18 20:13:51 +05:30
// Open document picker when @ mention is triggered
2025-12-25 13:44:18 +05:30
const handleMentionTrigger = useCallback ( ( query : string ) = > {
setShowDocumentPopover ( true ) ;
setMentionQuery ( query ) ;
} , [ ] ) ;
2025-12-22 23:17:48 +02:00
2026-01-18 20:13:51 +05:30
// Close document picker and reset query
2025-12-25 13:44:18 +05:30
const handleMentionClose = useCallback ( ( ) = > {
if ( showDocumentPopover ) {
setShowDocumentPopover ( false ) ;
2025-12-23 15:13:03 +05:30
setMentionQuery ( "" ) ;
2025-12-23 14:24:36 +05:30
}
2025-12-25 13:44:18 +05:30
} , [ showDocumentPopover ] ) ;
2025-12-23 14:24:36 +05:30
2026-03-28 23:20:10 +02:00
// Open action picker when / is triggered
const handleActionTrigger = useCallback ( ( query : string ) = > {
2026-03-29 00:07:08 +02:00
setShowPromptPicker ( true ) ;
2026-03-28 23:20:10 +02:00
setActionQuery ( query ) ;
} , [ ] ) ;
// Close action picker and reset query
const handleActionClose = useCallback ( ( ) = > {
2026-03-29 00:07:08 +02:00
if ( showPromptPicker ) {
setShowPromptPicker ( false ) ;
2026-03-28 23:20:10 +02:00
setActionQuery ( "" ) ;
}
2026-03-29 00:07:08 +02:00
} , [ showPromptPicker ] ) ;
2026-03-28 23:20:10 +02:00
const handleActionSelect = useCallback (
( action : { name : string ; prompt : string ; mode : "transform" | "explore" } ) = > {
2026-03-29 02:54:48 +02:00
let userText = editorRef . current ? . getText ( ) ? ? "" ;
const trigger = ` / ${ actionQuery } ` ;
if ( userText . endsWith ( trigger ) ) {
userText = userText . slice ( 0 , - trigger . length ) . trimEnd ( ) ;
}
const finalPrompt = action . prompt . includes ( "{selection}" )
? action . prompt . replace ( "{selection}" , ( ) = > userText )
2026-03-30 01:50:41 +05:30
: userText
? ` ${ action . prompt } \ n \ n ${ userText } `
: action . prompt ;
2026-04-07 00:43:40 -07:00
editorRef . current ? . setText ( finalPrompt ) ;
2026-03-29 02:54:48 +02:00
aui . composer ( ) . setText ( finalPrompt ) ;
2026-03-29 00:07:08 +02:00
setShowPromptPicker ( false ) ;
2026-03-28 23:20:10 +02:00
setActionQuery ( "" ) ;
2026-03-29 02:54:48 +02:00
} ,
2026-04-07 00:43:40 -07:00
[ actionQuery , aui ]
2026-03-29 02:54:48 +02:00
) ;
2026-03-29 00:45:11 +02:00
2026-03-29 02:54:48 +02:00
const handleQuickAskSelect = useCallback (
( action : { name : string ; prompt : string ; mode : "transform" | "explore" } ) = > {
if ( ! clipboardInitialText ) return ;
2026-04-07 00:43:40 -07:00
electronAPI ? . setQuickAskMode ( action . mode ) ;
2026-03-29 02:54:48 +02:00
const finalPrompt = action . prompt . includes ( "{selection}" )
? action . prompt . replace ( "{selection}" , ( ) = > clipboardInitialText )
: ` ${ action . prompt } \ n \ n ${ clipboardInitialText } ` ;
2026-04-07 00:43:40 -07:00
editorRef . current ? . setText ( finalPrompt ) ;
2026-03-29 02:54:48 +02:00
aui . composer ( ) . setText ( finalPrompt ) ;
setShowPromptPicker ( false ) ;
setActionQuery ( "" ) ;
setClipboardInitialText ( undefined ) ;
2026-03-28 23:20:10 +02:00
} ,
2026-04-07 00:43:40 -07:00
[ clipboardInitialText , electronAPI , aui ]
2026-03-28 23:20:10 +02:00
) ;
// Keyboard navigation for document/action picker (arrow keys, Enter, Escape)
2025-12-25 13:44:18 +05:30
const handleKeyDown = useCallback (
( e : React.KeyboardEvent ) = > {
2026-03-29 00:07:08 +02:00
if ( showPromptPicker ) {
2026-03-28 23:20:10 +02:00
if ( e . key === "ArrowDown" ) {
e . preventDefault ( ) ;
2026-03-29 00:07:08 +02:00
promptPickerRef . current ? . moveDown ( ) ;
2026-03-28 23:20:10 +02:00
return ;
}
if ( e . key === "ArrowUp" ) {
e . preventDefault ( ) ;
2026-03-29 00:07:08 +02:00
promptPickerRef . current ? . moveUp ( ) ;
2026-03-28 23:20:10 +02:00
return ;
}
if ( e . key === "Enter" ) {
e . preventDefault ( ) ;
2026-03-29 00:07:08 +02:00
promptPickerRef . current ? . selectHighlighted ( ) ;
2026-03-28 23:20:10 +02:00
return ;
}
if ( e . key === "Escape" ) {
e . preventDefault ( ) ;
2026-03-29 00:07:08 +02:00
setShowPromptPicker ( false ) ;
2026-03-28 23:20:10 +02:00
setActionQuery ( "" ) ;
return ;
}
}
2025-12-23 15:13:03 +05:30
if ( showDocumentPopover ) {
2025-12-25 13:44:18 +05:30
if ( e . key === "ArrowDown" ) {
e . preventDefault ( ) ;
documentPickerRef . current ? . moveDown ( ) ;
return ;
}
if ( e . key === "ArrowUp" ) {
e . preventDefault ( ) ;
documentPickerRef . current ? . moveUp ( ) ;
return ;
}
if ( e . key === "Enter" ) {
e . preventDefault ( ) ;
documentPickerRef . current ? . selectHighlighted ( ) ;
return ;
}
if ( e . key === "Escape" ) {
e . preventDefault ( ) ;
setShowDocumentPopover ( false ) ;
setMentionQuery ( "" ) ;
return ;
}
2025-12-23 15:13:03 +05:30
}
2025-12-25 13:44:18 +05:30
} ,
2026-03-29 00:07:08 +02:00
[ showDocumentPopover , showPromptPicker ]
2025-12-25 13:44:18 +05:30
) ;
2025-12-23 14:24:36 +05:30
2026-01-20 18:39:50 +02:00
// Submit message (blocked during streaming, document picker open, or AI responding to another user)
2025-12-25 13:44:18 +05:30
const handleSubmit = useCallback ( ( ) = > {
2026-03-06 14:40:10 +05:30
if ( isThreadRunning || isBlockedByOtherUser ) {
2025-12-25 17:52:48 +05:30
return ;
}
2026-03-29 00:07:08 +02:00
if ( ! showDocumentPopover && ! showPromptPicker ) {
2026-03-29 02:54:48 +02:00
if ( clipboardInitialText ) {
2026-03-28 23:45:11 +02:00
const userText = editorRef . current ? . getText ( ) ? ? "" ;
2026-03-30 01:50:41 +05:30
const combined = userText ? ` ${ userText } \ n \ n ${ clipboardInitialText } ` : clipboardInitialText ;
2026-03-29 02:54:48 +02:00
aui . composer ( ) . setText ( combined ) ;
setClipboardInitialText ( undefined ) ;
2026-03-28 23:45:11 +02:00
}
2026-03-24 02:22:51 +05:30
aui . composer ( ) . send ( ) ;
2025-12-25 13:44:18 +05:30
editorRef . current ? . clear ( ) ;
setMentionedDocuments ( [ ] ) ;
2026-03-06 15:59:45 +05:30
setSidebarDocs ( [ ] ) ;
2025-12-22 23:17:48 +02:00
}
2026-03-29 02:45:59 -07:00
if ( isThreadRunning || isBlockedByOtherUser ) return ;
if ( showDocumentPopover ) return ;
2026-04-08 05:21:39 +05:30
const viewportEl = viewportRef . current ;
2026-03-29 02:45:59 -07:00
const heightBefore = viewportEl ? . scrollHeight ? ? 0 ;
aui . composer ( ) . send ( ) ;
editorRef . current ? . clear ( ) ;
setMentionedDocuments ( [ ] ) ;
setSidebarDocs ( [ ] ) ;
// With turnAnchor="top", ViewportSlack adds min-height to the last
// assistant message so that scrolling-to-bottom actually positions the
// user message at the TOP of the viewport. That slack height is
// calculated asynchronously (ResizeObserver → style → layout).
//
// We poll via rAF for ~2 s, re-scrolling whenever scrollHeight changes
// (user msg render → assistant placeholder → ViewportSlack min-height →
// first streamed content). Backup setTimeout calls cover cases where
// the batcher's 50 ms throttle delays the DOM update past the rAF.
const scrollToBottom = ( ) = >
threadViewportStore . getState ( ) . scrollToBottom ( { behavior : "instant" } ) ;
let lastHeight = heightBefore ;
let frames = 0 ;
let cancelled = false ;
const POLL_FRAMES = 120 ;
const pollAndScroll = ( ) = > {
if ( cancelled ) return ;
2026-04-08 05:21:39 +05:30
const el = viewportRef . current ;
2026-03-29 02:45:59 -07:00
if ( el ) {
const h = el . scrollHeight ;
if ( h !== lastHeight ) {
lastHeight = h ;
scrollToBottom ( ) ;
}
}
if ( ++ frames < POLL_FRAMES ) {
requestAnimationFrame ( pollAndScroll ) ;
}
} ;
requestAnimationFrame ( pollAndScroll ) ;
const t1 = setTimeout ( scrollToBottom , 100 ) ;
const t2 = setTimeout ( scrollToBottom , 300 ) ;
const t3 = setTimeout ( scrollToBottom , 600 ) ;
// Cleanup if component unmounts during the polling window. The ref is
// checked inside pollAndScroll; timeouts are cleared in the return below.
// Store cleanup fn so it can be called from a useEffect cleanup if needed.
submitCleanupRef . current = ( ) = > {
cancelled = true ;
clearTimeout ( t1 ) ;
clearTimeout ( t2 ) ;
clearTimeout ( t3 ) ;
} ;
2025-12-25 19:32:18 +05:30
} , [
showDocumentPopover ,
2026-03-29 00:07:08 +02:00
showPromptPicker ,
2025-12-25 19:32:18 +05:30
isThreadRunning ,
2026-01-20 18:39:50 +02:00
isBlockedByOtherUser ,
2026-03-29 02:54:48 +02:00
clipboardInitialText ,
2026-03-24 02:22:51 +05:30
aui ,
2025-12-25 19:32:18 +05:30
setMentionedDocuments ,
2026-03-06 15:59:45 +05:30
setSidebarDocs ,
2026-03-29 02:45:59 -07:00
threadViewportStore ,
2025-12-25 19:32:18 +05:30
] ) ;
2025-12-25 13:44:18 +05:30
const handleDocumentRemove = useCallback (
2026-01-13 06:14:58 +02:00
( docId : number , docType? : string ) = > {
2026-03-06 23:33:51 +05:30
setMentionedDocuments ( ( prev ) = >
prev . filter ( ( doc ) = > ! ( doc . id === docId && doc . document_type === docType ) )
) ;
2025-12-25 13:44:18 +05:30
} ,
2026-03-06 23:33:51 +05:30
[ setMentionedDocuments ]
2025-12-25 13:44:18 +05:30
) ;
2025-12-22 23:17:48 +02:00
2025-12-25 13:44:18 +05:30
const handleDocumentsMention = useCallback (
2026-01-13 06:14:58 +02:00
( documents : Pick < Document , "id" | "title" | "document_type" > [ ] ) = > {
2026-01-13 01:45:58 -08:00
const existingKeys = new Set ( mentionedDocuments . map ( ( d ) = > ` ${ d . document_type } : ${ d . id } ` ) ) ;
2026-01-13 06:14:58 +02:00
const newDocs = documents . filter (
( doc ) = > ! existingKeys . has ( ` ${ doc . document_type } : ${ doc . id } ` )
) ;
2025-12-23 14:24:36 +05:30
2025-12-25 13:44:18 +05:30
for ( const doc of newDocs ) {
editorRef . current ? . insertDocumentChip ( doc ) ;
2025-12-22 23:17:48 +02:00
}
2025-12-24 07:06:35 +02:00
2025-12-25 13:44:18 +05:30
setMentionedDocuments ( ( prev ) = > {
2026-01-13 06:14:58 +02:00
const existingKeySet = new Set ( prev . map ( ( d ) = > ` ${ d . document_type } : ${ d . id } ` ) ) ;
const uniqueNewDocs = documents . filter (
( doc ) = > ! existingKeySet . has ( ` ${ doc . document_type } : ${ doc . id } ` )
) ;
2026-03-06 23:33:51 +05:30
return [ . . . prev , . . . uniqueNewDocs ] ;
2025-12-25 13:44:18 +05:30
} ) ;
2025-12-23 14:24:36 +05:30
2025-12-25 13:44:18 +05:30
setMentionQuery ( "" ) ;
} ,
2026-03-06 23:33:51 +05:30
[ mentionedDocuments , setMentionedDocuments ]
2025-12-25 13:44:18 +05:30
) ;
2025-12-22 23:17:48 +02:00
2025-12-19 16:42:58 +02:00
return (
2026-03-29 02:54:48 +02:00
< ComposerPrimitive.Root
className = "aui-composer-root relative flex w-full flex-col gap-2"
2026-03-30 01:50:41 +05:30
style = { showPromptPicker && clipboardInitialText ? { marginBottom : 220 } : undefined }
2026-03-29 02:54:48 +02:00
>
2026-01-20 18:39:50 +02:00
< ChatSessionStatus
isAiResponding = { isAiResponding }
respondingToUserId = { respondingToUserId }
currentUserId = { currentUser ? . id ? ? null }
members = { members ? ? [ ] }
/ >
2026-03-30 01:50:41 +05:30
< div
ref = { composerBoxRef }
className = "aui-composer-attachment-dropzone flex w-full flex-col overflow-hidden rounded-2xl border-input bg-muted pt-2 outline-none transition-shadow"
>
2026-03-29 02:54:48 +02:00
{ clipboardInitialText && (
< ClipboardChip
text = { clipboardInitialText }
onDismiss = { ( ) = > setClipboardInitialText ( undefined ) }
/ >
) }
2026-03-10 16:17:12 +05:30
{ /* Inline editor with @mention support */ }
< div ref = { editorContainerRef } className = "aui-composer-input-wrapper px-4 pt-3 pb-6" >
2025-12-25 13:44:18 +05:30
< InlineMentionEditor
ref = { editorRef }
2026-02-01 17:26:50 -05:00
placeholder = { currentPlaceholder }
2025-12-25 13:44:18 +05:30
onMentionTrigger = { handleMentionTrigger }
onMentionClose = { handleMentionClose }
2026-03-28 23:20:10 +02:00
onActionTrigger = { handleActionTrigger }
onActionClose = { handleActionClose }
2025-12-25 13:44:18 +05:30
onChange = { handleEditorChange }
onDocumentRemove = { handleDocumentRemove }
onSubmit = { handleSubmit }
2025-12-23 14:24:36 +05:30
onKeyDown = { handleKeyDown }
2025-12-25 13:44:18 +05:30
className = "min-h-[24px]"
2025-12-23 14:24:36 +05:30
/ >
< / div >
2026-01-18 20:13:51 +05:30
{ /* Document picker popover (portal to body for proper z-index stacking) */ }
2025-12-24 07:06:35 +02:00
{ showDocumentPopover &&
typeof document !== "undefined" &&
createPortal (
2026-01-18 19:43:46 +05:30
< DocumentMentionPicker
ref = { documentPickerRef }
searchSpaceId = { Number ( search_space_id ) }
onSelectionChange = { handleDocumentsMention }
onDone = { ( ) = > {
setShowDocumentPopover ( false ) ;
setMentionQuery ( "" ) ;
} }
initialSelectedDocuments = { mentionedDocuments }
externalSearch = { mentionQuery }
containerStyle = { {
2026-04-08 05:21:39 +05:30
bottom : containerPos.bottom ,
left : containerPos.left ,
2026-01-17 23:36:37 +05:30
} }
2026-01-18 19:43:46 +05:30
/ > ,
2025-12-24 07:06:35 +02:00
document . body
) }
2026-03-29 00:07:08 +02:00
{ showPromptPicker &&
2026-03-28 23:20:10 +02:00
typeof document !== "undefined" &&
createPortal (
2026-03-29 00:07:08 +02:00
< PromptPicker
ref = { promptPickerRef }
2026-03-29 02:54:48 +02:00
onSelect = { clipboardInitialText ? handleQuickAskSelect : handleActionSelect }
2026-03-28 23:20:10 +02:00
onDone = { ( ) = > {
2026-03-29 00:07:08 +02:00
setShowPromptPicker ( false ) ;
2026-03-28 23:20:10 +02:00
setActionQuery ( "" ) ;
} }
externalSearch = { actionQuery }
containerStyle = { {
position : "fixed" ,
2026-04-08 05:21:39 +05:30
. . . ( clipboardInitialText
? { top : containerPos.top }
: { bottom : containerPos.bottom } ) ,
left : containerPos.left ,
2026-03-28 23:20:10 +02:00
zIndex : 50 ,
} }
/ > ,
document . body
) }
2026-03-07 04:46:48 +05:30
< ComposerAction isBlockedByOtherUser = { isBlockedByOtherUser } / >
2026-03-10 17:36:26 -07:00
< ConnectorIndicator showTrigger = { false } / >
2026-03-21 21:03:23 +05:30
< ConnectToolsBanner isThreadEmpty = { isThreadEmpty } / >
2026-02-09 16:46:54 -08:00
< / div >
2025-12-19 16:42:58 +02:00
< / ComposerPrimitive.Root >
) ;
} ;
2026-01-20 19:48:28 +02:00
interface ComposerActionProps {
isBlockedByOtherUser? : boolean ;
}
2026-03-07 04:46:48 +05:30
const ComposerAction : FC < ComposerActionProps > = ( { isBlockedByOtherUser = false } ) = > {
2026-02-09 16:46:54 -08:00
const mentionedDocuments = useAtomValue ( mentionedDocumentsAtom ) ;
2026-03-06 15:59:45 +05:30
const sidebarDocs = useAtomValue ( sidebarSelectedDocumentsAtom ) ;
2026-03-06 14:40:10 +05:30
const setDocumentsSidebarOpen = useSetAtom ( documentsSidebarOpenAtom ) ;
2026-03-11 12:04:22 +05:30
const setConnectorDialogOpen = useSetAtom ( connectorDialogOpenAtom ) ;
2026-03-10 17:36:26 -07:00
const [ toolsPopoverOpen , setToolsPopoverOpen ] = useState ( false ) ;
2026-03-11 12:04:22 +05:30
const isDesktop = useMediaQuery ( "(min-width: 640px)" ) ;
2026-03-15 16:27:33 +05:30
const { openDialog : openUploadDialog } = useDocumentUploadDialog ( ) ;
2026-03-11 12:04:22 +05:30
const [ toolsScrollPos , setToolsScrollPos ] = useState < "top" | "middle" | "bottom" > ( "top" ) ;
2026-04-02 23:21:57 -07:00
const toolsRafRef = useRef < number > ( ) ;
2026-03-11 12:04:22 +05:30
const handleToolsScroll = useCallback ( ( e : React.UIEvent < HTMLDivElement > ) = > {
const el = e . currentTarget ;
2026-04-02 23:21:57 -07:00
if ( toolsRafRef . current ) return ;
toolsRafRef . current = requestAnimationFrame ( ( ) = > {
const atTop = el . scrollTop <= 2 ;
const atBottom = el . scrollHeight - el . scrollTop - el . clientHeight <= 2 ;
setToolsScrollPos ( atTop ? "top" : atBottom ? "bottom" : "middle" ) ;
toolsRafRef . current = undefined ;
} ) ;
2026-03-11 12:04:22 +05:30
} , [ ] ) ;
2026-04-02 23:43:19 -07:00
useEffect (
( ) = > ( ) = > {
if ( toolsRafRef . current ) cancelAnimationFrame ( toolsRafRef . current ) ;
} ,
[ ]
) ;
2026-03-24 02:22:51 +05:30
const isComposerTextEmpty = useAuiState ( ( { composer } ) = > {
2025-12-22 18:38:08 +05:30
const text = composer . text ? . trim ( ) || "" ;
return text . length === 0 ;
} ) ;
2026-02-09 16:46:54 -08:00
const isComposerEmpty = isComposerTextEmpty && mentionedDocuments . length === 0 ;
2025-12-22 18:38:08 +05:30
2025-12-23 01:16:25 -08:00
const { data : userConfigs } = useAtomValue ( newLLMConfigsAtom ) ;
const { data : globalConfigs } = useAtomValue ( globalNewLLMConfigsAtom ) ;
const { data : preferences } = useAtomValue ( llmPreferencesAtom ) ;
2026-03-10 17:36:26 -07:00
const { data : agentTools } = useAtomValue ( agentToolsAtom ) ;
const disabledTools = useAtomValue ( disabledToolsAtom ) ;
2026-04-01 23:09:57 +00:00
const disabledToolsSet = useMemo ( ( ) = > new Set ( disabledTools ) , [ disabledTools ] ) ;
2026-03-10 17:36:26 -07:00
const toggleTool = useSetAtom ( toggleToolAtom ) ;
2026-03-21 11:38:42 +05:30
const setDisabledTools = useSetAtom ( disabledToolsAtom ) ;
2026-03-10 17:36:26 -07:00
const hydrateDisabled = useSetAtom ( hydrateDisabledToolsAtom ) ;
2026-03-17 01:09:15 +05:30
2026-03-21 12:41:06 +05:30
const { data : connectors } = useAtomValue ( connectorsAtom ) ;
const connectedTypes = useMemo (
( ) = > new Set < string > ( ( connectors ? ? [ ] ) . map ( ( c ) = > c . connector_type ) ) ,
[ connectors ]
) ;
2026-03-21 11:38:42 +05:30
const toggleToolGroup = useCallback (
( toolNames : string [ ] ) = > {
2026-04-01 23:09:57 +00:00
const allDisabled = toolNames . every ( ( name ) = > disabledToolsSet . has ( name ) ) ;
2026-03-21 11:38:42 +05:30
if ( allDisabled ) {
setDisabledTools ( ( prev ) = > prev . filter ( ( t ) = > ! toolNames . includes ( t ) ) ) ;
} else {
setDisabledTools ( ( prev ) = > [ . . . new Set ( [ . . . prev , . . . toolNames ] ) ] ) ;
}
} ,
2026-04-01 23:09:57 +00:00
[ disabledToolsSet , setDisabledTools ]
2026-03-21 11:38:42 +05:30
) ;
2026-03-17 01:09:15 +05:30
const hasWebSearchTool = agentTools ? . some ( ( t ) = > t . name === "web_search" ) ? ? false ;
2026-04-01 23:09:57 +00:00
const isWebSearchEnabled = hasWebSearchTool && ! disabledToolsSet . has ( "web_search" ) ;
2026-03-17 01:09:15 +05:30
const filteredTools = useMemo (
( ) = > agentTools ? . filter ( ( t ) = > t . name !== "web_search" ) ,
[ agentTools ]
) ;
2026-03-17 15:18:58 +05:30
const groupedTools = useMemo ( ( ) = > {
if ( ! filteredTools ) return [ ] ;
const toolsByName = new Map ( filteredTools . map ( ( t ) = > [ t . name , t ] ) ) ;
2026-03-21 11:38:42 +05:30
const result : { label : string ; tools : typeof filteredTools ; connectorIcon? : string } [ ] = [ ] ;
2026-03-17 15:18:58 +05:30
const placed = new Set < string > ( ) ;
for ( const group of TOOL_GROUPS ) {
2026-03-21 12:41:06 +05:30
if ( group . connectorIcon ) {
const requiredTypes = CONNECTOR_ICON_TO_TYPES [ group . connectorIcon ] ;
const isConnected = requiredTypes ? . some ( ( t ) = > connectedTypes . has ( t ) ) ;
if ( ! isConnected ) {
for ( const name of group . tools ) placed . add ( name ) ;
continue ;
}
}
2026-03-17 15:18:58 +05:30
const matched = group . tools . flatMap ( ( name ) = > {
const tool = toolsByName . get ( name ) ;
if ( ! tool ) return [ ] ;
placed . add ( name ) ;
return [ tool ] ;
} ) ;
if ( matched . length > 0 ) {
2026-03-21 11:38:42 +05:30
result . push ( { label : group.label , tools : matched , connectorIcon : group.connectorIcon } ) ;
2026-03-17 15:18:58 +05:30
}
}
const ungrouped = filteredTools . filter ( ( t ) = > ! placed . has ( t . name ) ) ;
if ( ungrouped . length > 0 ) {
result . push ( { label : "Other" , tools : ungrouped } ) ;
}
return result ;
2026-03-21 12:41:06 +05:30
} , [ filteredTools , connectedTypes ] ) ;
2026-03-17 15:18:58 +05:30
2026-03-10 17:36:26 -07:00
useEffect ( ( ) = > {
hydrateDisabled ( ) ;
} , [ hydrateDisabled ] ) ;
2025-12-23 01:16:25 -08:00
const hasModelConfigured = useMemo ( ( ) = > {
if ( ! preferences ) return false ;
const agentLlmId = preferences . agent_llm_id ;
if ( agentLlmId === null || agentLlmId === undefined ) return false ;
2026-01-29 15:28:31 -08:00
if ( agentLlmId <= 0 ) {
2025-12-23 01:16:25 -08:00
return globalConfigs ? . some ( ( c ) = > c . id === agentLlmId ) ? ? false ;
}
return userConfigs ? . some ( ( c ) = > c . id === agentLlmId ) ? ? false ;
} , [ preferences , globalConfigs , userConfigs ] ) ;
2026-03-07 04:46:48 +05:30
const isSendDisabled = isComposerEmpty || ! hasModelConfigured || isBlockedByOtherUser ;
2025-12-22 18:38:08 +05:30
2025-12-19 16:42:58 +02:00
return (
2026-03-10 16:16:24 +05:30
< div className = "aui-composer-action-wrapper relative mx-3 mb-2 flex items-center justify-between" >
2025-12-22 23:57:16 +05:30
< div className = "flex items-center gap-1" >
2026-03-15 16:27:33 +05:30
{ ! isDesktop ? (
< >
< DropdownMenu >
2026-03-15 16:39:56 +05:30
< DropdownMenuTrigger asChild >
< Button
variant = "ghost"
size = "icon"
className = "size-[34px] rounded-full p-1 font-semibold text-xs hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30"
aria - label = "More actions"
data - joyride = "connector-icon"
>
< Plus className = "size-4" / >
< / Button >
< / DropdownMenuTrigger >
2026-03-17 03:36:32 +05:30
< DropdownMenuContent side = "bottom" align = "start" sideOffset = { 8 } >
2026-03-15 16:27:33 +05:30
< DropdownMenuItem onSelect = { ( ) = > setToolsPopoverOpen ( true ) } >
2026-03-15 16:39:56 +05:30
< Settings2 className = "size-4" / >
2026-03-15 16:27:33 +05:30
Manage Tools
< / DropdownMenuItem >
< DropdownMenuItem onSelect = { ( ) = > openUploadDialog ( ) } >
< Upload className = "size-4" / >
Upload Files
< / DropdownMenuItem >
< / DropdownMenuContent >
< / DropdownMenu >
2026-03-15 16:39:56 +05:30
< Drawer open = { toolsPopoverOpen } onOpenChange = { setToolsPopoverOpen } >
< DrawerContent className = "max-h-[60dvh]" >
< DrawerHandle / >
2026-03-30 23:56:46 +05:30
< div className = "px-4 py-2" >
< DrawerTitle className = "text-sm font-medium" > Manage Tools < / DrawerTitle >
2026-03-15 16:39:56 +05:30
< / div >
< div className = "overflow-y-auto pb-6" onScroll = { handleToolsScroll } >
2026-03-21 13:20:13 +05:30
{ groupedTools
. filter ( ( g ) = > ! g . connectorIcon )
. map ( ( group ) = > (
< div key = { group . label } >
< div className = "px-4 pt-3 pb-1 text-xs text-muted-foreground/80 font-medium select-none" >
{ group . label }
< / div >
{ group . tools . map ( ( tool ) = > {
2026-04-01 23:09:57 +00:00
const isDisabled = disabledToolsSet . has ( tool . name ) ;
2026-03-21 13:20:13 +05:30
const ToolIcon = getToolIcon ( tool . name ) ;
return (
< div
key = { tool . name }
className = "flex w-full items-center gap-3 px-4 py-2 hover:bg-muted-foreground/10 transition-colors"
>
< ToolIcon className = "size-4 shrink-0 text-muted-foreground" / >
< span className = "flex-1 min-w-0 text-sm font-medium truncate" >
{ formatToolName ( tool . name ) }
< / span >
< Switch
checked = { ! isDisabled }
onCheckedChange = { ( ) = > toggleTool ( tool . name ) }
className = "shrink-0"
/ >
< / div >
) ;
} ) }
2026-03-15 16:39:56 +05:30
< / div >
2026-03-21 13:20:13 +05:30
) ) }
2026-03-21 11:38:42 +05:30
{ groupedTools . some ( ( g ) = > g . connectorIcon ) && (
< div >
2026-03-17 15:19:16 +05:30
< div className = "px-4 pt-3 pb-1 text-xs text-muted-foreground/80 font-medium select-none" >
2026-03-21 11:38:42 +05:30
Connector Actions
2026-03-17 15:19:16 +05:30
< / div >
2026-03-21 13:20:13 +05:30
{ groupedTools
. filter ( ( g ) = > g . connectorIcon )
. map ( ( group ) = > {
const iconKey = group . connectorIcon ? ? "" ;
const iconInfo = CONNECTOR_TOOL_ICON_PATHS [ iconKey ] ;
const toolNames = group . tools . map ( ( t ) = > t . name ) ;
2026-04-01 23:09:57 +00:00
const allDisabled = toolNames . every ( ( n ) = > disabledToolsSet . has ( n ) ) ;
2026-03-21 13:20:13 +05:30
return (
< div
key = { group . label }
className = "flex w-full items-center gap-3 px-4 py-2 hover:bg-muted-foreground/10 transition-colors"
>
{ iconInfo ? (
< Image
src = { iconInfo . src }
alt = { iconInfo . alt }
width = { 18 }
height = { 18 }
className = "size-[18px] shrink-0 select-none pointer-events-none"
draggable = { false }
/ >
) : (
< Wrench className = "size-4 shrink-0 text-muted-foreground" / >
) }
< span className = "flex-1 min-w-0 text-sm font-medium truncate" >
{ group . label }
< / span >
< Switch
checked = { ! allDisabled }
onCheckedChange = { ( ) = > toggleToolGroup ( toolNames ) }
className = "shrink-0"
2026-03-21 11:38:42 +05:30
/ >
2026-03-21 13:20:13 +05:30
< / div >
) ;
} ) }
2026-03-15 16:27:33 +05:30
< / div >
2026-03-21 11:38:42 +05:30
) }
2026-03-17 15:19:16 +05:30
{ ! filteredTools ? . length && (
2026-03-15 16:39:56 +05:30
< div className = "px-4 py-6 text-center text-sm text-muted-foreground" >
Loading tools . . .
2026-03-15 16:27:33 +05:30
< / div >
2026-03-15 16:39:56 +05:30
) }
< / div >
< / DrawerContent >
< / Drawer >
< Button
variant = "ghost"
size = "icon"
className = "size-[34px] rounded-full p-1 font-semibold text-xs hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30"
aria - label = "Manage connectors"
onClick = { ( ) = > setConnectorDialogOpen ( true ) }
>
< Unplug className = "size-4" / >
< / Button >
2026-03-15 16:27:33 +05:30
< / >
) : (
< Popover open = { toolsPopoverOpen } onOpenChange = { setToolsPopoverOpen } >
< PopoverTrigger asChild >
< TooltipIconButton
tooltip = "Manage tools"
side = "bottom"
2026-03-17 15:09:24 +05:30
disableTooltip = { toolsPopoverOpen }
2026-03-15 16:27:33 +05:30
variant = "ghost"
size = "icon"
className = "size-[34px] rounded-full p-1 font-semibold text-xs hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30"
aria - label = "Manage tools"
data - joyride = "connector-icon"
>
2026-03-15 16:39:56 +05:30
< Settings2 className = "size-4" / >
2026-03-15 16:27:33 +05:30
< / TooltipIconButton >
< / PopoverTrigger >
< PopoverContent
side = "bottom"
align = "start"
sideOffset = { 12 }
className = "w-[calc(100vw-2rem)] max-w-56 sm:max-w-72 sm:w-72 p-0 select-none"
onOpenAutoFocus = { ( e ) = > e . preventDefault ( ) }
>
2026-03-30 23:56:46 +05:30
< div className = "sr-only" > Manage Tools < / div >
2026-03-15 16:27:33 +05:30
< div
2026-04-01 23:00:45 +05:30
className = "max-h-48 sm:max-h-64 overflow-y-auto overscroll-none py-0.5 sm:py-1"
2026-03-15 16:27:33 +05:30
onScroll = { handleToolsScroll }
style = { {
maskImage : ` linear-gradient(to bottom, ${ toolsScrollPos === "top" ? "black" : "transparent" } , black 16px, black calc(100% - 16px), ${ toolsScrollPos === "bottom" ? "black" : "transparent" } ) ` ,
WebkitMaskImage : ` linear-gradient(to bottom, ${ toolsScrollPos === "top" ? "black" : "transparent" } , black 16px, black calc(100% - 16px), ${ toolsScrollPos === "bottom" ? "black" : "transparent" } ) ` ,
} }
>
2026-03-21 13:20:13 +05:30
{ groupedTools
. filter ( ( g ) = > ! g . connectorIcon )
. map ( ( group ) = > (
< div key = { group . label } >
< div className = "px-2.5 sm:px-3 pt-2 pb-0.5 text-[10px] sm:text-xs text-muted-foreground/80 font-normal select-none" >
{ group . label }
< / div >
{ group . tools . map ( ( tool ) = > {
2026-04-01 23:09:57 +00:00
const isDisabled = disabledToolsSet . has ( tool . name ) ;
2026-03-21 13:20:13 +05:30
const ToolIcon = getToolIcon ( tool . name ) ;
const row = (
< div className = "flex w-full items-center gap-2 sm:gap-3 px-2.5 sm:px-3 py-1 sm:py-1.5 hover:bg-muted-foreground/10 transition-colors" >
< ToolIcon className = "size-3.5 sm:size-4 shrink-0 text-muted-foreground" / >
< span className = "flex-1 min-w-0 text-xs sm:text-sm font-medium truncate" >
{ formatToolName ( tool . name ) }
< / span >
< Switch
checked = { ! isDisabled }
onCheckedChange = { ( ) = > toggleTool ( tool . name ) }
className = "shrink-0 scale-[0.6] sm:scale-75"
/ >
< / div >
) ;
return (
< Tooltip key = { tool . name } >
< TooltipTrigger asChild > { row } < / TooltipTrigger >
< TooltipContent side = "right" className = "max-w-64 text-xs" >
{ tool . description }
< / TooltipContent >
< / Tooltip >
) ;
} ) }
2026-03-17 15:19:16 +05:30
< / div >
2026-03-21 13:20:13 +05:30
) ) }
2026-03-21 11:38:42 +05:30
{ groupedTools . some ( ( g ) = > g . connectorIcon ) && (
< div >
2026-03-17 15:19:16 +05:30
< div className = "px-2.5 sm:px-3 pt-2 pb-0.5 text-[10px] sm:text-xs text-muted-foreground/80 font-normal select-none" >
2026-03-21 11:38:42 +05:30
Connector Actions
2026-03-15 16:27:33 +05:30
< / div >
2026-03-21 13:20:13 +05:30
{ groupedTools
. filter ( ( g ) = > g . connectorIcon )
. map ( ( group ) = > {
const iconKey = group . connectorIcon ? ? "" ;
const iconInfo = CONNECTOR_TOOL_ICON_PATHS [ iconKey ] ;
const toolNames = group . tools . map ( ( t ) = > t . name ) ;
2026-04-01 23:09:57 +00:00
const allDisabled = toolNames . every ( ( n ) = > disabledToolsSet . has ( n ) ) ;
2026-03-21 13:20:13 +05:30
const groupDef = TOOL_GROUPS . find ( ( g ) = > g . label === group . label ) ;
const row = (
< div className = "flex w-full items-center gap-2 sm:gap-3 px-2.5 sm:px-3 py-1 sm:py-1.5 hover:bg-muted-foreground/10 transition-colors" >
{ iconInfo ? (
< Image
src = { iconInfo . src }
alt = { iconInfo . alt }
width = { 16 }
height = { 16 }
className = "size-3.5 sm:size-4 shrink-0 select-none pointer-events-none"
draggable = { false }
/ >
) : (
< Wrench className = "size-3.5 sm:size-4 shrink-0 text-muted-foreground" / >
) }
< span className = "flex-1 min-w-0 text-xs sm:text-sm font-medium truncate" >
{ group . label }
< / span >
< Switch
checked = { ! allDisabled }
onCheckedChange = { ( ) = > toggleToolGroup ( toolNames ) }
className = "shrink-0 scale-[0.6] sm:scale-75"
2026-03-21 11:38:42 +05:30
/ >
2026-03-21 13:20:13 +05:30
< / div >
) ;
return (
< Tooltip key = { group . label } >
< TooltipTrigger asChild > { row } < / TooltipTrigger >
< TooltipContent side = "right" className = "max-w-72 text-xs" >
{ groupDef ? . tooltip ? ?
2026-04-03 17:28:12 +05:30
group . tools . flatMap ( ( t , i ) = >
i === 0
? [ t . description ]
: [ < Dot key = { i } className = "inline h-4 w-4" / > , t . description ]
) }
2026-03-21 13:20:13 +05:30
< / TooltipContent >
< / Tooltip >
) ;
} ) }
2026-03-17 15:19:16 +05:30
< / div >
2026-03-21 11:38:42 +05:30
) }
2026-03-17 15:19:16 +05:30
{ ! filteredTools ? . length && (
2026-03-15 16:27:33 +05:30
< div className = "px-3 py-4 text-center text-xs text-muted-foreground" >
Loading tools . . .
< / div >
) }
< / div >
< / PopoverContent >
< / Popover >
2026-03-11 12:04:22 +05:30
) }
2026-03-17 01:09:15 +05:30
{ hasWebSearchTool && (
< button
type = "button"
2026-03-28 05:08:20 -04:00
aria - label = { isWebSearchEnabled ? "Disable web search" : "Enable web search" }
aria - pressed = { isWebSearchEnabled }
2026-03-17 01:09:15 +05:30
onClick = { ( ) = > toggleTool ( "web_search" ) }
className = { cn (
2026-04-08 05:36:19 +05:30
"rounded-full transition-[background-color,border-color,color] flex items-center gap-1 px-2 py-1 border h-8 select-none" ,
2026-03-17 01:09:15 +05:30
isWebSearchEnabled
? "bg-sky-500/15 border-sky-500/60 text-sky-500"
: "bg-transparent border-transparent text-muted-foreground hover:text-foreground"
) }
>
< motion.div
2026-03-17 04:40:46 +05:30
animate = { {
rotate : isWebSearchEnabled ? 360 : 0 ,
scale : isWebSearchEnabled ? 1.1 : 1 ,
} }
whileHover = { {
rotate : isWebSearchEnabled ? 360 : 15 ,
scale : 1.1 ,
transition : { type : "spring" , stiffness : 300 , damping : 10 } ,
} }
2026-03-17 01:09:15 +05:30
transition = { { type : "spring" , stiffness : 260 , damping : 25 } }
>
< Globe className = "size-4" / >
< / motion.div >
< AnimatePresence >
{ isWebSearchEnabled && (
< motion.span
initial = { { width : 0 , opacity : 0 } }
animate = { { width : "auto" , opacity : 1 } }
exit = { { width : 0 , opacity : 0 } }
transition = { { duration : 0.2 } }
className = "text-xs overflow-hidden whitespace-nowrap"
>
Search
< / motion.span >
) }
< / AnimatePresence >
< / button >
) }
2026-03-10 14:23:19 +05:30
{ sidebarDocs . length > 0 && (
< button
type = "button"
onClick = { ( ) = > setDocumentsSidebarOpen ( true ) }
className = "rounded-full border border-border/60 bg-accent/50 px-2.5 py-1 text-xs font-medium text-foreground/80 transition-colors hover:bg-accent"
>
{ sidebarDocs . length } { sidebarDocs . length === 1 ? "source" : "sources" } selected
< / button >
) }
2025-12-22 23:57:16 +05:30
< / div >
2026-03-06 14:40:10 +05:30
{ ! hasModelConfigured && (
2025-12-23 01:16:25 -08:00
< div className = "flex items-center gap-1.5 text-amber-600 dark:text-amber-400 text-xs" >
< AlertCircle className = "size-3" / >
< span > Select a model < / span >
< / div >
) }
2026-03-06 15:59:45 +05:30
< div className = "flex items-center gap-2" >
2026-03-24 02:22:51 +05:30
< AuiIf condition = { ( { thread } ) = > ! thread . isRunning } >
2026-03-07 04:46:48 +05:30
< ComposerPrimitive.Send asChild disabled = { isSendDisabled } >
< TooltipIconButton
tooltip = {
isBlockedByOtherUser
? "Wait for AI to finish responding"
: ! hasModelConfigured
? "Please select a model from the header to start chatting"
: isComposerEmpty
? "Enter a message to send"
: "Send message"
}
side = "bottom"
type = "submit"
variant = "default"
size = "icon"
className = { cn (
"aui-composer-send size-8 rounded-full" ,
isSendDisabled && "cursor-not-allowed opacity-50"
) }
aria - label = "Send message"
disabled = { isSendDisabled }
>
< ArrowUpIcon className = "aui-composer-send-icon size-4" / >
< / TooltipIconButton >
< / ComposerPrimitive.Send >
2026-03-24 02:22:51 +05:30
< / AuiIf >
2026-03-07 04:46:48 +05:30
2026-03-24 02:22:51 +05:30
< AuiIf condition = { ( { thread } ) = > thread . isRunning } >
2026-03-07 04:46:48 +05:30
< ComposerPrimitive.Cancel asChild >
< Button
type = "button"
variant = "default"
size = "icon"
className = "aui-composer-cancel size-8 rounded-full"
aria - label = "Stop generating"
>
< SquareIcon className = "aui-composer-cancel-icon size-3 fill-current" / >
< / Button >
< / ComposerPrimitive.Cancel >
2026-03-24 02:22:51 +05:30
< / AuiIf >
2026-03-06 15:59:45 +05:30
< / div >
2025-12-19 16:42:58 +02:00
< / div >
) ;
} ;
2026-03-10 17:36:26 -07:00
/** Convert snake_case tool names to human-readable labels */
function formatToolName ( name : string ) : string {
return name
. split ( "_" )
. map ( ( word ) = > word . charAt ( 0 ) . toUpperCase ( ) + word . slice ( 1 ) )
. join ( " " ) ;
}
2026-03-21 11:38:42 +05:30
interface ToolGroup {
label : string ;
tools : string [ ] ;
connectorIcon? : string ;
tooltip? : string ;
}
const TOOL_GROUPS : ToolGroup [ ] = [
2026-03-17 15:18:58 +05:30
{
label : "Research" ,
2026-03-28 16:39:46 -07:00
tools : [ "search_surfsense_docs" , "scrape_webpage" ] ,
2026-03-17 15:18:58 +05:30
} ,
{
label : "Generate" ,
2026-03-24 16:28:11 +05:30
tools : [ "generate_podcast" , "generate_video_presentation" , "generate_report" , "generate_image" ] ,
2026-03-17 15:18:58 +05:30
} ,
{
label : "Memory" ,
tools : [ "save_memory" , "recall_memory" ] ,
} ,
2026-03-21 11:38:42 +05:30
{
label : "Gmail" ,
tools : [ "create_gmail_draft" , "update_gmail_draft" , "send_gmail_email" , "trash_gmail_email" ] ,
connectorIcon : "gmail" ,
2026-03-31 17:31:54 +05:30
tooltip : "Create drafts, update drafts, send emails, and trash emails in Gmail" ,
2026-03-21 11:38:42 +05:30
} ,
{
label : "Google Calendar" ,
tools : [ "create_calendar_event" , "update_calendar_event" , "delete_calendar_event" ] ,
connectorIcon : "google_calendar" ,
2026-03-31 17:31:54 +05:30
tooltip : "Create, update, and delete events in Google Calendar" ,
2026-03-21 11:38:42 +05:30
} ,
{
label : "Google Drive" ,
tools : [ "create_google_drive_file" , "delete_google_drive_file" ] ,
connectorIcon : "google_drive" ,
2026-03-31 17:31:54 +05:30
tooltip : "Create and delete files in Google Drive" ,
2026-03-21 11:38:42 +05:30
} ,
2026-03-28 17:00:52 +05:30
{
label : "OneDrive" ,
tools : [ "create_onedrive_file" , "delete_onedrive_file" ] ,
connectorIcon : "onedrive" ,
2026-03-31 17:31:54 +05:30
tooltip : "Create and delete files in OneDrive" ,
2026-03-28 17:00:52 +05:30
} ,
2026-03-30 22:37:19 +05:30
{
label : "Dropbox" ,
tools : [ "create_dropbox_file" , "delete_dropbox_file" ] ,
connectorIcon : "dropbox" ,
2026-03-31 17:31:54 +05:30
tooltip : "Create and delete files in Dropbox" ,
2026-03-30 22:37:19 +05:30
} ,
2026-03-21 11:38:42 +05:30
{
label : "Notion" ,
tools : [ "create_notion_page" , "update_notion_page" , "delete_notion_page" ] ,
connectorIcon : "notion" ,
2026-03-31 17:31:54 +05:30
tooltip : "Create, update, and delete pages in Notion" ,
2026-03-21 11:38:42 +05:30
} ,
{
label : "Linear" ,
tools : [ "create_linear_issue" , "update_linear_issue" , "delete_linear_issue" ] ,
connectorIcon : "linear" ,
2026-03-31 17:31:54 +05:30
tooltip : "Create, update, and delete issues in Linear" ,
2026-03-21 11:38:42 +05:30
} ,
2026-03-21 12:41:06 +05:30
{
label : "Jira" ,
tools : [ "create_jira_issue" , "update_jira_issue" , "delete_jira_issue" ] ,
connectorIcon : "jira" ,
2026-03-31 17:31:54 +05:30
tooltip : "Create, update, and delete issues in Jira" ,
2026-03-21 12:41:06 +05:30
} ,
{
label : "Confluence" ,
tools : [ "create_confluence_page" , "update_confluence_page" , "delete_confluence_page" ] ,
connectorIcon : "confluence" ,
2026-03-31 17:31:54 +05:30
tooltip : "Create, update, and delete pages in Confluence" ,
2026-03-21 12:41:06 +05:30
} ,
2026-03-17 15:18:58 +05:30
] ;
2025-12-19 16:42:58 +02:00
const EditComposer : FC = ( ) = > {
return (
< MessagePrimitive.Root className = "aui-edit-composer-wrapper mx-auto flex w-full max-w-(--thread-max-width) flex-col px-2 py-3" >
< ComposerPrimitive.Root className = "aui-edit-composer-root ml-auto flex w-full max-w-[85%] flex-col rounded-2xl bg-muted" >
< ComposerPrimitive.Input
className = "aui-edit-composer-input min-h-14 w-full resize-none bg-transparent p-4 text-foreground text-sm outline-none"
autoFocus
/ >
< div className = "aui-edit-composer-footer mx-3 mb-3 flex items-center gap-2 self-end" >
< ComposerPrimitive.Cancel asChild >
< Button variant = "ghost" size = "sm" >
Cancel
< / Button >
< / ComposerPrimitive.Cancel >
< ComposerPrimitive.Send asChild >
< Button size = "sm" > Update < / Button >
< / ComposerPrimitive.Send >
< / div >
< / ComposerPrimitive.Root >
< / MessagePrimitive.Root >
) ;
} ;