Skip to content

Commit

Permalink
refactor: Enhance chat and markdown rendering with improved component…
Browse files Browse the repository at this point in the history
…s and type safety
  • Loading branch information
vishrutkmr7 committed Feb 2, 2025
1 parent 11b2783 commit 25d1f7f
Show file tree
Hide file tree
Showing 2 changed files with 192 additions and 109 deletions.
85 changes: 56 additions & 29 deletions app/components/AnimatedMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface AnimatedMarkdownProps {

interface CodeComponentProps {
className?: string;
children: React.ReactNode;
children?: React.ReactNode;
inline?: boolean;
}

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

const processContent = (content: string) => {
try {
const parsed = JSON.parse(content);
if (parsed.response && parsed.response.content) {
return parsed.response.content;
}
} catch {
if (content.includes('```')) {
return content;
}
}
return content;
};

const processedContent = processContent(content);
const hasCodeBlock = processedContent.includes('```');
const hasCodeBlock = content.includes('```');

return (
<motion.div
Expand All @@ -113,41 +98,83 @@ export function AnimatedMarkdown({ content, isAssistant = false }: AnimatedMarkd
>
{hasCodeBlock ? (
<div
className={`prose prose-sm max-w-none ${
className={cn(
'prose prose-sm max-w-none',
isAssistant ? 'prose-neutral dark:prose-invert' : 'prose-primary'
}`}
)}
>
<ReactMarkdown
components={{
// @ts-ignore
code({ className, children, inline }: CodeComponentProps) {
code: ({ className, children = '', inline }: CodeComponentProps) => {
if (inline) {
return <code className={className}>{children}</code>;
return (
<code className={cn('bg-muted px-1 py-0.5 rounded text-sm', className)}>
{children}
</code>
);
}
const match = /language-(\w+)/.exec(className || '');
const language = match ? match[1] : '';
const value = String(children).replace(/\n$/, '');
return <CodeBlock language={language} value={value} />;
},
p: ({ children }) => (
<p
className={cn(
'my-2 leading-7',
isAssistant ? 'text-foreground' : 'text-primary-foreground'
)}
>
{children}
</p>
),
ul: ({ children }) => (
<ul className="list-disc list-inside space-y-1 my-2">{children}</ul>
),
ol: ({ children }) => (
<ol className="list-decimal list-inside space-y-1 my-2">{children}</ol>
),
li: ({ children }) => <li className="leading-7">{children}</li>,
a: ({ href, children }) => (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:text-primary/90 underline underline-offset-4"
>
{children}
</a>
),
strong: ({ children }) => <strong className="font-semibold">{children}</strong>,
em: ({ children }) => <em className="italic">{children}</em>,
blockquote: ({ children }) => (
<blockquote className="border-l-4 border-muted pl-4 italic my-2">
{children}
</blockquote>
),
}}
>
{processedContent}
{content}
</ReactMarkdown>
</div>
) : (
<ChatBubble isAssistant={isAssistant}>
<div
className={`prose prose-sm max-w-none ${
className={cn(
'prose prose-sm max-w-none',
isAssistant ? 'prose-neutral dark:prose-invert' : 'prose-primary'
}`}
)}
>
<ReactMarkdown
components={{
p: ({ children }) => <div className="m-0">{children}</div>,
// @ts-ignore
code({ className, children, inline }: CodeComponentProps) {
code: ({ className, children = '', inline }: CodeComponentProps) => {
if (inline) {
return <code className={className}>{children}</code>;
return (
<code className={cn('bg-muted px-1 py-0.5 rounded text-sm', className)}>
{children}
</code>
);
}
const match = /language-(\w+)/.exec(className || '');
const language = match ? match[1] : '';
Expand All @@ -156,7 +183,7 @@ export function AnimatedMarkdown({ content, isAssistant = false }: AnimatedMarkd
},
}}
>
{processedContent}
{content}
</ReactMarkdown>
</div>
</ChatBubble>
Expand Down
216 changes: 136 additions & 80 deletions app/components/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useState, useEffect, useRef } from 'react';
import { useChat } from 'ai/react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, ExternalLink, Info, Sparkles } from 'lucide-react';
import ReactMarkdown from 'react-markdown';

import { cn } from '@/app/lib/utils';

Expand Down Expand Up @@ -34,6 +35,39 @@ interface ValidationState {
message?: string;
}

function ChatBubble({
children,
isAssistant,
confidence,
}: {
children: React.ReactNode;
isAssistant: boolean;
confidence?: number;
}) {
const getConfidenceColor = (confidence?: number) => {
if (!confidence || !isAssistant) return null;
if (confidence >= 0.8) return 'bg-green-500';
if (confidence >= 0.5) return 'bg-yellow-500';
return 'bg-red-500';
};

return (
<div className="relative">
<div className={cn('max-w-[85%] w-fit', isAssistant ? 'ml-0 mr-auto' : 'ml-auto mr-0')}>
{children}
</div>
{isAssistant && confidence && (
<div
className={cn(
'absolute -bottom-1 left-4 w-1 h-1 rounded-full',
getConfidenceColor(confidence)
)}
/>
)}
</div>
);
}

export default function Chat() {
const [isOpen, setIsOpen] = useState(false);
const [validationState, setValidationState] = useState<ValidationState>({ isValid: true });
Expand Down Expand Up @@ -77,8 +111,21 @@ export default function Chat() {

const parseResponse = (content: string): StructuredResponse | null => {
try {
return JSON.parse(content);
// If it's already a JSON string, parse it
const parsed = JSON.parse(content);
if (parsed.response?.content) {
// Ensure markdown formatting is preserved
return {
response: {
...parsed.response,
// Convert **text** to proper markdown bold syntax if not already
content: parsed.response.content.replace(/\*\*([^*]+)\*\*/g, '**$1**'),
},
};
}
return null;
} catch {
// If it's not JSON, return null
return null;
}
};
Expand Down Expand Up @@ -271,95 +318,104 @@ export default function Chat() {
}}
exit={{ opacity: 0, y: -10, scale: 0.95 }}
transition={{ duration: 0.2 }}
className={`flex ${
message.role === 'user' ? 'justify-end' : 'justify-start'
}`}
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className={cn(
'max-w-[90%] space-y-2 relative',
message.role === 'user'
? "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"
: ''
)}
>
{message.role === 'user' ? (
<p className="text-sm whitespace-pre-wrap leading-relaxed">
{message.role === 'user' ? (
<ChatBubble isAssistant={false}>
<div
className={cn(
'text-sm whitespace-pre-wrap leading-relaxed',
'bg-primary text-primary-foreground rounded-[20px] px-4 py-2.5',
'relative',
"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"
)}
>
{message.content}
</p>
) : structuredResponse ? (
<div className="space-y-3">
<div className="bg-muted rounded-[20px] px-4 py-2.5">
</div>
</ChatBubble>
) : structuredResponse ? (
<div className="max-w-[90%] space-y-3">
<ChatBubble
isAssistant={true}
confidence={structuredResponse.response.confidence}
>
<div
className={cn(
'prose prose-sm max-w-none',
'prose-neutral dark:prose-invert'
)}
>
<AnimatedMarkdown
content={structuredResponse.response.content}
isAssistant
isAssistant={true}
/>
</div>
{structuredResponse.response.sources.length > 0 && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
transition={{ duration: 0.3, delay: 0.2 }}
className="space-y-2"
>
<p className="text-xs text-muted-foreground font-medium">
Sources:
</p>
<div className="grid gap-2">
{structuredResponse.response.sources.map((source, index) => (
<motion.div
key={index}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.2, delay: 0.1 * (index + 1) }}
className="text-xs bg-muted/50 rounded p-2 flex items-start gap-2"
>
<div className="flex-1">
<p className="font-medium">{source.title}</p>
{source.description && (
<p className="text-muted-foreground mt-0.5">
{source.description}
</p>
)}
{source.date && (
<p className="text-muted-foreground mt-0.5">
{source.date}
</p>
)}
</div>
{source.url && (
<a
href={source.url}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:text-primary/90"
>
<ExternalLink className="h-3 w-3" />
</a>
</ChatBubble>
{structuredResponse.response.sources.length > 0 && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
transition={{ duration: 0.3, delay: 0.2 }}
className="space-y-2"
>
<p className="text-xs text-muted-foreground font-medium">Sources:</p>
<div className="grid gap-2">
{structuredResponse.response.sources.map((source, index) => (
<motion.div
key={index}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.2, delay: 0.1 * (index + 1) }}
className="text-xs bg-muted/50 rounded p-2 flex items-start gap-2"
>
<div className="flex-1">
<p className="font-medium">{source.title}</p>
{source.description && (
<p className="text-muted-foreground mt-0.5">
{source.description}
</p>
)}
</motion.div>
))}
</div>
</motion.div>
)}
{structuredResponse && !structuredResponse.response.isRelevant && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="p-3 bg-yellow-100/20 rounded-lg border border-yellow-300/30 text-sm text-yellow-300"
>
<Info className="h-4 w-4 inline mr-2" />
Let's focus on Vishrut's professional background. Ask about
projects, skills, or experience.
</motion.div>
)}
</div>
) : (
{source.date && (
<p className="text-muted-foreground mt-0.5">
{source.date}
</p>
)}
</div>
{source.url && (
<a
href={source.url}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:text-primary/90"
>
<ExternalLink className="h-3 w-3" />
</a>
)}
</motion.div>
))}
</div>
</motion.div>
)}
{structuredResponse && !structuredResponse.response.isRelevant && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="p-3 bg-yellow-100/20 rounded-lg border border-yellow-300/30 text-sm text-yellow-300"
>
<Info className="h-4 w-4 inline mr-2" />
Let's focus on Vishrut's professional background. Ask about projects,
skills, or experience.
</motion.div>
)}
</div>
) : (
<ChatBubble isAssistant={true}>
<div className="bg-muted rounded-lg p-3">
<p className="text-sm whitespace-pre-wrap">{message.content}</p>
</div>
)}
</div>
</ChatBubble>
)}
</motion.div>
);
})}
Expand Down

1 comment on commit 25d1f7f

@vercel
Copy link

@vercel vercel bot commented on 25d1f7f Feb 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.