1- import { useCallback , useEffect , useRef , useState } from "react" ;
1+ import { memo , useCallback , useEffect , useRef , useState } from "react" ;
22import { useCortexChat , type ToolActivity } from "@/hooks/useCortexChat" ;
33import { Markdown } from "@/components/Markdown" ;
44import { ToolCall , type ToolCallPair } from "@/components/ToolCall" ;
5- import { api , type CortexChatToolCall , type CortexChatThread } from "@/api/client" ;
5+ import { api , type CortexChatMessage , type CortexChatToolCall , type CortexChatThread } from "@/api/client" ;
66import { Button } from "@/ui" ;
77import { Popover , PopoverContent , PopoverTrigger } from "@/ui/Popover" ;
88import { PlusSignIcon , Cancel01Icon , Clock01Icon , Delete02Icon } from "@hugeicons/core-free-icons" ;
@@ -164,44 +164,51 @@ function ThinkingIndicator() {
164164 ) ;
165165}
166166
167- function CortexChatInput ( {
168- value,
169- onChange,
167+ const CortexChatInput = memo ( function CortexChatInput ( {
170168 onSubmit,
171169 isStreaming,
172170} : {
173- value : string ;
174- onChange : ( value : string ) => void ;
175- onSubmit : ( ) => void ;
171+ onSubmit : ( text : string ) => void ;
176172 isStreaming : boolean ;
177173} ) {
178174 const textareaRef = useRef < HTMLTextAreaElement > ( null ) ;
175+ const [ hasText , setHasText ] = useState ( false ) ;
179176
180177 useEffect ( ( ) => {
181178 textareaRef . current ?. focus ( ) ;
182179 } , [ ] ) ;
183180
184- useEffect ( ( ) => {
181+ const adjustHeight = ( ) => {
185182 const textarea = textareaRef . current ;
186183 if ( ! textarea ) return ;
184+ textarea . style . height = "auto" ;
185+ const scrollHeight = textarea . scrollHeight ;
186+ const maxHeight = 160 ;
187+ textarea . style . height = `${ Math . min ( scrollHeight , maxHeight ) } px` ;
188+ textarea . style . overflowY = scrollHeight > maxHeight ? "auto" : "hidden" ;
189+ } ;
187190
188- const adjustHeight = ( ) => {
189- textarea . style . height = "auto" ;
190- const scrollHeight = textarea . scrollHeight ;
191- const maxHeight = 160 ;
192- textarea . style . height = `${ Math . min ( scrollHeight , maxHeight ) } px` ;
193- textarea . style . overflowY = scrollHeight > maxHeight ? "auto" : "hidden" ;
194- } ;
191+ const doSubmit = ( ) => {
192+ const textarea = textareaRef . current ;
193+ if ( ! textarea ) return ;
194+ const trimmed = textarea . value . trim ( ) ;
195+ if ( ! trimmed ) return ;
196+ textarea . value = "" ;
197+ setHasText ( false ) ;
198+ adjustHeight ( ) ;
199+ onSubmit ( trimmed ) ;
200+ } ;
195201
202+ const handleInput = ( ) => {
203+ const value = textareaRef . current ?. value ?? "" ;
204+ setHasText ( value . trim ( ) . length > 0 ) ;
196205 adjustHeight ( ) ;
197- textarea . addEventListener ( "input" , adjustHeight ) ;
198- return ( ) => textarea . removeEventListener ( "input" , adjustHeight ) ;
199- } , [ value ] ) ;
206+ } ;
200207
201208 const handleKeyDown = ( event : React . KeyboardEvent < HTMLTextAreaElement > ) => {
202209 if ( event . key === "Enter" && ! event . shiftKey ) {
203210 event . preventDefault ( ) ;
204- onSubmit ( ) ;
211+ doSubmit ( ) ;
205212 }
206213 } ;
207214
@@ -210,8 +217,7 @@ function CortexChatInput({
210217 < div className = "flex items-end gap-2 p-2.5" >
211218 < textarea
212219 ref = { textareaRef }
213- value = { value }
214- onChange = { ( event ) => onChange ( event . target . value ) }
220+ onInput = { handleInput }
215221 onKeyDown = { handleKeyDown }
216222 placeholder = {
217223 isStreaming ? "Waiting for response..." : "Message the cortex..."
@@ -223,8 +229,8 @@ function CortexChatInput({
223229 />
224230 < button
225231 type = "button"
226- onClick = { onSubmit }
227- disabled = { isStreaming || ! value . trim ( ) }
232+ onClick = { doSubmit }
233+ disabled = { isStreaming || ! hasText }
228234 className = "flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-accent text-white transition-all duration-150 hover:bg-accent-deep disabled:opacity-30 disabled:hover:bg-accent"
229235 >
230236 < svg
@@ -243,7 +249,7 @@ function CortexChatInput({
243249 </ div >
244250 </ div >
245251 ) ;
246- }
252+ } ) ;
247253
248254function formatRelativeTime ( dateStr : string ) : string {
249255 const date = new Date ( dateStr ) ;
@@ -354,6 +360,43 @@ function ThreadList({
354360 ) ;
355361}
356362
363+ const CortexMessageList = memo ( function CortexMessageList ( {
364+ messages,
365+ } : {
366+ messages : CortexChatMessage [ ] ;
367+ } ) {
368+ return (
369+ < >
370+ { messages . map ( ( message ) => (
371+ < div key = { message . id } >
372+ { message . role === "user" ? (
373+ < div className = "flex justify-end" >
374+ < div className = "max-w-[85%] rounded-2xl rounded-br-md bg-app-hover/30 px-3 py-2" >
375+ < p className = "text-sm text-ink" > { message . content } </ p >
376+ </ div >
377+ </ div >
378+ ) : (
379+ < div className = "flex flex-col gap-2" >
380+ { message . tool_calls && message . tool_calls . length > 0 && (
381+ < div className = "flex flex-col gap-1.5" >
382+ { message . tool_calls . map ( ( call ) => (
383+ < ToolCall key = { call . id } pair = { toToolCallPair ( call ) } />
384+ ) ) }
385+ </ div >
386+ ) }
387+ { message . content && (
388+ < div className = "text-sm text-ink-dull" >
389+ < Markdown > { message . content } </ Markdown >
390+ </ div >
391+ ) }
392+ </ div >
393+ ) }
394+ </ div >
395+ ) ) }
396+ </ >
397+ ) ;
398+ } ) ;
399+
357400export function CortexChatPanel ( {
358401 agentId,
359402 channelId,
@@ -371,7 +414,6 @@ export function CortexChatPanel({
371414 newThread,
372415 loadThread,
373416 } = useCortexChat ( agentId , channelId , { freshThread : ! ! initialPrompt } ) ;
374- const [ input , setInput ] = useState ( "" ) ;
375417 const [ threadListOpen , setThreadListOpen ] = useState ( false ) ;
376418 const messagesEndRef = useRef < HTMLDivElement > ( null ) ;
377419 const initialPromptSentRef = useRef ( false ) ;
@@ -394,12 +436,13 @@ export function CortexChatPanel({
394436 messagesEndRef . current ?. scrollIntoView ( { behavior : "smooth" } ) ;
395437 } , [ messages . length , isStreaming , toolActivity . length ] ) ;
396438
397- const handleSubmit = ( ) => {
398- const trimmed = input . trim ( ) ;
399- if ( ! trimmed || isStreaming ) return ;
400- setInput ( "" ) ;
401- sendMessage ( trimmed ) ;
402- } ;
439+ const handleSubmit = useCallback (
440+ ( text : string ) => {
441+ if ( isStreaming ) return ;
442+ sendMessage ( text ) ;
443+ } ,
444+ [ isStreaming , sendMessage ] ,
445+ ) ;
403446
404447 const handleStarterPrompt = ( prompt : string ) => {
405448 if ( isStreaming || ! threadId ) return ;
@@ -478,32 +521,7 @@ export function CortexChatPanel({
478521 { /* Messages */ }
479522 < div className = "min-h-0 flex-1 overflow-y-auto" >
480523 < div className = "flex flex-col gap-5 p-3 pb-4" >
481- { messages . map ( ( message ) => (
482- < div key = { message . id } >
483- { message . role === "user" ? (
484- < div className = "flex justify-end" >
485- < div className = "max-w-[85%] rounded-2xl rounded-br-md bg-app-hover/30 px-3 py-2" >
486- < p className = "text-sm text-ink" > { message . content } </ p >
487- </ div >
488- </ div >
489- ) : (
490- < div className = "flex flex-col gap-2" >
491- { message . tool_calls && message . tool_calls . length > 0 && (
492- < div className = "flex flex-col gap-1.5" >
493- { message . tool_calls . map ( ( call ) => (
494- < ToolCall key = { call . id } pair = { toToolCallPair ( call ) } />
495- ) ) }
496- </ div >
497- ) }
498- { message . content && (
499- < div className = "text-sm text-ink-dull" >
500- < Markdown > { message . content } </ Markdown >
501- </ div >
502- ) }
503- </ div >
504- ) }
505- </ div >
506- ) ) }
524+ < CortexMessageList messages = { messages } />
507525
508526 { /* Streaming state */ }
509527 { isStreaming && (
@@ -537,8 +555,6 @@ export function CortexChatPanel({
537555 { /* Input */ }
538556 < div className = "border-t border-app-line/50 p-3" >
539557 < CortexChatInput
540- value = { input }
541- onChange = { setInput }
542558 onSubmit = { handleSubmit }
543559 isStreaming = { isStreaming }
544560 />
0 commit comments