diff --git a/apps/web/app/(admin)/chat/[chatId]/page.tsx b/apps/web/app/(admin)/chat/[chatId]/page.tsx index fff73fb..c13a305 100644 --- a/apps/web/app/(admin)/chat/[chatId]/page.tsx +++ b/apps/web/app/(admin)/chat/[chatId]/page.tsx @@ -61,7 +61,7 @@ export default async function Page({ })) as LinkedAccount[]; return ( -
+
diff --git a/apps/web/components/chat/ChatContainer.tsx b/apps/web/components/chat/ChatContainer.tsx index 2a6636d..1f2b145 100644 --- a/apps/web/components/chat/ChatContainer.tsx +++ b/apps/web/components/chat/ChatContainer.tsx @@ -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'; @@ -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(0); const hasSentFirstMessage = useRef(false); const { @@ -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', @@ -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 () => { @@ -154,14 +177,28 @@ export default function ChatContainer({ } return ( - <> - -
- +
+ +
+ +
- + {chatId && ( + + )} +
); } diff --git a/apps/web/components/chat/MessagesList.tsx b/apps/web/components/chat/MessagesList.tsx index 3828895..5684420 100644 --- a/apps/web/components/chat/MessagesList.tsx +++ b/apps/web/components/chat/MessagesList.tsx @@ -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 (
- {messages.map((message) => ( - + {messages.map((message, index) => ( +
+ + + {/* Show visualize button for assistant messages with code that are complete */} + {message.role === 'assistant' && + hasGeneratedCode(message.content) && + isMessageComplete(message.content) && + onVisualizeMessage && ( +
+ onVisualizeMessage(index)} + className="animate-in slide-in-from-left duration-300" + /> +
+ )} +
))}
diff --git a/apps/web/components/chat/RecentChats.tsx b/apps/web/components/chat/RecentChats.tsx index 572e0bf..990f68e 100644 --- a/apps/web/components/chat/RecentChats.tsx +++ b/apps/web/components/chat/RecentChats.tsx @@ -64,7 +64,7 @@ export async function RecentChats() { orderBy: { createdAt: 'desc', }, - take:10, + take: 10, }); if (chats.length === 0) { diff --git a/apps/web/components/chat/VisualizeCodeButton.tsx b/apps/web/components/chat/VisualizeCodeButton.tsx new file mode 100644 index 0000000..5f77d9c --- /dev/null +++ b/apps/web/components/chat/VisualizeCodeButton.tsx @@ -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 ( + + ); +} diff --git a/apps/web/components/chatDrawer/ChatDrawer.tsx b/apps/web/components/chatDrawer/ChatDrawer.tsx new file mode 100644 index 0000000..0b705c2 --- /dev/null +++ b/apps/web/components/chatDrawer/ChatDrawer.tsx @@ -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 ( + + + + + + handleOpenChange(false)} + /> + + + + ); +} diff --git a/apps/web/components/chatDrawer/CodeBlock.tsx b/apps/web/components/chatDrawer/CodeBlock.tsx new file mode 100644 index 0000000..8073588 --- /dev/null +++ b/apps/web/components/chatDrawer/CodeBlock.tsx @@ -0,0 +1,19 @@ +import { cn } from '@repo/ui/lib/utils'; +import { CodeBlockProps } from '@/types/chat'; + +export function CodeBlock({ code, language }: CodeBlockProps) { + return ( +
+      
+        {code}
+      
+    
+ ); +} diff --git a/apps/web/components/chatDrawer/CodeDetectedNotification.tsx b/apps/web/components/chatDrawer/CodeDetectedNotification.tsx new file mode 100644 index 0000000..ebc0280 --- /dev/null +++ b/apps/web/components/chatDrawer/CodeDetectedNotification.tsx @@ -0,0 +1,12 @@ +import { Code2 } from 'lucide-react'; + +export function CodeDetectedNotification() { + return ( +
+
+ + Code preview ready +
+
+ ); +} diff --git a/apps/web/components/chatDrawer/CodeView.tsx b/apps/web/components/chatDrawer/CodeView.tsx new file mode 100644 index 0000000..f4ad3d2 --- /dev/null +++ b/apps/web/components/chatDrawer/CodeView.tsx @@ -0,0 +1,56 @@ +import { CodeViewProps } from '@/types/chat'; +import { CodeBlock } from './CodeBlock'; + +export function CodeView({ + html, + css, + js, + isLoading, +}: CodeViewProps) { + if (isLoading) { + return ( +
+
+
+ ); + } + + const hasCode = html || css || js; + + if (!hasCode) { + return ( +
+ No code available +
+ ); + } + + return ( +
+ {html && ( +
+

+ HTML +

+ +
+ )} + {css && ( +
+

+ CSS +

+ +
+ )} + {js && ( +
+

+ JavaScript +

+ +
+ )} +
+ ); +} diff --git a/apps/web/components/chatDrawer/DrawerContent.tsx b/apps/web/components/chatDrawer/DrawerContent.tsx new file mode 100644 index 0000000..4022d4b --- /dev/null +++ b/apps/web/components/chatDrawer/DrawerContent.tsx @@ -0,0 +1,58 @@ +import { + Tabs, + TabsList, + TabsTrigger, + TabsContent, +} from '@repo/ui/components/tabs'; +import { Code2, Eye } from 'lucide-react'; +import { ExtractedCode } from '@/types/chat'; +import { VisualizationFrame } from './VisualizationFrame'; +import { CodeView } from './CodeView'; + +interface DrawerContentProps { + extractedCode: ExtractedCode; + isLoading: boolean; +} + +export function DrawerContent({ + extractedCode, + isLoading, +}: DrawerContentProps) { + return ( +
+ + + + + Preview + + + + Code + + + + + + + + + +
+ ); +} diff --git a/apps/web/components/chatDrawer/DrawerHeader.tsx b/apps/web/components/chatDrawer/DrawerHeader.tsx new file mode 100644 index 0000000..cf334c9 --- /dev/null +++ b/apps/web/components/chatDrawer/DrawerHeader.tsx @@ -0,0 +1,35 @@ +import { Button } from '@repo/ui/components/button'; +import { DrawerTitle } from '@repo/ui/components/drawer'; +import { X } from 'lucide-react'; + +interface DrawerHeaderProps { + currentPreviewIndex: number | null; + previewMessageIndex: number | null | undefined; + onClose: () => void; +} + +export function DrawerHeader({ + currentPreviewIndex, + previewMessageIndex, + onClose, +}: DrawerHeaderProps) { + const messageNumber = (currentPreviewIndex ?? previewMessageIndex ?? 0) + 1; + const showMessageNumber = + currentPreviewIndex !== null || previewMessageIndex !== null; + + return ( +
+ + Code Preview + {showMessageNumber && ( + + (Message {messageNumber}) + + )} + + +
+ ); +} diff --git a/apps/web/components/chatDrawer/README.md b/apps/web/components/chatDrawer/README.md new file mode 100644 index 0000000..eaea6a9 --- /dev/null +++ b/apps/web/components/chatDrawer/README.md @@ -0,0 +1,97 @@ +# Chat Drawer Component + +This directory contains the ChatDrawer component with proper separation of concerns and global organization. + +## Structure + +``` +chatDrawer/ +├── ChatDrawer.tsx # Main component +├── CodeBlock.tsx # Individual code block display +├── CodeDetectedNotification.tsx # Notification when code is detected +├── CodeView.tsx # Code view with syntax highlighting +├── DrawerContent.tsx # Main drawer content with tabs +├── DrawerHeader.tsx # Drawer header with title and close button +├── VisualizationFrame.tsx # Iframe for code preview +├── index.ts # Component exports +└── README.md # This file +``` + +## Global Organization + +The ChatDrawer component is now properly organized across the global directories: + +### **Hooks** (`@/hooks/useChatDrawer.ts`) + +- Custom hook for managing chat drawer state +- Handles message fetching, code extraction, and auto-opening behavior +- Encapsulates all complex state management logic + +### **Types** (`@/types/chat.ts`) + +- All TypeScript interfaces are centralized +- Includes `ExtractedCode`, `ChatDrawerProps`, `CodeViewProps`, `CodeBlockProps` +- Extends existing chat types + +### **Utils** (`@/utils/codeExtractor.ts`) + +- Code extraction and parsing utilities +- Functions for categorizing and extracting HTML, CSS, and JavaScript +- Message completion checking logic + +### **Components** (`@/components/chatDrawer/`) + +- All UI components are organized in their own directory +- Each component has a single responsibility +- Clean separation between logic and presentation + +## Key Benefits + +1. **Global Accessibility**: Components, hooks, and utilities can be used throughout the application +2. **No Import Conflicts**: Proper organization prevents naming conflicts +3. **Better Maintainability**: Clear separation of concerns +4. **Reusability**: Components and utilities can be reused in other parts of the app +5. **Consistent Structure**: Follows the established project patterns + +## Usage + +```tsx +import { ChatDrawer } from '@/components/chatDrawer'; + +; +``` + +## Import Structure + +```tsx +// Main component +import { ChatDrawer } from '@/components/chatDrawer'; + +// Individual components (if needed) +import { + CodeBlock, + CodeView, + VisualizationFrame, +} from '@/components/chatDrawer'; + +// Hook (if needed elsewhere) +import { useChatDrawer } from '@/hooks/useChatDrawer'; + +// Types +import { ChatDrawerProps, ExtractedCode } from '@/types/chat'; + +// Utils +import { extractCodeFromContent, hasCode } from '@/utils/codeExtractor'; +``` + +## Migration Notes + +- All imports have been updated to use global paths (`@/`) +- No conflicts with existing code +- Maintains all original functionality +- Improved organization and maintainability diff --git a/apps/web/components/chatDrawer/VisualizationFrame.tsx b/apps/web/components/chatDrawer/VisualizationFrame.tsx new file mode 100644 index 0000000..a54893b --- /dev/null +++ b/apps/web/components/chatDrawer/VisualizationFrame.tsx @@ -0,0 +1,53 @@ +import { ExtractedCode } from '@/types/chat'; + +export function VisualizationFrame({ html, css, js }: ExtractedCode) { + if (!html && !css && !js) { + return ( +
+ No code to visualize +
+ ); + } + + const iframeSrcDoc = ` + + + + + + + ${html} + + + + `; + + return ( +
+