Skip to content
Open
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
2 changes: 1 addition & 1 deletion apps/web/app/(admin)/chat/[chatId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default async function Page({
})) as LinkedAccount[];

return (
<div className="pb-6 h-[calc(100vh-161px)]">
<div className="pb-6 h-[calc(100vh-161px)] relative">
<BreadcrumbsConsumer breadcrumbs={breadCrumbs} />
<ChatContainer currentUser={user} linkedAccounts={linkedAccounts} />
</div>
Expand Down
51 changes: 44 additions & 7 deletions apps/web/components/chat/ChatContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { CurrentUser, SelectedAccount } from '@/types/chat';
import { ChatService } from '@/services/client/chat.service';
import { LinkedAccount } from '@/types/linkedAccounts';
import { WelcomeChat } from './WelcomeChat';
import { ChatDrawer } from '@/components/chatDrawer';

const STORED_MESSAGE_KEY = 'firstMessage';

Expand All @@ -28,6 +29,10 @@ export default function ChatContainer({
params.chatId as string,
);
const [isRedirecting, setIsRedirecting] = useState(false);
const [previewedMessageIndex, setPreviewedMessageIndex] = useState<
number | null
>(null);
const [previewTimestamp, setPreviewTimestamp] = useState<number>(0);
const hasSentFirstMessage = useRef(false);

const {
Expand All @@ -38,6 +43,12 @@ export default function ChatContainer({
markLastAssistantMessageAsComplete,
} = useMessages(chatId || '');

// Convert messages to the format expected by ChatDrawer
const drawerMessages = messages.map((msg) => ({
content: msg.content,
role: msg.role as 'user' | 'assistant',
}));

const createNewChat = async (content: string) => {
const response = await fetch('/api/chats', {
method: 'POST',
Expand Down Expand Up @@ -120,6 +131,18 @@ export default function ChatContainer({
}
};

// Handle visualize code button click
const handleVisualizeMessage = (messageIndex: number) => {
// Always set the preview index and timestamp to force update
setPreviewedMessageIndex(messageIndex);
setPreviewTimestamp(Date.now());
};

// Reset previewed message when chat changes
useEffect(() => {
setPreviewedMessageIndex(null);
}, [chatId]);

useEffect(() => {
if (chatId && pathname.includes('/chat/')) {
const run = async () => {
Expand Down Expand Up @@ -154,14 +177,28 @@ export default function ChatContainer({
}

return (
<>
<MessagesList messages={messages} currentUser={currentUser} />
<div className="absolute inset-x-0 bottom-0 border-t overflow-hidden rounded-b-lg bg-background">
<ChatInput
onSendMessage={handleNewMessage}
linkedAccounts={linkedAccounts}
<div className="flex h-full">
<div className="flex-1">
<MessagesList
messages={messages}
currentUser={currentUser}
onVisualizeMessage={handleVisualizeMessage}
/>
<div className="absolute inset-x-0 bottom-0 border-t overflow-hidden rounded-b-lg bg-background">
<ChatInput
onSendMessage={handleNewMessage}
linkedAccounts={linkedAccounts}
/>
</div>
</div>
</>
{chatId && (
<ChatDrawer
messages={messages}
chatId={chatId}
previewMessageIndex={previewedMessageIndex}
previewTimestamp={previewTimestamp}
/>
)}
</div>
);
}
51 changes: 41 additions & 10 deletions apps/web/components/chat/MessagesList.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,50 @@
import { ChatBubble } from '@repo/ui/components/chat-bubble';
import { ScrollToBottom } from './scroll-to-bottom';
import { MessagesListProps } from '@/types/chat';
import { hasGeneratedCode } from '../../utils/codeDetection';
import { VisualizeCodeButton } from './VisualizeCodeButton';

export function MessagesList({ messages, currentUser }: MessagesListProps) {
interface MessagesListWithVisualizeProps extends MessagesListProps {
onVisualizeMessage?: (messageIndex: number) => void;
}

// Function to check if a message is complete (not still streaming)
const isMessageComplete = (content: string): boolean => {
// Check if the message has incomplete code blocks
const codeBlockCount = (content.match(/```/g) || []).length;
return codeBlockCount % 2 === 0; // Even number means all code blocks are closed
};

export function MessagesList({
messages,
currentUser,
onVisualizeMessage,
}: MessagesListWithVisualizeProps) {
return (
<div className="p-4 pb-24 flex flex-col gap-4 max-h-full overflow-y-scroll">
{messages.map((message) => (
<ChatBubble
key={message.id}
variant={message.role}
message={message.content}
userName={message.role === 'user' ? currentUser.name : undefined}
userImage={message.role === 'user' ? currentUser.image : undefined}
isLoading={message.isLoading}
/>
{messages.map((message, index) => (
<div key={message.id} className="flex flex-col gap-2 over">
<ChatBubble
variant={message.role}
message={message.content}
userName={message.role === 'user' ? currentUser.name : undefined}
userImage={message.role === 'user' ? currentUser.image : undefined}
isLoading={message.isLoading}
/>

{/* Show visualize button for assistant messages with code that are complete */}
{message.role === 'assistant' &&
hasGeneratedCode(message.content) &&
isMessageComplete(message.content) &&
onVisualizeMessage && (
<div className="flex justify-start ml-12">
<VisualizeCodeButton
onClick={() => onVisualizeMessage(index)}
className="animate-in slide-in-from-left duration-300"
/>
</div>
)}
</div>
))}
<ScrollToBottom deps={[messages]} />
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/components/chat/RecentChats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export async function RecentChats() {
orderBy: {
createdAt: 'desc',
},
take:10,
take: 10,
});

if (chats.length === 0) {
Expand Down
37 changes: 37 additions & 0 deletions apps/web/components/chat/VisualizeCodeButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client';

import { Button } from '@repo/ui/components/button';
import { Eye, Code2 } from 'lucide-react';
import { cn } from '@repo/ui/lib/utils';

interface VisualizeCodeButtonProps {
onClick: () => void;
className?: string;
variant?: 'default' | 'outline' | 'ghost';
size?: 'sm' | 'default';
}

export function VisualizeCodeButton({
onClick,
className,
variant = 'outline',
size = 'sm',
}: VisualizeCodeButtonProps) {
return (
<Button
variant={variant}
size={size}
onClick={onClick}
className={cn(
'flex items-center gap-2 text-xs transition-all hover:scale-105',
'bg-blue-50 hover:bg-blue-100 dark:bg-blue-900/20 dark:hover:bg-blue-900/30',
'border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-300',
className,
)}
>
<Eye className="h-3 w-3" />
<Code2 className="h-3 w-3" />
Visualize Code
</Button>
);
}
55 changes: 55 additions & 0 deletions apps/web/components/chatDrawer/ChatDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use client';

import {
Drawer,
DrawerContent,
DrawerTrigger,
} from '@repo/ui/components/drawer';
import { Button } from '@repo/ui/components/button';
import { Settings2 } from 'lucide-react';

import { ChatDrawerProps } from '@/types/chat';
import { useChatDrawer } from '@/hooks/useChatDrawer';
import { DrawerHeader, DrawerContent as ChatDrawerContent } from './index';

export function ChatDrawer({
messages: initialMessages,
chatId,
previewMessageIndex,
previewTimestamp,
}: ChatDrawerProps) {
const {
messages,
extractedCode,
open,
isLoading,
currentPreviewIndex,
handleOpenChange,
} = useChatDrawer({
chatId,
initialMessages,
previewMessageIndex,
previewTimestamp,
});

return (
<Drawer direction="right" open={open} onOpenChange={handleOpenChange}>
<DrawerTrigger asChild>
<Button variant="ghost" size="icon" className="absolute right-4 top-4">
<Settings2 className="h-4 w-4" />
</Button>
</DrawerTrigger>
<DrawerContent className="!max-w-xl h-full flex flex-col bg-white dark:bg-gray-900 shadow-2xl border-l border-gray-200 dark:border-gray-800 p-0">
<DrawerHeader
currentPreviewIndex={currentPreviewIndex}
previewMessageIndex={previewMessageIndex}
onClose={() => handleOpenChange(false)}
/>
<ChatDrawerContent
extractedCode={extractedCode}
isLoading={isLoading}
/>
</DrawerContent>
</Drawer>
);
}
19 changes: 19 additions & 0 deletions apps/web/components/chatDrawer/CodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { cn } from '@repo/ui/lib/utils';
import { CodeBlockProps } from '@/types/chat';

export function CodeBlock({ code, language }: CodeBlockProps) {
return (
<pre className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg overflow-x-auto border border-gray-200 dark:border-gray-700">
<code
className={cn(
'text-sm font-mono',
language === 'html' && 'text-blue-600 dark:text-blue-400',
language === 'css' && 'text-green-600 dark:text-green-400',
language === 'javascript' && 'text-yellow-600 dark:text-yellow-400',
)}
>
{code}
</code>
</pre>
);
}
12 changes: 12 additions & 0 deletions apps/web/components/chatDrawer/CodeDetectedNotification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Code2 } from 'lucide-react';

export function CodeDetectedNotification() {
return (
<div className="fixed top-4 right-4 z-50 bg-blue-500 text-white px-3 py-2 rounded-md shadow-lg animate-in slide-in-from-right duration-200">
<div className="flex items-center gap-2">
<Code2 className="h-3 w-3" />
<span className="text-xs font-medium">Code preview ready</span>
</div>
</div>
);
}
56 changes: 56 additions & 0 deletions apps/web/components/chatDrawer/CodeView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { CodeViewProps } from '@/types/chat';
import { CodeBlock } from './CodeBlock';

export function CodeView({
html,
css,
js,
isLoading,
}: CodeViewProps) {
if (isLoading) {
return (
<div className="flex items-center justify-center h-full">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 dark:border-gray-100" />
</div>
);
}

const hasCode = html || css || js;

if (!hasCode) {
return (
<div className="text-center text-gray-500 dark:text-gray-400 py-8">
No code available
</div>
);
}

return (
<div className="flex-1 flex flex-col gap-6 overflow-y-auto">
{html && (
<div>
<h3 className="text-sm font-medium mb-3 text-gray-700 dark:text-gray-300">
HTML
</h3>
<CodeBlock code={html} language="html" />
</div>
)}
{css && (
<div>
<h3 className="text-sm font-medium mb-3 text-gray-700 dark:text-gray-300">
CSS
</h3>
<CodeBlock code={css} language="css" />
</div>
)}
{js && (
<div>
<h3 className="text-sm font-medium mb-3 text-gray-700 dark:text-gray-300">
JavaScript
</h3>
<CodeBlock code={js} language="javascript" />
</div>
)}
</div>
);
}
Loading