@@ -5,6 +5,7 @@ import { useState, useEffect, useRef } from 'react';
5
5
import { useChat } from 'ai/react' ;
6
6
import { motion , AnimatePresence } from 'framer-motion' ;
7
7
import { X , ExternalLink , Info , Sparkles } from 'lucide-react' ;
8
+ import ReactMarkdown from 'react-markdown' ;
8
9
9
10
import { cn } from '@/app/lib/utils' ;
10
11
@@ -34,6 +35,39 @@ interface ValidationState {
34
35
message ?: string ;
35
36
}
36
37
38
+ function ChatBubble ( {
39
+ children,
40
+ isAssistant,
41
+ confidence,
42
+ } : {
43
+ children : React . ReactNode ;
44
+ isAssistant : boolean ;
45
+ confidence ?: number ;
46
+ } ) {
47
+ const getConfidenceColor = ( confidence ?: number ) => {
48
+ if ( ! confidence || ! isAssistant ) return null ;
49
+ if ( confidence >= 0.8 ) return 'bg-green-500' ;
50
+ if ( confidence >= 0.5 ) return 'bg-yellow-500' ;
51
+ return 'bg-red-500' ;
52
+ } ;
53
+
54
+ return (
55
+ < div className = "relative" >
56
+ < div className = { cn ( 'max-w-[85%] w-fit' , isAssistant ? 'ml-0 mr-auto' : 'ml-auto mr-0' ) } >
57
+ { children }
58
+ </ div >
59
+ { isAssistant && confidence && (
60
+ < div
61
+ className = { cn (
62
+ 'absolute -bottom-1 left-4 w-1 h-1 rounded-full' ,
63
+ getConfidenceColor ( confidence )
64
+ ) }
65
+ />
66
+ ) }
67
+ </ div >
68
+ ) ;
69
+ }
70
+
37
71
export default function Chat ( ) {
38
72
const [ isOpen , setIsOpen ] = useState ( false ) ;
39
73
const [ validationState , setValidationState ] = useState < ValidationState > ( { isValid : true } ) ;
@@ -77,8 +111,21 @@ export default function Chat() {
77
111
78
112
const parseResponse = ( content : string ) : StructuredResponse | null => {
79
113
try {
80
- return JSON . parse ( content ) ;
114
+ // If it's already a JSON string, parse it
115
+ const parsed = JSON . parse ( content ) ;
116
+ if ( parsed . response ?. content ) {
117
+ // Ensure markdown formatting is preserved
118
+ return {
119
+ response : {
120
+ ...parsed . response ,
121
+ // Convert **text** to proper markdown bold syntax if not already
122
+ content : parsed . response . content . replace ( / \* \* ( [ ^ * ] + ) \* \* / g, '**$1**' ) ,
123
+ } ,
124
+ } ;
125
+ }
126
+ return null ;
81
127
} catch {
128
+ // If it's not JSON, return null
82
129
return null ;
83
130
}
84
131
} ;
@@ -271,95 +318,104 @@ export default function Chat() {
271
318
} }
272
319
exit = { { opacity : 0 , y : - 10 , scale : 0.95 } }
273
320
transition = { { duration : 0.2 } }
274
- className = { `flex ${
275
- message . role === 'user' ? 'justify-end' : 'justify-start'
276
- } `}
321
+ className = { `flex ${ message . role === 'user' ? 'justify-end' : 'justify-start' } ` }
277
322
>
278
- < div
279
- className = { cn (
280
- 'max-w-[90%] space-y-2 relative' ,
281
- message . role === 'user'
282
- ? "bg-primary text-primary-foreground rounded-[20px] px-4 py-2.5 before:content-[''] before:absolute before:right-[-8px] before:bottom-0 before:w-[15px] before:h-[20px] before:bg-primary before:rounded-bl-[16px] after:content-[''] after:absolute after:right-[-20px] after:bottom-0 after:w-[20px] after:h-[20px] after:bg-background after:rounded-bl-[10px] after:border-b after:border-l after:border-transparent"
283
- : ''
284
- ) }
285
- >
286
- { message . role === 'user' ? (
287
- < p className = "text-sm whitespace-pre-wrap leading-relaxed" >
323
+ { message . role === 'user' ? (
324
+ < ChatBubble isAssistant = { false } >
325
+ < div
326
+ className = { cn (
327
+ 'text-sm whitespace-pre-wrap leading-relaxed' ,
328
+ 'bg-primary text-primary-foreground rounded-[20px] px-4 py-2.5' ,
329
+ 'relative' ,
330
+ "before:content-[''] before:absolute before:right-[-8px] before:bottom-0 before:w-[15px] before:h-[20px] before:bg-primary before:rounded-bl-[16px]" ,
331
+ "after:content-[''] after:absolute after:right-[-20px] after:bottom-0 after:w-[20px] after:h-[20px] after:bg-background after:rounded-bl-[10px] after:border-b after:border-l after:border-transparent"
332
+ ) }
333
+ >
288
334
{ message . content }
289
- </ p >
290
- ) : structuredResponse ? (
291
- < div className = "space-y-3" >
292
- < div className = "bg-muted rounded-[20px] px-4 py-2.5" >
335
+ </ div >
336
+ </ ChatBubble >
337
+ ) : structuredResponse ? (
338
+ < div className = "max-w-[90%] space-y-3" >
339
+ < ChatBubble
340
+ isAssistant = { true }
341
+ confidence = { structuredResponse . response . confidence }
342
+ >
343
+ < div
344
+ className = { cn (
345
+ 'prose prose-sm max-w-none' ,
346
+ 'prose-neutral dark:prose-invert'
347
+ ) }
348
+ >
293
349
< AnimatedMarkdown
294
350
content = { structuredResponse . response . content }
295
- isAssistant
351
+ isAssistant = { true }
296
352
/>
297
353
</ div >
298
- { structuredResponse . response . sources . length > 0 && (
299
- < motion . div
300
- initial = { { opacity : 0 , height : 0 } }
301
- animate = { { opacity : 1 , height : 'auto' } }
302
- transition = { { duration : 0.3 , delay : 0.2 } }
303
- className = "space-y-2"
304
- >
305
- < p className = "text-xs text-muted-foreground font-medium" >
306
- Sources:
307
- </ p >
308
- < div className = "grid gap-2" >
309
- { structuredResponse . response . sources . map ( ( source , index ) => (
310
- < motion . div
311
- key = { index }
312
- initial = { { opacity : 0 , x : - 10 } }
313
- animate = { { opacity : 1 , x : 0 } }
314
- transition = { { duration : 0.2 , delay : 0.1 * ( index + 1 ) } }
315
- className = "text-xs bg-muted/50 rounded p-2 flex items-start gap-2"
316
- >
317
- < div className = "flex-1" >
318
- < p className = "font-medium" > { source . title } </ p >
319
- { source . description && (
320
- < p className = "text-muted-foreground mt-0.5" >
321
- { source . description }
322
- </ p >
323
- ) }
324
- { source . date && (
325
- < p className = "text-muted-foreground mt-0.5" >
326
- { source . date }
327
- </ p >
328
- ) }
329
- </ div >
330
- { source . url && (
331
- < a
332
- href = { source . url }
333
- target = "_blank"
334
- rel = "noopener noreferrer"
335
- className = "text-primary hover:text-primary/90"
336
- >
337
- < ExternalLink className = "h-3 w-3" />
338
- </ a >
354
+ </ ChatBubble >
355
+ { structuredResponse . response . sources . length > 0 && (
356
+ < motion . div
357
+ initial = { { opacity : 0 , height : 0 } }
358
+ animate = { { opacity : 1 , height : 'auto' } }
359
+ transition = { { duration : 0.3 , delay : 0.2 } }
360
+ className = "space-y-2"
361
+ >
362
+ < p className = "text-xs text-muted-foreground font-medium" > Sources:</ p >
363
+ < div className = "grid gap-2" >
364
+ { structuredResponse . response . sources . map ( ( source , index ) => (
365
+ < motion . div
366
+ key = { index }
367
+ initial = { { opacity : 0 , x : - 10 } }
368
+ animate = { { opacity : 1 , x : 0 } }
369
+ transition = { { duration : 0.2 , delay : 0.1 * ( index + 1 ) } }
370
+ className = "text-xs bg-muted/50 rounded p-2 flex items-start gap-2"
371
+ >
372
+ < div className = "flex-1" >
373
+ < p className = "font-medium" > { source . title } </ p >
374
+ { source . description && (
375
+ < p className = "text-muted-foreground mt-0.5" >
376
+ { source . description }
377
+ </ p >
339
378
) }
340
- </ motion . div >
341
- ) ) }
342
- </ div >
343
- </ motion . div >
344
- ) }
345
- { structuredResponse && ! structuredResponse . response . isRelevant && (
346
- < motion . div
347
- initial = { { opacity : 0 } }
348
- animate = { { opacity : 1 } }
349
- className = "p-3 bg-yellow-100/20 rounded-lg border border-yellow-300/30 text-sm text-yellow-300"
350
- >
351
- < Info className = "h-4 w-4 inline mr-2" />
352
- Let's focus on Vishrut's professional background. Ask about
353
- projects, skills, or experience.
354
- </ motion . div >
355
- ) }
356
- </ div >
357
- ) : (
379
+ { source . date && (
380
+ < p className = "text-muted-foreground mt-0.5" >
381
+ { source . date }
382
+ </ p >
383
+ ) }
384
+ </ div >
385
+ { source . url && (
386
+ < a
387
+ href = { source . url }
388
+ target = "_blank"
389
+ rel = "noopener noreferrer"
390
+ className = "text-primary hover:text-primary/90"
391
+ >
392
+ < ExternalLink className = "h-3 w-3" />
393
+ </ a >
394
+ ) }
395
+ </ motion . div >
396
+ ) ) }
397
+ </ div >
398
+ </ motion . div >
399
+ ) }
400
+ { structuredResponse && ! structuredResponse . response . isRelevant && (
401
+ < motion . div
402
+ initial = { { opacity : 0 } }
403
+ animate = { { opacity : 1 } }
404
+ className = "p-3 bg-yellow-100/20 rounded-lg border border-yellow-300/30 text-sm text-yellow-300"
405
+ >
406
+ < Info className = "h-4 w-4 inline mr-2" />
407
+ Let's focus on Vishrut's professional background. Ask about projects,
408
+ skills, or experience.
409
+ </ motion . div >
410
+ ) }
411
+ </ div >
412
+ ) : (
413
+ < ChatBubble isAssistant = { true } >
358
414
< div className = "bg-muted rounded-lg p-3" >
359
415
< p className = "text-sm whitespace-pre-wrap" > { message . content } </ p >
360
416
</ div >
361
- ) }
362
- </ div >
417
+ </ ChatBubble >
418
+ ) }
363
419
</ motion . div >
364
420
) ;
365
421
} ) }
0 commit comments