Skip to content

Commit 25d1f7f

Browse files
committed
refactor: Enhance chat and markdown rendering with improved components and type safety
1 parent 11b2783 commit 25d1f7f

File tree

2 files changed

+192
-109
lines changed

2 files changed

+192
-109
lines changed

app/components/AnimatedMarkdown.tsx

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ interface AnimatedMarkdownProps {
1717

1818
interface CodeComponentProps {
1919
className?: string;
20-
children: React.ReactNode;
20+
children?: React.ReactNode;
2121
inline?: boolean;
2222
}
2323

@@ -87,22 +87,7 @@ export function AnimatedMarkdown({ content, isAssistant = false }: AnimatedMarkd
8787
const ref = useRef(null);
8888
const isInView = useInView(ref, { once: true });
8989

90-
const processContent = (content: string) => {
91-
try {
92-
const parsed = JSON.parse(content);
93-
if (parsed.response && parsed.response.content) {
94-
return parsed.response.content;
95-
}
96-
} catch {
97-
if (content.includes('```')) {
98-
return content;
99-
}
100-
}
101-
return content;
102-
};
103-
104-
const processedContent = processContent(content);
105-
const hasCodeBlock = processedContent.includes('```');
90+
const hasCodeBlock = content.includes('```');
10691

10792
return (
10893
<motion.div
@@ -113,41 +98,83 @@ export function AnimatedMarkdown({ content, isAssistant = false }: AnimatedMarkd
11398
>
11499
{hasCodeBlock ? (
115100
<div
116-
className={`prose prose-sm max-w-none ${
101+
className={cn(
102+
'prose prose-sm max-w-none',
117103
isAssistant ? 'prose-neutral dark:prose-invert' : 'prose-primary'
118-
}`}
104+
)}
119105
>
120106
<ReactMarkdown
121107
components={{
122-
// @ts-ignore
123-
code({ className, children, inline }: CodeComponentProps) {
108+
code: ({ className, children = '', inline }: CodeComponentProps) => {
124109
if (inline) {
125-
return <code className={className}>{children}</code>;
110+
return (
111+
<code className={cn('bg-muted px-1 py-0.5 rounded text-sm', className)}>
112+
{children}
113+
</code>
114+
);
126115
}
127116
const match = /language-(\w+)/.exec(className || '');
128117
const language = match ? match[1] : '';
129118
const value = String(children).replace(/\n$/, '');
130119
return <CodeBlock language={language} value={value} />;
131120
},
121+
p: ({ children }) => (
122+
<p
123+
className={cn(
124+
'my-2 leading-7',
125+
isAssistant ? 'text-foreground' : 'text-primary-foreground'
126+
)}
127+
>
128+
{children}
129+
</p>
130+
),
131+
ul: ({ children }) => (
132+
<ul className="list-disc list-inside space-y-1 my-2">{children}</ul>
133+
),
134+
ol: ({ children }) => (
135+
<ol className="list-decimal list-inside space-y-1 my-2">{children}</ol>
136+
),
137+
li: ({ children }) => <li className="leading-7">{children}</li>,
138+
a: ({ href, children }) => (
139+
<a
140+
href={href}
141+
target="_blank"
142+
rel="noopener noreferrer"
143+
className="text-primary hover:text-primary/90 underline underline-offset-4"
144+
>
145+
{children}
146+
</a>
147+
),
148+
strong: ({ children }) => <strong className="font-semibold">{children}</strong>,
149+
em: ({ children }) => <em className="italic">{children}</em>,
150+
blockquote: ({ children }) => (
151+
<blockquote className="border-l-4 border-muted pl-4 italic my-2">
152+
{children}
153+
</blockquote>
154+
),
132155
}}
133156
>
134-
{processedContent}
157+
{content}
135158
</ReactMarkdown>
136159
</div>
137160
) : (
138161
<ChatBubble isAssistant={isAssistant}>
139162
<div
140-
className={`prose prose-sm max-w-none ${
163+
className={cn(
164+
'prose prose-sm max-w-none',
141165
isAssistant ? 'prose-neutral dark:prose-invert' : 'prose-primary'
142-
}`}
166+
)}
143167
>
144168
<ReactMarkdown
145169
components={{
146170
p: ({ children }) => <div className="m-0">{children}</div>,
147-
// @ts-ignore
148-
code({ className, children, inline }: CodeComponentProps) {
171+
code: ({ className, children = '', inline }: CodeComponentProps) => {
149172
if (inline) {
150-
return <code className={className}>{children}</code>;
173+
return (
174+
<code className={cn('bg-muted px-1 py-0.5 rounded text-sm', className)}>
175+
{children}
176+
</code>
177+
);
151178
}
152179
const match = /language-(\w+)/.exec(className || '');
153180
const language = match ? match[1] : '';
@@ -156,7 +183,7 @@ export function AnimatedMarkdown({ content, isAssistant = false }: AnimatedMarkd
156183
},
157184
}}
158185
>
159-
{processedContent}
186+
{content}
160187
</ReactMarkdown>
161188
</div>
162189
</ChatBubble>

app/components/Chat.tsx

Lines changed: 136 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useState, useEffect, useRef } from 'react';
55
import { useChat } from 'ai/react';
66
import { motion, AnimatePresence } from 'framer-motion';
77
import { X, ExternalLink, Info, Sparkles } from 'lucide-react';
8+
import ReactMarkdown from 'react-markdown';
89

910
import { 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+
3771
export 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

Comments
 (0)