11'use client' ;
22
33import {
4- Brain ,
5- CaretDown ,
6- ChartBar ,
7- ChartLine ,
8- ChartPie ,
9- Clock ,
10- Hash ,
11- Robot ,
12- User ,
4+ BrainIcon ,
5+ CaretDownIcon ,
6+ ChartBarIcon ,
7+ ChartLineIcon ,
8+ ChartPieIcon ,
9+ ClockIcon ,
10+ HashIcon ,
11+ RobotIcon ,
12+ UserIcon ,
1313} from '@phosphor-icons/react' ;
14- import React , { useEffect , useState } from 'react' ;
14+ import { useEffect , useState } from 'react' ;
1515import {
1616 Accordion ,
1717 AccordionContent ,
@@ -28,13 +28,13 @@ interface MessageBubbleProps {
2828const getChartIcon = ( chartType : string ) => {
2929 switch ( chartType ) {
3030 case 'bar' :
31- return < ChartBar className = "h-4 w-4" /> ;
31+ return < ChartBarIcon className = "h-4 w-4" /> ;
3232 case 'line' :
33- return < ChartLine className = "h-4 w-4" /> ;
33+ return < ChartLineIcon className = "h-4 w-4" /> ;
3434 case 'pie' :
35- return < ChartPie className = "h-4 w-4" /> ;
35+ return < ChartPieIcon className = "h-4 w-4" /> ;
3636 default :
37- return < ChartBar className = "h-4 w-4" /> ;
37+ return < ChartBarIcon className = "h-4 w-4" /> ;
3838 }
3939} ;
4040
@@ -44,7 +44,9 @@ function ThinkingStepsPreview({ steps }: { steps: string[] }) {
4444 const maxPreviewSteps = 3 ;
4545
4646 useEffect ( ( ) => {
47- if ( steps . length === 0 ) return ;
47+ if ( steps . length === 0 ) {
48+ return ;
49+ }
4850
4951 // Show the latest steps in the preview (sliding window)
5052 const latestSteps = steps . slice ( - maxPreviewSteps ) ;
@@ -59,7 +61,9 @@ function ThinkingStepsPreview({ steps }: { steps: string[] }) {
5961 }
6062 } , [ steps ] ) ;
6163
62- if ( visibleSteps . length === 0 ) return null ;
64+ if ( visibleSteps . length === 0 ) {
65+ return null ;
66+ }
6367
6468 return (
6569 < div className = "mt-2 max-h-20 space-y-1 overflow-hidden" >
@@ -76,14 +80,14 @@ function ThinkingStepsPreview({ steps }: { steps: string[] }) {
7680 ) }
7781 key = { `preview-${ index } -${ step . slice ( 0 , 20 ) } ` }
7882 >
79- < Clock className = "mt-0.5 h-3 w-3 flex-shrink-0" />
83+ < ClockIcon className = "mt-0.5 h-3 w-3 flex-shrink-0" />
8084 < span className = "break-words leading-relaxed" > { step } </ span >
8185 </ div >
8286 ) ;
8387 } ) }
8488 { steps . length > maxPreviewSteps && (
8589 < div className = "flex items-center gap-2 py-1 pl-1 text-muted-foreground text-xs opacity-60" >
86- < CaretDown className = "h-3 w-3" />
90+ < CaretDownIcon className = "h-3 w-3" />
8791 < span > +{ steps . length - maxPreviewSteps } more steps...</ span >
8892 </ div >
8993 ) }
@@ -92,14 +96,16 @@ function ThinkingStepsPreview({ steps }: { steps: string[] }) {
9296}
9397
9498function ThinkingStepsAccordion ( { steps } : { steps : string [ ] } ) {
95- if ( steps . length === 0 ) return null ;
99+ if ( steps . length === 0 ) {
100+ return null ;
101+ }
96102
97103 return (
98104 < Accordion className = "w-full" collapsible type = "single" >
99105 < AccordionItem className = "border-border/30" value = "thinking-steps" >
100106 < AccordionTrigger className = "py-2 text-xs hover:no-underline" >
101107 < div className = "flex items-center gap-2" >
102- < Brain className = "h-3 w-3" />
108+ < BrainIcon className = "h-3 w-3" />
103109 < span > Thinking Process ({ steps . length } steps)</ span >
104110 </ div >
105111 </ AccordionTrigger >
@@ -125,37 +131,44 @@ function ThinkingStepsAccordion({ steps }: { steps: string[] }) {
125131 ) ;
126132}
127133
128- export function MessageBubble ( { message } : MessageBubbleProps ) {
129- const isUser = message . type === 'user' ;
130- const isInProgress = message . type === 'assistant' && ! message . content ;
134+ function InProgressMessage ( { message } : { message : Message } ) {
131135 const hasThinkingSteps =
132136 message . thinkingSteps && message . thinkingSteps . length > 0 ;
133137
134- if ( isInProgress ) {
135- return (
136- < div className = "flex w-full max-w-[85%] gap-3" >
137- < div className = "mt-1 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full border bg-muted shadow-sm" >
138- < Robot className = "h-4 w-4" />
139- </ div >
140- < div className = "min-w-0 flex-1 rounded-lg border bg-muted px-4 py-3 shadow-sm" >
141- < div className = "flex items-center gap-2 text-sm" >
142- < div className = "flex space-x-1" >
143- < div className = "h-2 w-2 animate-bounce rounded-full bg-foreground [animation-delay:-0.3s]" />
144- < div className = "h-2 w-2 animate-bounce rounded-full bg-foreground [animation-delay:-0.15s]" />
145- < div className = "h-2 w-2 animate-bounce rounded-full bg-foreground" />
146- </ div >
147- < span className = "text-muted-foreground" > Nova is analyzing...</ span >
138+ return (
139+ < div className = "flex w-full max-w-[85%] gap-3" >
140+ < div className = "mt-1 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full border bg-muted shadow-sm" >
141+ < RobotIcon className = "h-4 w-4" />
142+ </ div >
143+ < div className = "min-w-0 flex-1 rounded-lg border bg-muted px-4 py-3 shadow-sm" >
144+ < div className = "flex items-center gap-2 text-sm" >
145+ < div className = "flex space-x-1" >
146+ < div className = "h-2 w-2 animate-bounce rounded-full bg-foreground [animation-delay:-0.3s]" />
147+ < div className = "h-2 w-2 animate-bounce rounded-full bg-foreground [animation-delay:-0.15s]" />
148+ < div className = "h-2 w-2 animate-bounce rounded-full bg-foreground" />
148149 </ div >
149-
150- { hasThinkingSteps && (
151- < div className = "mt-3 border-border/30 border-t pt-3" >
152- < ThinkingStepsPreview steps = { message . thinkingSteps || [ ] } />
153- </ div >
154- ) }
150+ < span className = "text-muted-foreground" > Nova is analyzing...</ span >
155151 </ div >
152+
153+ { hasThinkingSteps && (
154+ < div className = "mt-3 border-border/30 border-t pt-3" >
155+ < ThinkingStepsPreview steps = { message . thinkingSteps || [ ] } />
156+ </ div >
157+ ) }
156158 </ div >
157- ) ;
158- }
159+ </ div >
160+ ) ;
161+ }
162+
163+ function CompletedMessage ( {
164+ message,
165+ isUser,
166+ } : {
167+ message : Message ;
168+ isUser : boolean ;
169+ } ) {
170+ const hasThinkingSteps =
171+ message . thinkingSteps && message . thinkingSteps . length > 0 ;
159172
160173 return (
161174 < div
@@ -164,7 +177,6 @@ export function MessageBubble({ message }: MessageBubbleProps) {
164177 isUser ? 'justify-end' : 'justify-start'
165178 ) }
166179 >
167- { /* Avatar */ }
168180 < div
169181 className = { cn (
170182 'mt-1 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full shadow-sm' ,
@@ -173,10 +185,13 @@ export function MessageBubble({ message }: MessageBubbleProps) {
173185 : 'order-1 border bg-muted'
174186 ) }
175187 >
176- { isUser ? < User className = "h-4 w-4" /> : < Robot className = "h-4 w-4" /> }
188+ { isUser ? (
189+ < UserIcon className = "h-4 w-4" />
190+ ) : (
191+ < RobotIcon className = "h-4 w-4" />
192+ ) }
177193 </ div >
178194
179- { /* Message Content */ }
180195 < div
181196 className = { cn (
182197 'relative min-w-0 max-w-[85%] rounded-lg px-4 py-3 shadow-sm' ,
@@ -185,26 +200,23 @@ export function MessageBubble({ message }: MessageBubbleProps) {
185200 : 'order-2 border bg-muted'
186201 ) }
187202 >
188- { /* Main message text */ }
189203 < div className = "overflow-wrap-anywhere whitespace-pre-wrap break-words text-sm leading-relaxed" >
190204 { message . content }
191205 </ div >
192206
193- { /* Thinking Steps Accordion (for completed messages) */ }
194207 { hasThinkingSteps && ! isUser && message . content && (
195208 < div className = "mt-3" >
196209 < ThinkingStepsAccordion steps = { message . thinkingSteps || [ ] } />
197210 </ div >
198211 ) }
199212
200- { /* Metric Display */ }
201213 { message . responseType === 'metric' &&
202214 message . metricValue !== undefined &&
203215 ! isUser && (
204216 < div className = "mt-4 rounded-lg border border-primary/20 bg-primary/5 p-4" >
205217 < div className = "flex min-w-0 items-center gap-3" >
206218 < div className = "flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-primary/10" >
207- < Hash className = "h-5 w-5 text-primary" />
219+ < HashIcon className = "h-5 w-5 text-primary" />
208220 </ div >
209221 < div className = "min-w-0 flex-1" >
210222 < div className = "truncate font-medium text-muted-foreground text-xs uppercase tracking-wide" >
@@ -220,7 +232,6 @@ export function MessageBubble({ message }: MessageBubbleProps) {
220232 </ div >
221233 ) }
222234
223- { /* Visualization Indicator */ }
224235 { message . hasVisualization && ! isUser && (
225236 < div className = "mt-3 border-border/30 border-t pt-3" >
226237 < div className = "flex items-center gap-2 text-muted-foreground text-xs" >
@@ -230,7 +241,6 @@ export function MessageBubble({ message }: MessageBubbleProps) {
230241 </ div >
231242 ) }
232243
233- { /* Timestamp (hover) */ }
234244 < div className = "-bottom-5 absolute right-0 opacity-0 transition-opacity group-hover:opacity-60" >
235245 < div className = "mt-1 font-mono text-xs" >
236246 { message . timestamp . toLocaleTimeString ( [ ] , {
@@ -243,3 +253,14 @@ export function MessageBubble({ message }: MessageBubbleProps) {
243253 </ div >
244254 ) ;
245255}
256+
257+ export function MessageBubble ( { message } : MessageBubbleProps ) {
258+ const isUser = message . type === 'user' ;
259+ const isInProgress = message . type === 'assistant' && ! message . content ;
260+
261+ if ( isInProgress ) {
262+ return < InProgressMessage message = { message } /> ;
263+ }
264+
265+ return < CompletedMessage isUser = { isUser } message = { message } /> ;
266+ }
0 commit comments