@@ -5,6 +5,7 @@ import { useState, useEffect, useRef } from 'react';
55import { useChat } from 'ai/react' ;
66import { motion , AnimatePresence } from 'framer-motion' ;
77import { X , ExternalLink , Info , Sparkles } from 'lucide-react' ;
8+ import ReactMarkdown from 'react-markdown' ;
89
910import { cn } from '@/app/lib/utils' ;
1011
@@ -34,6 +35,39 @@ interface ValidationState {
3435 message ?: string ;
3536}
3637
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+
3771export default function Chat ( ) {
3872 const [ isOpen , setIsOpen ] = useState ( false ) ;
3973 const [ validationState , setValidationState ] = useState < ValidationState > ( { isValid : true } ) ;
@@ -77,8 +111,21 @@ export default function Chat() {
77111
78112 const parseResponse = ( content : string ) : StructuredResponse | null => {
79113 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 ;
81127 } catch {
128+ // If it's not JSON, return null
82129 return null ;
83130 }
84131 } ;
@@ -271,95 +318,104 @@ export default function Chat() {
271318 } }
272319 exit = { { opacity : 0 , y : - 10 , scale : 0.95 } }
273320 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' } ` }
277322 >
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+ >
288334 { 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+ >
293349 < AnimatedMarkdown
294350 content = { structuredResponse . response . content }
295- isAssistant
351+ isAssistant = { true }
296352 />
297353 </ 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 >
339378 ) }
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 } >
358414 < div className = "bg-muted rounded-lg p-3" >
359415 < p className = "text-sm whitespace-pre-wrap" > { message . content } </ p >
360416 </ div >
361- ) }
362- </ div >
417+ </ ChatBubble >
418+ ) }
363419 </ motion . div >
364420 ) ;
365421 } ) }
0 commit comments