@@ -6,16 +6,18 @@ import { useFeatureFlags } from '@open-condo/featureflags/FeatureFlagsContext'
66import { useAuth } from '@open-condo/next/auth'
77import { useIntl } from '@open-condo/next/intl'
88import { useOrganization } from '@open-condo/next/organization'
9- import { Button , Input , Markdown , Space , Tooltip , Typography } from '@open-condo/ui'
9+ import { Button , Markdown , Space , Tooltip , Typography } from '@open-condo/ui'
1010
1111import { CHAT_WITH_CONDO_FLOW_TYPE , TASK_STATUSES } from '@condo/domains/ai/constants'
12+ import { useAIChatAttachments , type AIChatAttachmentMeta } from '@condo/domains/ai/hooks/useAIChatAttachments'
1213import { useAIFlow } from '@condo/domains/ai/hooks/useAIFlow'
1314import { runToolCall , ToolCallResult } from '@condo/domains/ai/utils/toolCalls'
1415import { AI_CHAT_BUTTON_CONFIG } from '@condo/domains/common/constants/featureflags'
1516import { analytics } from '@condo/domains/common/utils/analytics'
1617import { LocalStorageManager } from '@condo/domains/common/utils/localStorageManager'
1718
1819import styles from './AIChat.module.css'
20+ import { AIChatInput } from './AIChatInput'
1921import { AIChatMessage } from './AIChatMessage'
2022
2123const STORAGE_KEY = 'condo-ai-chat-history'
@@ -115,9 +117,16 @@ function parseAiChatButtonConfigFromFlag (raw: unknown): AiChatButtonConfig | nu
115117 return { welcomeMessage, buttons }
116118}
117119
120+ export type MessageAttachmentDisplay = {
121+ name : string
122+ mimeType ?: string
123+ url ?: string
124+ }
125+
118126export type MessageContent = {
119127 text : string
120128 suggestions ?: string [ ]
129+ attachments ?: MessageAttachmentDisplay [ ]
121130}
122131
123132export type Message = {
@@ -130,6 +139,14 @@ export type Message = {
130139 copyable ?: boolean
131140}
132141
142+ type ExecuteAIMessageOptions = {
143+ additionalContext ?: Record < string , unknown >
144+ toolCallDepth ?: number
145+ messageId ?: string | null
146+ scenarioButtonId ?: string | null
147+ attachments ?: AIChatAttachmentMeta [ ]
148+ }
149+
133150type AIChatProps = {
134151 aiSessionId : string
135152 onSessionChange ?: ( sessionId : string ) => void
@@ -166,8 +183,6 @@ export const AIChat: React.FC<AIChatProps> = ({
166183
167184 const messagesEndRef = useRef < HTMLDivElement > ( null )
168185 const inputRef = useRef < any > ( null )
169- const inputContainerRef = useRef < HTMLDivElement > ( null )
170- const [ inputContainerHeight , setInputContainerHeight ] = useState ( 0 )
171186
172187 const [ { execute, resume } , { loading, currentTaskId } ] = useAIFlow < { answer : string , toolCalls ?: Array < { name : string , args : any } > } > ( {
173188 aiSessionId : aiSessionId ,
@@ -221,6 +236,16 @@ export const AIChat: React.FC<AIChatProps> = ({
221236 return ! ( currentTaskId && loading )
222237 } , [ currentTaskId , loading ] )
223238
239+ const attachments = useAIChatAttachments ( {
240+ onFileListChange : ( ) => inputRef . current ?. focus ( ) ,
241+ } )
242+ const attachmentsUploading = attachments ? attachments . uploading : false
243+ const canSendWithAttachments = attachments ? attachments . canSendWithAttachments : false
244+
245+ const canSendMessage = useMemo ( ( ) => {
246+ return Boolean ( inputValue . trim ( ) || canSendWithAttachments ) && ! attachmentsUploading
247+ } , [ inputValue , canSendWithAttachments , attachmentsUploading ] )
248+
224249 const addMessage = useCallback ( ( newMessage : Message ) => {
225250 setMessages ( prev => {
226251 return [ ...prev , newMessage ]
@@ -301,26 +326,18 @@ export const AIChat: React.FC<AIChatProps> = ({
301326 } , 100 )
302327 } , [ ] )
303328
304- useEffect ( ( ) => {
305- if ( ! inputContainerRef . current ) return
306-
307- const observer = new ResizeObserver ( ( entries ) => {
308- for ( const entry of entries ) {
309- setInputContainerHeight ( entry . contentRect . height )
310- }
311- } )
312-
313- observer . observe ( inputContainerRef . current )
314- return ( ) => observer . disconnect ( )
315- } , [ ] )
316-
317329 const executeAIMessage = useCallback ( async (
318330 userInput : string ,
319- additionalContext ?: any ,
320- toolCallDepth = 0 ,
321- messageId : string | null = null ,
322- scenarioButtonId ?: string | null ,
331+ options : ExecuteAIMessageOptions = { } ,
323332 ) => {
333+ const {
334+ additionalContext,
335+ toolCallDepth = 0 ,
336+ messageId = null ,
337+ scenarioButtonId = null ,
338+ attachments : attachmentsForRequest ,
339+ } = options
340+
324341 if ( toolCallDepth >= MAX_TOOL_CALL_DEPTH ) {
325342 addMessage ( {
326343 id : `depth-error-${ Date . now ( ) } ` ,
@@ -354,6 +371,7 @@ export const AIChat: React.FC<AIChatProps> = ({
354371 organizationId : organization ?. id ,
355372 ...additionalContext ,
356373 } ,
374+ ...( attachmentsForRequest ?. length ? { attachments : attachmentsForRequest } : { } ) ,
357375 ...( scenarioButtonId ? { button_id : scenarioButtonId } : { } ) ,
358376 } )
359377
@@ -449,7 +467,11 @@ export const AIChat: React.FC<AIChatProps> = ({
449467 } ) )
450468
451469 if ( allToolCallResults . length > 0 ) {
452- await executeAIMessage ( 'toolCalls:' , { toolCalls : allToolCallResults } , toolCallDepth + 1 , toolExecutionMessage . id )
470+ await executeAIMessage ( 'toolCalls:' , {
471+ additionalContext : { toolCalls : allToolCallResults } ,
472+ toolCallDepth : toolCallDepth + 1 ,
473+ messageId : toolExecutionMessage . id ,
474+ } )
453475 }
454476 } catch ( error ) {
455477 changeMessage ( toolExecutionMessage . id , {
@@ -472,18 +494,27 @@ export const AIChat: React.FC<AIChatProps> = ({
472494 } , [ aiSessionId , currentTaskId , loadingLabel , errorMessage , failedToGetResponseMessage , organization , user , client , intl , addMessage , changeMessage , removeMessage , execute , toolDepthExceededMessage , noResponseMessage , executingToolsMessage , errorExecutingToolsMessage ] )
473495
474496 const handleSendMessage = async ( ) => {
475- if ( ! inputValue . trim ( ) || loading || ! user ) return
497+ const trimmedInput = inputValue . trim ( )
498+ const canSend = ( trimmedInput || canSendWithAttachments ) && ! loading && ! attachmentsUploading && user
499+ if ( ! canSend ) return
476500
501+ const attachmentsToSend = attachments ? [ ...attachments . readyAttachments ] : [ ]
477502 const isFirstInSession = ! messages . some ( ( msg ) => msg . role === 'user' )
478503 void analytics . track ( 'ai_assistant_message_send' , {
479504 source : 'typed' ,
480505 is_first_in_session : isFirstInSession ,
481506 location : typeof window !== 'undefined' ? window . location . href : '' ,
507+ attachments_count : attachmentsToSend . length ,
482508 } )
483509
484510 const userMessage : Message = {
485511 id : Date . now ( ) . toString ( ) ,
486- content : { text : inputValue . trim ( ) } ,
512+ content : {
513+ text : trimmedInput ,
514+ ...( attachmentsToSend . length ? {
515+ attachments : attachmentsToSend . map ( ( { name, mimeType } ) => ( { name, mimeType } ) ) ,
516+ } : { } ) ,
517+ } ,
487518 role : 'user' ,
488519 timestamp : new Date ( ) ,
489520 status : 'sent' ,
@@ -492,10 +523,10 @@ export const AIChat: React.FC<AIChatProps> = ({
492523
493524 addMessage ( userMessage )
494525
495- const currentInput = inputValue . trim ( )
496526 setInputValue ( '' )
527+ attachments ?. resetAttachments ( )
497528
498- await executeAIMessage ( currentInput )
529+ await executeAIMessage ( trimmedInput , { attachments : attachmentsToSend } )
499530 }
500531
501532 const handleScenarioButtonClick = useCallback ( async ( buttonId : string , buttonName : string ) => {
@@ -506,6 +537,7 @@ export const AIChat: React.FC<AIChatProps> = ({
506537 source : 'scenario_button' ,
507538 is_first_in_session : isFirstInSession ,
508539 location : typeof window !== 'undefined' ? window . location . href : '' ,
540+ attachments_count : 0 ,
509541 button_id : buttonId ,
510542 button_name : buttonName ,
511543 } )
@@ -520,7 +552,7 @@ export const AIChat: React.FC<AIChatProps> = ({
520552 }
521553
522554 addMessage ( userMessage )
523- await executeAIMessage ( buttonName , undefined , 0 , null , buttonId )
555+ await executeAIMessage ( buttonName , { scenarioButtonId : buttonId } )
524556 } , [ loading , user , canExecuteAIFlow , messages , addMessage , executeAIMessage ] )
525557
526558 const handleSuggestionButtonClick = useCallback ( async ( suggestedText : string ) => {
@@ -531,6 +563,7 @@ export const AIChat: React.FC<AIChatProps> = ({
531563 source : 'suggestion' ,
532564 is_first_in_session : isFirstInSession ,
533565 location : typeof window !== 'undefined' ? window . location . href : '' ,
566+ attachments_count : 0 ,
534567 button_name : suggestedText ,
535568 } )
536569
@@ -547,12 +580,15 @@ export const AIChat: React.FC<AIChatProps> = ({
547580 await executeAIMessage ( suggestedText )
548581 } , [ loading , user , canExecuteAIFlow , messages , addMessage , executeAIMessage ] )
549582
550- const handleKeyPress = ( e : React . KeyboardEvent ) => {
551- if ( e . key === 'Enter' && ! e . shiftKey ) {
552- e . preventDefault ( )
553- handleSendMessage ( )
583+ const handleComposerKeyDown = useCallback ( ( e : React . KeyboardEvent < HTMLTextAreaElement > ) => {
584+ if ( e . key !== 'Enter' || e . shiftKey ) return
585+
586+ e . preventDefault ( )
587+
588+ if ( canSendMessage && canExecuteAIFlow ) {
589+ void handleSendMessage ( )
554590 }
555- }
591+ } , [ canExecuteAIFlow , canSendMessage , handleSendMessage ] )
556592
557593 return (
558594 < div className = { styles . chatContainer } >
@@ -624,21 +660,19 @@ export const AIChat: React.FC<AIChatProps> = ({
624660 ) )
625661 ) }
626662 < div ref = { messagesEndRef } />
627- < div className = { styles . inputSpacer } style = { { height : inputContainerHeight } } />
628663 </ div >
629664
630- < div ref = { inputContainerRef } className = { styles . inputContainer } >
631- < Input . TextArea
632- ref = { inputRef }
633- value = { inputValue }
634- onChange = { ( e ) => setInputValue ( e . target . value ) }
635- onKeyDown = { handleKeyPress }
636- onSubmit = { ( ) => handleSendMessage ( ) }
637- placeholder = { placeholder }
638- disabled = { ! canExecuteAIFlow }
639- autoSize = { { minRows : 1 , maxRows : 4 } }
640- />
641- </ div >
665+ < AIChatInput
666+ attachments = { attachments }
667+ canExecuteAIFlow = { canExecuteAIFlow }
668+ canSendMessage = { canSendMessage }
669+ inputRef = { inputRef }
670+ inputValue = { inputValue }
671+ onInputChange = { setInputValue }
672+ onInputKeyDown = { handleComposerKeyDown }
673+ onSendMessage = { ( ) => void handleSendMessage ( ) }
674+ placeholder = { placeholder }
675+ />
642676 </ div >
643677 )
644678}
0 commit comments