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 (
+
+
+
+ );
+}
diff --git a/apps/web/components/chatDrawer/index.ts b/apps/web/components/chatDrawer/index.ts
new file mode 100644
index 0000000..1eed269
--- /dev/null
+++ b/apps/web/components/chatDrawer/index.ts
@@ -0,0 +1,6 @@
+export { ChatDrawer } from './ChatDrawer';
+export { CodeBlock } from './CodeBlock';
+export { CodeView } from './CodeView';
+export { DrawerContent } from './DrawerContent';
+export { DrawerHeader } from './DrawerHeader';
+export { VisualizationFrame } from './VisualizationFrame';
diff --git a/apps/web/components/sidebar/sidebar.tsx b/apps/web/components/sidebar/sidebar.tsx
index 8e1f175..5665f0c 100644
--- a/apps/web/components/sidebar/sidebar.tsx
+++ b/apps/web/components/sidebar/sidebar.tsx
@@ -66,6 +66,7 @@ export function AppSidebar({ recentChatsContent, ...props }: AppSidebarProps) {
+
{/* We create a SidebarGroup for each parent. */}
{data.navMain.map((item) => (
diff --git a/apps/web/hooks/useChatDrawer.ts b/apps/web/hooks/useChatDrawer.ts
new file mode 100644
index 0000000..2893b22
--- /dev/null
+++ b/apps/web/hooks/useChatDrawer.ts
@@ -0,0 +1,186 @@
+import { useState, useEffect, useCallback } from 'react';
+import { Message } from '@/types/chat';
+import {
+ extractCodeFromContent,
+ isMessageComplete,
+ hasCode,
+} from '@/utils/codeExtractor';
+
+interface ExtractedCode {
+ html: string;
+ css: string;
+ js: string;
+}
+
+interface UseChatDrawerProps {
+ chatId: string;
+ initialMessages: Message[];
+ previewMessageIndex?: number | null;
+ previewTimestamp?: number;
+}
+
+interface UseChatDrawerReturn {
+ messages: Message[];
+ extractedCode: ExtractedCode;
+ open: boolean;
+ isLoading: boolean;
+ currentPreviewIndex: number | null;
+ manuallyClosedMessageId: string;
+ handleOpenChange: (newOpen: boolean) => void;
+ setExtractedCode: (code: ExtractedCode) => void;
+}
+
+export const useChatDrawer = ({
+ chatId,
+ initialMessages,
+ previewMessageIndex,
+ previewTimestamp,
+}: UseChatDrawerProps): UseChatDrawerReturn => {
+ const [messages, setMessages] = useState(initialMessages);
+ const [extractedCode, setExtractedCode] = useState({
+ html: '',
+ css: '',
+ js: '',
+ });
+ const [open, setOpen] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const [currentPreviewIndex, setCurrentPreviewIndex] = useState(
+ null,
+ );
+ const [manuallyClosedMessageId, setManuallyClosedMessageId] =
+ useState('');
+ const [lastProcessedMessageId, setLastProcessedMessageId] =
+ useState('');
+
+ const fetchMessages = useCallback(async () => {
+ try {
+ setIsLoading(true);
+ const response = await fetch(`/api/chats/${chatId}`);
+ if (!response.ok) throw new Error('Failed to fetch messages');
+ const { messages: fetchedMessages } = await response.json();
+ setMessages(fetchedMessages);
+ } catch (error) {
+ console.error('Failed to fetch messages:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ }, [chatId]);
+
+ const handleOpenChange = useCallback(
+ (newOpen: boolean) => {
+ setOpen(newOpen);
+ if (!newOpen) {
+ const lastAssistantMessage = [...messages]
+ .reverse()
+ .find((m) => m.role === 'assistant');
+ if (lastAssistantMessage?.id) {
+ setManuallyClosedMessageId(lastAssistantMessage.id);
+ }
+ setCurrentPreviewIndex(null);
+ }
+ },
+ [messages],
+ );
+
+ // Handle external preview message index changes
+ useEffect(() => {
+ if (previewMessageIndex !== null && previewMessageIndex !== undefined) {
+ const message = messages[previewMessageIndex];
+ if (message && message.role === 'assistant') {
+ if (!isMessageComplete(message.content)) {
+ return;
+ }
+
+ const newExtractedCode = extractCodeFromContent(message.content);
+ setExtractedCode(newExtractedCode);
+ setCurrentPreviewIndex(previewMessageIndex);
+ setOpen(true);
+ }
+ }
+ }, [previewMessageIndex, previewTimestamp, messages]);
+
+ // Fetch messages when drawer opens
+ useEffect(() => {
+ if (open) {
+ fetchMessages();
+ }
+ }, [open, fetchMessages]);
+
+ // Update messages when initialMessages changes
+ useEffect(() => {
+ setMessages(initialMessages);
+ }, [initialMessages]);
+
+ // Auto-open drawer when code is detected in completed assistant messages
+ useEffect(() => {
+ // Skip if we're already showing a specific message or if drawer is open
+ if (currentPreviewIndex !== null || previewMessageIndex !== null || open) {
+ return;
+ }
+
+ const lastAssistantMessage = [...messages]
+ .reverse()
+ .find((m) => m.role === 'assistant');
+
+ if (!lastAssistantMessage) {
+ return;
+ }
+
+ // Skip if we've already processed this message
+ if (lastAssistantMessage.id === lastProcessedMessageId) {
+ return;
+ }
+
+ // Skip if message was manually closed
+ if (lastAssistantMessage.id === manuallyClosedMessageId) {
+ return;
+ }
+
+ // Only process complete messages that aren't loading
+ if (
+ !isMessageComplete(lastAssistantMessage.content) ||
+ lastAssistantMessage.isLoading
+ ) {
+ return;
+ }
+
+ const newExtractedCode = extractCodeFromContent(
+ lastAssistantMessage.content,
+ );
+ const hasCodeInMessage = hasCode(newExtractedCode);
+
+ if (hasCodeInMessage) {
+ // Open drawer immediately when code is detected
+ setExtractedCode(newExtractedCode);
+ setLastProcessedMessageId(lastAssistantMessage.id);
+ setOpen(true);
+ }
+ }, [
+ messages,
+ open,
+ currentPreviewIndex,
+ previewMessageIndex,
+ manuallyClosedMessageId,
+ lastProcessedMessageId,
+ ]);
+
+ // Reset states when messages change significantly (new chat)
+ useEffect(() => {
+ if (messages.length <= 1) {
+ setCurrentPreviewIndex(null);
+ setManuallyClosedMessageId('');
+ setLastProcessedMessageId('');
+ }
+ }, [messages.length]);
+
+ return {
+ messages,
+ extractedCode,
+ open,
+ isLoading,
+ currentPreviewIndex,
+ manuallyClosedMessageId,
+ handleOpenChange,
+ setExtractedCode,
+ };
+};
diff --git a/apps/web/services/server/AI/image-generation-tool.definition.ts b/apps/web/services/server/AI/image-generation-tool.definition.ts
new file mode 100644
index 0000000..757a726
--- /dev/null
+++ b/apps/web/services/server/AI/image-generation-tool.definition.ts
@@ -0,0 +1,34 @@
+export const imageGenerationToolDefinition = {
+ type: 'function',
+ function: {
+ name: 'generateImage',
+ description: 'Generate an image based on a text prompt',
+ parameters: {
+ type: 'object',
+ properties: {
+ prompt: {
+ type: 'string',
+ description: 'The text prompt describing the image to generate',
+ },
+ revised_prompt: {
+ type: 'string',
+ description:
+ 'A more detailed or refined version of the original prompt for better results',
+ },
+ size: {
+ type: 'string',
+ enum: ['256x256', '512x512', '1024x1024'],
+ description: 'The size of the generated image',
+ default: '512x512',
+ },
+ quality: {
+ type: 'string',
+ enum: ['standard', 'hd'],
+ description: 'The quality of the generated image',
+ default: 'standard',
+ },
+ },
+ required: ['prompt'],
+ },
+ },
+};
diff --git a/apps/web/services/server/AI/image-generation-tool.execution.ts b/apps/web/services/server/AI/image-generation-tool.execution.ts
new file mode 100644
index 0000000..5f52a1e
--- /dev/null
+++ b/apps/web/services/server/AI/image-generation-tool.execution.ts
@@ -0,0 +1,36 @@
+import OpenAI from 'openai';
+
+interface GenerateImageParams {
+ prompt: string;
+ size?: '256x256' | '512x512' | '1024x1024';
+ quality?: 'standard' | 'hd';
+}
+
+export const imageGenerationToolsExecution = (client: OpenAI) => ({
+ generateImage: async (params: GenerateImageParams) => {
+ try {
+ const response = await client.images.generate({
+ model: 'dall-e-3',
+ prompt: params.prompt,
+ n: 1,
+ size: params.size || '512x512',
+ quality: params.quality || 'standard',
+ });
+
+ if (!response.data?.[0]?.url) {
+ throw new Error('No image URL in response');
+ }
+
+ return {
+ success: true,
+ data: response.data[0].url,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error:
+ error instanceof Error ? error.message : 'Failed to generate image',
+ };
+ }
+ },
+});
diff --git a/apps/web/services/server/AI/openai.service.ts b/apps/web/services/server/AI/openai.service.ts
index 553c307..7627197 100644
--- a/apps/web/services/server/AI/openai.service.ts
+++ b/apps/web/services/server/AI/openai.service.ts
@@ -5,6 +5,8 @@ import {
} from 'openai/resources/chat/completions.mjs';
import { facebookToolDefinition } from './facebook-tools.definition';
import { facebookToolsExecution } from './facebook-tools.execution';
+import { imageGenerationToolDefinition } from './image-generation-tool.definition';
+import { imageGenerationToolsExecution } from './image-generation-tool.execution';
export class OpenAIChatService {
private client: OpenAI;
@@ -21,8 +23,11 @@ export class OpenAIChatService {
}
this.client = new OpenAI({ apiKey, timeout: 30000, maxRetries: 2 });
- this.tools = facebookToolsExecution(accessToken);
- this.instructions = `You're a marketer expert. Help user perform the best marketing actions, you'll provide a very specific plan for the user based on information you'll get through tools. Use the following act_id as ad account id: ${adAccountId} if user didn't provide another one in the message. If you got an error that access token is not provided ask user to make sure they selected an account or try to re-link them`;
+ this.tools = {
+ ...facebookToolsExecution(accessToken),
+ ...imageGenerationToolsExecution(this.client),
+ };
+ this.instructions = `You're a marketer expert. Help user perform the best marketing actions, you'll provide a very specific plan for the user based on information you'll get through tools. Use the following act_id as ad account id: ${adAccountId} if user didn't provide another one in the message. If you got an error that access token is not provided ask user to make sure they selected an account or try to re-link them, if you were asked to generate reports generate page using html, css, js page contains the report and tableau like charts needed for marketer and make the max generated components 500px wide`;
}
async *streamChat(
@@ -38,10 +43,13 @@ export class OpenAIChatService {
},
...messages,
],
- tools: facebookToolDefinition as ChatCompletionTool[],
+ tools: [
+ ...facebookToolDefinition,
+ imageGenerationToolDefinition,
+ ] as ChatCompletionTool[],
stream: true,
temperature: 0.7,
- max_tokens: 1024,
+ // max_tokens: 1024,
stream_options: { include_usage: false },
top_p: 0.9,
frequency_penalty: 0,
diff --git a/apps/web/types/chat.ts b/apps/web/types/chat.ts
index 85de5df..dafc655 100644
--- a/apps/web/types/chat.ts
+++ b/apps/web/types/chat.ts
@@ -54,3 +54,26 @@ export interface CreateMessageData {
role: 'user' | 'assistant';
isLoading?: boolean;
}
+
+// Chat Drawer Types
+export interface ExtractedCode {
+ html: string;
+ css: string;
+ js: string;
+}
+
+export interface ChatDrawerProps {
+ chatId: string;
+ messages: Message[];
+ previewMessageIndex?: number | null;
+ previewTimestamp?: number;
+}
+
+export interface CodeViewProps extends ExtractedCode {
+ isLoading: boolean;
+}
+
+export interface CodeBlockProps {
+ code: string;
+ language: string;
+}
diff --git a/apps/web/utils/codeDetection.ts b/apps/web/utils/codeDetection.ts
new file mode 100644
index 0000000..93a101f
--- /dev/null
+++ b/apps/web/utils/codeDetection.ts
@@ -0,0 +1,45 @@
+// Function to detect if a message contains generated code
+export const hasGeneratedCode = (content: string): boolean => {
+ // Check for code blocks with HTML, CSS, or JavaScript
+ const htmlMatch = content.match(/```html\n([\s\S]*?)```/);
+ const cssMatch = content.match(/```css\n([\s\S]*?)```/);
+ const jsMatch = content.match(/```javascript\n([\s\S]*?)```/);
+ const jsAltMatch = content.match(/```js\n([\s\S]*?)```/);
+
+ // Also check for code blocks without language specification that contain HTML/CSS/JS
+ const genericCodeBlocks = content.match(/```\n([\s\S]*?)```/g);
+ let hasGenericCode = false;
+
+ if (genericCodeBlocks) {
+ for (const block of genericCodeBlocks) {
+ const codeContent = block.replace(/```\n/, '').replace(/```/, '');
+ // Check if the code content looks like HTML, CSS, or JavaScript
+ if (
+ codeContent.includes(' {
+ const newResult = { ...result };
+
+ if (HTML_PATTERNS.some((pattern) => codeContent.includes(pattern))) {
+ newResult.html = codeContent;
+ } else if (CSS_PATTERNS.some((pattern) => codeContent.includes(pattern))) {
+ newResult.css = codeContent;
+ } else if (JS_PATTERNS.some((pattern) => codeContent.includes(pattern))) {
+ newResult.js = codeContent;
+ }
+
+ return newResult;
+};
+
+export const extractCodeFromContent = (content: string): ExtractedCode => {
+ const result: ExtractedCode = { html: '', css: '', js: '' };
+
+ // Extract language-specific code blocks
+ const htmlMatch = content.match(/```html\n([\s\S]*?)```/);
+ const cssMatch = content.match(/```css\n([\s\S]*?)```/);
+ const jsMatch = content.match(/```javascript\n([\s\S]*?)```/);
+ const jsAltMatch = content.match(/```js\n([\s\S]*?)```/);
+
+ result.html = htmlMatch?.[1]?.trim() || '';
+ result.css = cssMatch?.[1]?.trim() || '';
+ result.js = (jsMatch?.[1] || jsAltMatch?.[1])?.trim() || '';
+
+ // Extract and categorize generic code blocks
+ const genericCodeBlocks = content.match(/```\n([\s\S]*?)```/g);
+ if (genericCodeBlocks) {
+ genericCodeBlocks.forEach((block) => {
+ const codeContent = block.replace(/```\n/, '').replace(/```/, '').trim();
+ const categorizedResult = categorizeGenericCode(codeContent, result);
+ Object.assign(result, categorizedResult);
+ });
+ }
+
+ return result;
+};
+
+export const isMessageComplete = (content: string): boolean => {
+ const codeBlockCount = (content.match(/```/g) || []).length;
+ return codeBlockCount % 2 === 0; // Even number means all code blocks are closed
+};
+
+export const hasCode = (extractedCode: ExtractedCode): boolean => {
+ return !!(extractedCode.html || extractedCode.css || extractedCode.js);
+};
diff --git a/packages/ui/src/components/chat-bubble.tsx b/packages/ui/src/components/chat-bubble.tsx
index 4cc28db..6b3cc4d 100644
--- a/packages/ui/src/components/chat-bubble.tsx
+++ b/packages/ui/src/components/chat-bubble.tsx
@@ -6,13 +6,14 @@ import { Loader2 } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import type { Components } from 'react-markdown';
+import { ImagePreviewDialog } from './image-preview-dialog';
const chatBubbleVariants = cva('rounded-lg p-4 max-w-[80%] text-sm', {
variants: {
variant: {
user: 'bg-primary text-primary-foreground ml-auto',
assistant:
- 'bg-muted text-muted-foreground mr-auto prose prose-invert dark:prose-invert max-w-none',
+ 'bg-muted text-muted-foreground mr-auto prose prose-invert dark:prose-invert max-w-none break-words overflow-hidden',
},
},
defaultVariants: {
@@ -27,20 +28,39 @@ interface ChatBubbleProps
userImage?: string;
userName?: string;
isLoading?: boolean;
+ image?: string;
}
const markdownComponents: Components = {
pre: ({ children }) => (
- {children}
+
+ {children}
+
),
code: ({ children, className }) => {
const isInline = !className;
return isInline ? (
- {children}
+
+ {children}
+
) : (
- {children}
+ {children}
);
},
+ p: ({ children }) => (
+ {children}
+ ),
+ img: ({ src, alt }) => (
+
+
+
+
+
+ ),
};
export function ChatBubble({
@@ -50,6 +70,7 @@ export function ChatBubble({
userImage,
userName,
isLoading,
+ image,
...props
}: ChatBubbleProps) {
return (
@@ -70,16 +91,29 @@ export function ChatBubble({
)}
{...props}
>
-
+
{variant === 'user' ? (
- message.split('\n').map((line, i) => (
-
- {line}
- {i < message.split('\n').length - 1 &&
}
-
- ))
+ <>
+ {image && (
+
+
+
+
+
+ )}
+ {message.split('\n').map((line, i) => (
+
+ {line}
+ {i < message.split('\n').length - 1 &&
}
+
+ ))}
+ >
) : (
-
+
+
+ {children}
+
+
+
+

e.stopPropagation()}
+ />
+
+
+
+
+
+
+ );
+}