Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 25 additions & 14 deletions src/components/EnhancedChatInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { toast } from 'sonner';

// Import extracted components
import { ChatSidebar } from './chat/ChatSidebar';
import { ChatMessage } from './chat/ChatMessage';
import { EnhancedChatMessage } from './chat/EnhancedChatMessage';
import { ChatInput } from './chat/ChatInput';
import { WelcomeScreen } from './chat/WelcomeScreen';
import { ErrorBoundary } from './ErrorBoundary';
Expand Down Expand Up @@ -398,19 +398,30 @@ const EnhancedChatInterface: React.FC = () => {
{/* Messages area */}
<ScrollArea className="flex-1 custom-scrollbar">
<div className="p-6 space-y-6 max-w-4xl mx-auto">
{memoizedMessages.map((message, index) => (
<ChatMessage
key={message._id}
message={message}
user={user}
copiedMessage={copiedMessage}
setCopiedMessage={setCopiedMessage}
isLast={index === memoizedMessages.length - 1}
handleApproveDiagram={handleApproveDiagram}
handleRequestDiagramChanges={handleRequestDiagramChanges}
isSubmittingDiagram={isSubmittingDiagram}
/>
))}
{memoizedMessages.map((message, index) => {
const isUser = message.role === 'user';
const isFirstInGroup = index === 0 || memoizedMessages[index - 1].role !== message.role;

return (
<EnhancedChatMessage
key={message._id}
message={message}
isUser={isUser}
isFirstInGroup={isFirstInGroup}
formatTimestamp={formatTimestamp}
copyToClipboard={async (text: string, messageId: string) => {
await navigator.clipboard.writeText(text);
setCopiedMessage(messageId);
setTimeout(() => setCopiedMessage(null), 2000);
toast.success('Message copied to clipboard!');
}}
copiedMessage={copiedMessage}
onApproveDiagram={handleApproveDiagram}
onRequestDiagramChanges={handleRequestDiagramChanges}
isSubmittingDiagram={isSubmittingDiagram}
/>
);
})}

{typingIndicator}

Expand Down
274 changes: 274 additions & 0 deletions src/components/chat/EnhancedChatMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import { memo, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';

Check failure

Code scanning / ESLint

Disallow unused variables Error

'ScrollArea' is defined but never used.
import { CodeArtifact } from '@/components/ui/code-artifact';
import { executeCode } from '@/lib/sandbox';

import {
Copy,
Check,
Play,

Check failure

Code scanning / ESLint

Disallow unused variables Error

'Play' is defined but never used.
Code2,
Sparkles,
FileText,

Check failure

Code scanning / ESLint

Disallow unused variables Error

'FileText' is defined but never used.
Image,

Check failure

Code scanning / ESLint

Disallow unused variables Error

'Image' is defined but never used.
Terminal,

Check failure

Code scanning / ESLint

Disallow unused variables Error

'Terminal' is defined but never used.
ChevronDown,
ChevronUp
} from 'lucide-react';
import { toast } from 'sonner';

Check failure

Code scanning / ESLint

Disallow unused variables Error

'toast' is defined but never used.
import DiagramMessageComponent from '../DiagramMessageComponent';

// Types for the enhanced message component
interface ConvexMessage {
_id: string;
content: string | unknown;
role: 'user' | 'assistant';
createdAt: number;
metadata?: {
model?: string;
tokens?: number;
cost?: number;
diagramData?: unknown;
};
}

interface EnhancedChatMessageProps {
message: ConvexMessage;
isUser: boolean;
isFirstInGroup: boolean;
formatTimestamp: (timestamp: number) => string;
copyToClipboard: (text: string, messageId: string) => Promise<void>;
copiedMessage: string | null;
onApproveDiagram?: (messageId: string) => Promise<void>;
onRequestDiagramChanges?: (messageId: string, feedback: string) => Promise<void>;
isSubmittingDiagram?: boolean;
}

// Enhanced message component with modern AI tool styling
export const EnhancedChatMessage = memo<EnhancedChatMessageProps>(({
message,
isUser,
isFirstInGroup,
formatTimestamp,
copyToClipboard,
copiedMessage,
onApproveDiagram,
onRequestDiagramChanges,
isSubmittingDiagram
}) => {
const [showCodeArtifacts, setShowCodeArtifacts] = useState(true);
const [isExpanded, setIsExpanded] = useState(false);

// Extract code blocks from message content
const extractCodeBlocks = (content: string) => {
const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;

Check failure

Code scanning / ESLint

Detects potentially unsafe regular expressions, which may take a very long time to run, blocking the event loop. Error

Unsafe Regular Expression
const codeBlocks: Array<{ language: string; code: string; id: string }> = [];
let match;

while ((match = codeBlockRegex.exec(content)) !== null) {
const language = match[1] || 'plaintext';
const code = match[2];
codeBlocks.push({
language,
code: code.trim(),
id: `code-${Math.random().toString(36).substr(2, 9)}`
});
}

return codeBlocks;
};

// Remove code blocks from text content for display
const cleanContent = (content: string) => {
return content.replace(/```(\w+)?\n[\s\S]*?```/g, '').trim();

Check failure

Code scanning / ESLint

Detects potentially unsafe regular expressions, which may take a very long time to run, blocking the event loop. Error

Unsafe Regular Expression
};

const codeBlocks = extractCodeBlocks(typeof message.content === 'string' ? message.content : '');
const textContent = cleanContent(typeof message.content === 'string' ? message.content : '');

return (
<div className="space-y-4">
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ duration: 0.3 }}
whileHover={{ scale: 1.01 }}
className="group"
>
<Card className={`
transition-all duration-300 shadow-soft hover:shadow-medium border-0
backdrop-blur-xl
${isUser
? 'chat-bubble-user ml-auto max-w-[90%]'
: 'chat-bubble-assistant max-w-[95%]'
}
${isFirstInGroup ? 'rounded-2xl' : (isUser ? 'rounded-2xl rounded-tr-md' : 'rounded-2xl rounded-tl-md')}
`}>
<CardContent className="p-6 relative">
<div className="space-y-4">
{/* Message Header */}
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
{isUser ? (
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center">
<span className="text-white font-medium text-sm">U</span>
</div>
) : (
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-green-500 to-emerald-600 flex items-center justify-center">
<Sparkles className="w-4 h-4 text-white" />
</div>
)}
<span className="text-sm font-medium text-gray-300">
{isUser ? 'You' : 'ZapDev AI'}
</span>
</div>

{/* Action Buttons */}
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-all duration-300">
{codeBlocks.length > 0 && (
<Button
variant="ghost"
size="sm"
onClick={() => setShowCodeArtifacts(!showCodeArtifacts)}
className="h-8 w-8 p-0 glass-hover rounded-lg border border-white/10 hover:border-white/20 hover:bg-white/5"
>
<Code2 className="w-4 h-4" />
</Button>
)}

<Button
variant="ghost"
size="sm"
onClick={() => setIsExpanded(!isExpanded)}
className="h-8 w-8 p-0 glass-hover rounded-lg border border-white/10 hover:border-white/20 hover:bg-white/5"
>
{isExpanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</Button>

<Button
variant="ghost"
size="sm"
onClick={() => copyToClipboard(typeof message.content === 'string' ? message.content : String(message.content), message._id)}
className="h-8 w-8 p-0 glass-hover rounded-lg border border-white/10 hover:border-white/20 hover:bg-white/5"
>
{copiedMessage === message._id ? (
<Check className="w-4 h-4 text-green-400" />
) : (
<Copy className="w-4 h-4" />
)}
</Button>
</div>
</div>

{/* Message Content */}
{textContent && (
<div
className={`text-base leading-relaxed font-medium prose prose-invert max-w-none ${
isUser ? 'text-foreground' : 'text-foreground/95'
}`}
>
{textContent}
</div>
)}

{/* Code Artifacts */}
<AnimatePresence>
{showCodeArtifacts && codeBlocks.length > 0 && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
className="space-y-3"
>
{codeBlocks.map((block, index) => (
<CodeArtifact
key={block.id}
code={block.code}
language={block.language}
title={`${block.language.charAt(0).toUpperCase() + block.language.slice(1)} Code Block ${index + 1}`}
onExecute={executeCode}
autoRun={false}
showLineNumbers={true}
className="mt-3"
/>
))}
</motion.div>
)}
</AnimatePresence>

{/* Metadata */}
<AnimatePresence>
{isExpanded && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
className="pt-3 border-t border-white/10"
>
<div className="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
{message.metadata?.model && (
<Badge variant="outline" className="glass text-xs border-white/20 bg-white/5 backdrop-blur-sm rounded-lg px-2 py-1">
{String(message.metadata.model)}
</Badge>
)}

{message.metadata?.tokens && (
<Badge variant="outline" className="glass text-xs border-white/20 bg-white/5 backdrop-blur-sm rounded-lg px-2 py-1">
{String(message.metadata.tokens)} tokens
</Badge>
)}

{message.metadata?.cost && (
<Badge variant="outline" className="glass text-xs border-green-400/30 text-green-300 bg-green-500/10 backdrop-blur-sm rounded-lg px-2 py-1">
${String(message.metadata.cost.toFixed(4))}
</Badge>
)}

{message.metadata?.diagramData && (
<Badge variant="outline" className="glass text-xs text-blue-300 border border-blue-400/30 bg-blue-500/10 backdrop-blur-sm rounded-lg px-2 py-1">
Contains Diagram
</Badge>
)}

{codeBlocks.length > 0 && (
<Badge variant="outline" className="glass text-xs border-purple-400/30 text-purple-300 bg-purple-500/10 backdrop-blur-sm rounded-lg px-2 py-1">
{codeBlocks.length} Code Block{codeBlocks.length > 1 ? 's' : ''}
</Badge>
)}
</div>
</motion.div>
)}
</AnimatePresence>
</div>

{/* Timestamp */}
<div className={`text-xs text-muted-foreground/70 mt-3 ${
isUser ? 'text-right' : 'text-left'
}`}>
{String(formatTimestamp(message.createdAt))}
</div>
</CardContent>
</Card>
</motion.div>

{/* Diagram Component */}
{onApproveDiagram && onRequestDiagramChanges && message.metadata?.diagramData && (
<DiagramMessageComponent
diagramData={message.metadata.diagramData as any}

Check failure

Code scanning / ESLint

Disallow the `any` type Error

Unexpected any. Specify a different type.
messageId={message._id}
onApprove={onApproveDiagram}
onRequestChanges={onRequestDiagramChanges}
isSubmitting={isSubmittingDiagram}
/>
)}
</div>
);
});

EnhancedChatMessage.displayName = 'EnhancedChatMessage';
Loading