@@ -15,11 +15,11 @@ function ToolActivityIndicator({ activity }: { activity: ToolActivity[] }) {
1515 if ( activity . length === 0 ) return null ;
1616
1717 return (
18- < div className = "flex flex-col gap-1 px-3 py -2" >
18+ < div className = "flex flex-wrap items-center gap-1.5 mt -2" >
1919 { activity . map ( ( tool , index ) => (
20- < div
20+ < span
2121 key = { `${ tool . tool } -${ index } ` }
22- className = "flex items-center gap-2 rounded bg-app-darkBox/40 px-2 py-1 "
22+ className = "inline- flex items-center gap-1.5 rounded-full bg-app-box/60 px-2.5 py-0.5 "
2323 >
2424 { tool . status === "running" ? (
2525 < span className = "h-1.5 w-1.5 animate-pulse rounded-full bg-amber-400" />
@@ -28,60 +28,141 @@ function ToolActivityIndicator({ activity }: { activity: ToolActivity[] }) {
2828 ) }
2929 < span className = "font-mono text-tiny text-ink-faint" > { tool . tool } </ span >
3030 { tool . status === "done" && tool . result_preview && (
31- < span className = "min-w-0 flex-1 truncate text-tiny text-ink-faint/60" >
31+ < span className = "min-w-0 max-w-[120px] truncate text-tiny text-ink-faint/60" >
3232 { tool . result_preview . slice ( 0 , 80 ) }
3333 </ span >
3434 ) }
35- </ div >
35+ </ span >
3636 ) ) }
3737 </ div >
3838 ) ;
3939}
4040
41+ function ThinkingIndicator ( ) {
42+ return (
43+ < div className = "flex items-center gap-1.5 py-1" >
44+ < span className = "inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-ink-faint" />
45+ < span className = "inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-ink-faint [animation-delay:0.2s]" />
46+ < span className = "inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-ink-faint [animation-delay:0.4s]" />
47+ </ div >
48+ ) ;
49+ }
50+
51+ function CortexChatInput ( {
52+ value,
53+ onChange,
54+ onSubmit,
55+ isStreaming,
56+ } : {
57+ value : string ;
58+ onChange : ( value : string ) => void ;
59+ onSubmit : ( ) => void ;
60+ isStreaming : boolean ;
61+ } ) {
62+ const textareaRef = useRef < HTMLTextAreaElement > ( null ) ;
63+
64+ useEffect ( ( ) => {
65+ textareaRef . current ?. focus ( ) ;
66+ } , [ ] ) ;
67+
68+ useEffect ( ( ) => {
69+ const textarea = textareaRef . current ;
70+ if ( ! textarea ) return ;
71+
72+ const adjustHeight = ( ) => {
73+ textarea . style . height = "auto" ;
74+ const scrollHeight = textarea . scrollHeight ;
75+ const maxHeight = 160 ;
76+ textarea . style . height = `${ Math . min ( scrollHeight , maxHeight ) } px` ;
77+ textarea . style . overflowY = scrollHeight > maxHeight ? "auto" : "hidden" ;
78+ } ;
79+
80+ adjustHeight ( ) ;
81+ textarea . addEventListener ( "input" , adjustHeight ) ;
82+ return ( ) => textarea . removeEventListener ( "input" , adjustHeight ) ;
83+ } , [ value ] ) ;
84+
85+ const handleKeyDown = ( event : React . KeyboardEvent < HTMLTextAreaElement > ) => {
86+ if ( event . key === "Enter" && ! event . shiftKey ) {
87+ event . preventDefault ( ) ;
88+ onSubmit ( ) ;
89+ }
90+ } ;
91+
92+ return (
93+ < div className = "rounded-xl border border-app-line/50 bg-app-box/40 backdrop-blur-xl transition-colors duration-200 hover:border-app-line/70" >
94+ < div className = "flex items-end gap-2 p-2.5" >
95+ < textarea
96+ ref = { textareaRef }
97+ value = { value }
98+ onChange = { ( event ) => onChange ( event . target . value ) }
99+ onKeyDown = { handleKeyDown }
100+ placeholder = { isStreaming ? "Waiting for response..." : "Message the cortex..." }
101+ disabled = { isStreaming }
102+ rows = { 1 }
103+ className = "flex-1 resize-none bg-transparent px-1 py-1 text-sm text-ink placeholder:text-ink-faint/60 focus:outline-none disabled:opacity-40"
104+ style = { { maxHeight : "160px" } }
105+ />
106+ < button
107+ type = "button"
108+ onClick = { onSubmit }
109+ disabled = { isStreaming || ! value . trim ( ) }
110+ 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"
111+ >
112+ < svg
113+ width = "14"
114+ height = "14"
115+ viewBox = "0 0 24 24"
116+ fill = "none"
117+ stroke = "currentColor"
118+ strokeWidth = "2"
119+ strokeLinecap = "round"
120+ strokeLinejoin = "round"
121+ >
122+ < path d = "M12 19V5M5 12l7-7 7 7" />
123+ </ svg >
124+ </ button >
125+ </ div >
126+ </ div >
127+ ) ;
128+ }
129+
41130export function CortexChatPanel ( { agentId, channelId, onClose } : CortexChatPanelProps ) {
42131 const { messages, isStreaming, error, toolActivity, sendMessage, newThread } = useCortexChat ( agentId , channelId ) ;
43132 const [ input , setInput ] = useState ( "" ) ;
44133 const messagesEndRef = useRef < HTMLDivElement > ( null ) ;
45- const inputRef = useRef < HTMLInputElement > ( null ) ;
46134
47- // Auto-scroll on new messages or tool activity
48135 useEffect ( ( ) => {
49136 messagesEndRef . current ?. scrollIntoView ( { behavior : "smooth" } ) ;
50137 } , [ messages . length , isStreaming , toolActivity . length ] ) ;
51138
52- // Focus input on mount
53- useEffect ( ( ) => {
54- inputRef . current ?. focus ( ) ;
55- } , [ ] ) ;
56-
57- const handleSubmit = ( event : React . FormEvent ) => {
58- event . preventDefault ( ) ;
139+ const handleSubmit = ( ) => {
59140 const trimmed = input . trim ( ) ;
60141 if ( ! trimmed || isStreaming ) return ;
61142 setInput ( "" ) ;
62143 sendMessage ( trimmed ) ;
63144 } ;
64145
65146 return (
66- < div className = "flex h-full w-full flex-col bg-app-darkBox/30 " >
147+ < div className = "flex h-full w-full flex-col" >
67148 { /* Header */ }
68- < div className = "flex h-12 items-center justify-between border-b border-app-line/50 px-4 " >
149+ < div className = "flex h-10 items-center justify-between border-b border-app-line/50 px-3 " >
69150 < div className = "flex items-center gap-2" >
70151 < span className = "text-sm font-medium text-ink" > Cortex</ span >
71152 { channelId && (
72- < span className = "rounded bg-violet-500/10 px-1.5 py-0.5 text-tiny text-violet-400 " >
73- { channelId . length > 24 ? `${ channelId . slice ( 0 , 24 ) } ...` : channelId }
153+ < span className = "rounded-full bg-app-box px-2 py-0.5 text-tiny text-ink-faint " >
154+ { channelId . length > 20 ? `${ channelId . slice ( 0 , 20 ) } ...` : channelId }
74155 </ span >
75156 ) }
76157 </ div >
77- < div className = "flex items-center gap-1 " >
158+ < div className = "flex items-center gap-0.5 " >
78159 < Button
79160 onClick = { newThread }
80161 variant = "ghost"
81162 size = "icon"
82163 disabled = { isStreaming }
83164 className = "h-7 w-7"
84- title = "New chat "
165+ title = "New thread "
85166 >
86167 < HugeiconsIcon icon = { PlusSignIcon } className = "h-3.5 w-3.5" />
87168 </ Button >
@@ -101,51 +182,39 @@ export function CortexChatPanel({ agentId, channelId, onClose }: CortexChatPanel
101182
102183 { /* Messages */ }
103184 < div className = "flex-1 overflow-y-auto" >
104- < div className = "flex flex-col gap-3 p -4" >
185+ < div className = "flex flex-col gap-5 p-3 pb -4" >
105186 { messages . length === 0 && ! isStreaming && (
106- < p className = "py-8 text -center text-sm text-ink-faint " >
107- Ask the cortex anything
108- </ p >
187+ < div className = "flex items -center justify-center py-12 " >
188+ < p className = "text-sm text-ink-faint" > Ask the cortex anything</ p >
189+ </ div >
109190 ) }
191+
110192 { messages . map ( ( message ) => (
111- < div
112- key = { message . id }
113- className = { `rounded-md px-3 py-2 ${
114- message . role === "user"
115- ? "ml-8 bg-accent/10"
116- : "mr-2 bg-app-darkBox/50"
117- } `}
118- >
119- < span className = { `text-tiny font-medium ${
120- message . role === "user" ? "text-accent-faint" : "text-violet-400"
121- } `} >
122- { message . role === "user" ? "admin" : "cortex" }
123- </ span >
124- < div className = "mt-0.5 text-sm text-ink-dull" >
125- { message . role === "assistant" ? (
193+ < div key = { message . id } >
194+ { message . role === "user" ? (
195+ < div className = "flex justify-end" >
196+ < div className = "max-w-[85%] rounded-2xl rounded-br-md bg-accent/10 px-3 py-2" >
197+ < p className = "text-sm text-ink" > { message . content } </ p >
198+ </ div >
199+ </ div >
200+ ) : (
201+ < div className = "text-sm text-ink-dull" >
126202 < Markdown > { message . content } </ Markdown >
127- ) : (
128- < p > { message . content } </ p >
129- ) }
130- </ div >
203+ </ div >
204+ ) }
131205 </ div >
132206 ) ) }
207+
208+ { /* Streaming state */ }
133209 { isStreaming && (
134- < div className = "mr-2 rounded-md bg-app-darkBox/50 px-3 py-2" >
135- < span className = "text-tiny font-medium text-violet-400" > cortex</ span >
210+ < div >
136211 < ToolActivityIndicator activity = { toolActivity } />
137- { toolActivity . length === 0 && (
138- < div className = "mt-1 flex items-center gap-1" >
139- < span className = "inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-violet-400" />
140- < span className = "inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-violet-400 [animation-delay:0.2s]" />
141- < span className = "inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-violet-400 [animation-delay:0.4s]" />
142- < span className = "ml-1 text-tiny text-ink-faint" > thinking...</ span >
143- </ div >
144- ) }
212+ { toolActivity . length === 0 && < ThinkingIndicator /> }
145213 </ div >
146214 ) }
215+
147216 { error && (
148- < div className = "rounded-md border border-red-500/20 bg-red-500/10 px-3 py-2 text-sm text-red-400" >
217+ < div className = "rounded-lg border border-red-500/20 bg-red-500/5 px-3 py-2.5 text-sm text-red-400" >
149218 { error }
150219 </ div >
151220 ) }
@@ -154,27 +223,14 @@ export function CortexChatPanel({ agentId, channelId, onClose }: CortexChatPanel
154223 </ div >
155224
156225 { /* Input */ }
157- < form onSubmit = { handleSubmit } className = "border-t border-app-line/50 p-3" >
158- < div className = "flex gap-2" >
159- < input
160- ref = { inputRef }
161- type = "text"
162- value = { input }
163- onChange = { ( event ) => setInput ( event . target . value ) }
164- placeholder = { isStreaming ? "Waiting for response..." : "Message the cortex..." }
165- disabled = { isStreaming }
166- className = "flex-1 rounded-md border border-app-line bg-app-darkBox px-3 py-1.5 text-sm text-ink placeholder:text-ink-faint focus:border-violet-500/50 focus:outline-none disabled:opacity-50"
167- />
168- < Button
169- type = "submit"
170- disabled = { isStreaming || ! input . trim ( ) }
171- size = "sm"
172- className = "bg-violet-500/20 text-violet-400 hover:bg-violet-500/30"
173- >
174- Send
175- </ Button >
176- </ div >
177- </ form >
226+ < div className = "border-t border-app-line/50 p-3" >
227+ < CortexChatInput
228+ value = { input }
229+ onChange = { setInput }
230+ onSubmit = { handleSubmit }
231+ isStreaming = { isStreaming }
232+ />
233+ </ div >
178234 </ div >
179235 ) ;
180236}
0 commit comments