80 | {isMobile && (
81 |
87 | )}
88 |
89 |
99 |
100 |
101 | {currentSession && (
102 | <>
103 |
104 |
105 |
106 |
107 |
114 |
115 | >
116 | )}
117 |
118 |
119 | {isMobile && isSidebarOpen && (
120 |
setIsSidebarOpen(false)}
123 | />
124 | )}
125 |
126 | );
127 | };
128 |
--------------------------------------------------------------------------------
/src/components/CodeBlock.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button } from './ui/button';
3 | import { Copy, PlayCircle } from 'lucide-react';
4 | import { useToast } from '@/hooks/use-toast';
5 | import { useNavigate } from 'react-router-dom';
6 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
7 | import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
8 |
9 | interface CodeBlockProps {
10 | language: string;
11 | children: string;
12 | }
13 |
14 | // Map common language names to Monaco editor language IDs
15 | const languageMap: { [key: string]: string } = {
16 | js: 'javascript',
17 | javascript: 'javascript',
18 | typescript: 'typescript',
19 | ts: 'typescript',
20 | python: 'python',
21 | py: 'python',
22 | java: 'java',
23 | cpp: 'cpp',
24 | 'c++': 'cpp',
25 | csharp: 'csharp',
26 | cs: 'csharp',
27 | go: 'go',
28 | rust: 'rust',
29 | rs: 'rust',
30 | ruby: 'ruby',
31 | rb: 'ruby',
32 | php: 'php',
33 | sql: 'sql',
34 | html: 'html',
35 | css: 'css',
36 | json: 'json',
37 | markdown: 'markdown',
38 | md: 'markdown',
39 | terraform: 'hcl',
40 | tf: 'hcl',
41 | bicep: 'bicep',
42 | powershell: 'powershell',
43 | ps1: 'powershell',
44 | shell: 'shell',
45 | bash: 'shell',
46 | sh: 'shell',
47 | };
48 |
49 | export const CodeBlock = ({ language, children }: CodeBlockProps) => {
50 | const { toast } = useToast();
51 | const navigate = useNavigate();
52 | const codeText = children.replace(/\n$/, '');
53 |
54 | const handleCopy = async () => {
55 | try {
56 | await navigator.clipboard.writeText(codeText);
57 | toast({
58 | description: "Code copied to clipboard",
59 | duration: 2000,
60 | });
61 | } catch (err) {
62 | console.error('Failed to copy code:', err);
63 | toast({
64 | description: "Failed to copy code",
65 | variant: "destructive",
66 | duration: 2000,
67 | });
68 | }
69 | };
70 |
71 | const handleCopyToPlayground = () => {
72 | localStorage.setItem('playground-code', codeText);
73 | // Map the language to Monaco editor format
74 | const normalizedLang = language.toLowerCase();
75 | const monacoLang = languageMap[normalizedLang] || normalizedLang;
76 |
77 | if (monacoLang) {
78 | localStorage.setItem('playground-language', monacoLang);
79 | }
80 |
81 | navigate('/playground');
82 | toast({
83 | description: "Code copied to playground",
84 | duration: 2000,
85 | });
86 | };
87 |
88 | return (
89 |
90 |
91 |
92 | {language.toUpperCase()}
93 |
94 |
102 |
110 |
111 |
122 | {codeText}
123 |
124 |
125 | );
126 | };
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as DialogPrimitive from "@radix-ui/react-dialog"
3 | import { X } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Dialog = DialogPrimitive.Root
8 |
9 | const DialogTrigger = DialogPrimitive.Trigger
10 |
11 | const DialogPortal = DialogPrimitive.Portal
12 |
13 | const DialogClose = DialogPrimitive.Close
14 |
15 | const DialogOverlay = React.forwardRef<
16 | React.ElementRef
,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
29 |
30 | const DialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, children, ...props }, ref) => (
34 |
35 |
36 |
44 | {children}
45 |
46 |
47 | Close
48 |
49 |
50 |
51 | ))
52 | DialogContent.displayName = DialogPrimitive.Content.displayName
53 |
54 | const DialogHeader = ({
55 | className,
56 | ...props
57 | }: React.HTMLAttributes) => (
58 |
65 | )
66 | DialogHeader.displayName = "DialogHeader"
67 |
68 | const DialogFooter = ({
69 | className,
70 | ...props
71 | }: React.HTMLAttributes) => (
72 |
79 | )
80 | DialogFooter.displayName = "DialogFooter"
81 |
82 | const DialogTitle = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
94 | ))
95 | DialogTitle.displayName = DialogPrimitive.Title.displayName
96 |
97 | const DialogDescription = React.forwardRef<
98 | React.ElementRef,
99 | React.ComponentPropsWithoutRef
100 | >(({ className, ...props }, ref) => (
101 |
106 | ))
107 | DialogDescription.displayName = DialogPrimitive.Description.displayName
108 |
109 | export {
110 | Dialog,
111 | DialogPortal,
112 | DialogOverlay,
113 | DialogClose,
114 | DialogTrigger,
115 | DialogContent,
116 | DialogHeader,
117 | DialogFooter,
118 | DialogTitle,
119 | DialogDescription,
120 | }
121 |
--------------------------------------------------------------------------------
/src/components/playground/EditorHeader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button } from '@/components/ui/button';
3 | import { Copy, Download, Play, Maximize2 } from 'lucide-react';
4 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
5 | import { CardTitle } from '@/components/ui/card';
6 | import { SUPPORTED_LANGUAGES } from './constants';
7 | import { useToast } from '@/hooks/use-toast';
8 |
9 | interface EditorHeaderProps {
10 | language: string;
11 | setLanguage: (value: string) => void;
12 | code: string;
13 | onRun: () => void;
14 | onPopOutput: () => void;
15 | isOutputPopped: boolean;
16 | }
17 |
18 | export const EditorHeader: React.FC = ({
19 | language,
20 | setLanguage,
21 | code,
22 | onRun,
23 | onPopOutput,
24 | isOutputPopped
25 | }) => {
26 | const { toast } = useToast();
27 | const currentLanguage = SUPPORTED_LANGUAGES.find(lang => lang.value === language);
28 | const canRunInBrowser = currentLanguage?.canRunInBrowser ?? false;
29 |
30 | const handleCopy = async () => {
31 | try {
32 | await navigator.clipboard.writeText(code);
33 | toast({
34 | title: "Copied!",
35 | description: "Code copied to clipboard",
36 | });
37 | } catch (err) {
38 | toast({
39 | title: "Error",
40 | description: "Failed to copy code",
41 | variant: "destructive",
42 | });
43 | }
44 | };
45 |
46 | const handleSaveToFile = () => {
47 | try {
48 | const currentLang = SUPPORTED_LANGUAGES.find(lang => lang.value === language);
49 | const extension = currentLang?.extension || '.txt';
50 | const filename = `code${extension}`;
51 |
52 | const blob = new Blob([code], { type: 'text/plain' });
53 | const url = window.URL.createObjectURL(blob);
54 | const link = document.createElement('a');
55 |
56 | link.href = url;
57 | link.download = filename;
58 | document.body.appendChild(link);
59 | link.click();
60 | document.body.removeChild(link);
61 | window.URL.revokeObjectURL(url);
62 |
63 | toast({
64 | description: `File saved as ${filename}`,
65 | });
66 | } catch (err) {
67 | toast({
68 | description: "Failed to save file",
69 | variant: "destructive",
70 | });
71 | }
72 | };
73 |
74 | return (
75 |
76 |
77 | Code Playground
78 |
90 |
91 |
92 | {canRunInBrowser && (
93 |
102 | )}
103 | {canRunInBrowser && (
104 |
113 | )}
114 |
123 |
132 |
133 |
134 | );
135 | };
--------------------------------------------------------------------------------
/src/hooks/useMessageSender.ts:
--------------------------------------------------------------------------------
1 |
2 | import { useState } from 'react';
3 | import { Message } from '@/types/chat';
4 | import { fetchWithTimeout, FETCH_TIMEOUT } from '@/utils/fetchWithTimeout';
5 | import { extractResponseContent } from '@/utils/responseHandler';
6 | import { QueryClient } from '@tanstack/react-query';
7 | import { handleApiResponse, handleApiError } from '@/utils/apiResponseHandler';
8 | import { prepareFileData } from '@/utils/fileOperations';
9 | import { toast } from "sonner";
10 | import { v4 as uuidv4 } from 'uuid';
11 |
12 | export const useMessageSender = (
13 | updateSession: (sessionId: string, messages: Message[]) => void,
14 | queryClient: QueryClient
15 | ) => {
16 | const [isLoading, setIsLoading] = useState(false);
17 | const [isTyping, setIsTyping] = useState(false);
18 |
19 | const sendMessage = async (
20 | input: string,
21 | sessionId: string,
22 | currentMessages: Message[],
23 | file?: File
24 | ) => {
25 | const effectiveWebhookUrl = window.env?.VITE_N8N_WEBHOOK_URL || import.meta.env.VITE_N8N_WEBHOOK_URL;
26 | const username = window.env?.VITE_N8N_WEBHOOK_USERNAME || import.meta.env.VITE_N8N_WEBHOOK_USERNAME;
27 | const secret = window.env?.VITE_N8N_WEBHOOK_SECRET || import.meta.env.VITE_N8N_WEBHOOK_SECRET;
28 |
29 | if (!effectiveWebhookUrl) {
30 | toast.error("Configuration error: No webhook URL available");
31 | return false;
32 | }
33 |
34 | try {
35 | setIsLoading(true);
36 | setIsTyping(true);
37 |
38 | console.log('Processing file for message:', file ? {
39 | name: file.name,
40 | type: file.type,
41 | size: file.size
42 | } : 'No file');
43 |
44 | const fileData = file ? await prepareFileData(file) : null;
45 |
46 | console.log('File data prepared:', fileData ? 'Successfully processed' : 'No file data');
47 |
48 | const userMessage: Message = {
49 | id: uuidv4(),
50 | content: input,
51 | role: "user",
52 | timestamp: Date.now(),
53 | ...(fileData && { imageData: fileData })
54 | };
55 |
56 | const newMessages = [...currentMessages, userMessage];
57 | updateSession(sessionId, newMessages);
58 | queryClient.setQueryData(['chatSessions', sessionId], newMessages);
59 |
60 | const headers: HeadersInit = {
61 | 'Content-Type': 'application/json',
62 | };
63 |
64 | if (username && secret) {
65 | const authString = `${username}:${secret}`;
66 | const base64Auth = btoa(authString);
67 | headers['Authorization'] = `Basic ${base64Auth}`;
68 | }
69 |
70 | console.log('Sending message to webhook:', {
71 | url: effectiveWebhookUrl.split('/webhook/')[0] + '/webhook/[WEBHOOK_ID]',
72 | hasAuth: !!username && !!secret,
73 | hasFile: !!fileData
74 | });
75 |
76 | const response = await fetchWithTimeout(
77 | effectiveWebhookUrl,
78 | {
79 | method: "POST",
80 | headers,
81 | body: JSON.stringify({
82 | chatInput: input,
83 | sessionId: sessionId,
84 | ...(fileData && {
85 | data: fileData.data,
86 | mimeType: fileData.mimeType,
87 | fileName: fileData.fileName
88 | })
89 | }),
90 | },
91 | FETCH_TIMEOUT
92 | );
93 |
94 | const responseData = await handleApiResponse(response);
95 |
96 | if (!response.ok) {
97 | throw new Error(`HTTP error! status: ${response.status}`);
98 | }
99 |
100 | if (!responseData) {
101 | throw new Error('Empty response from server');
102 | }
103 |
104 | const responseContent = extractResponseContent(responseData);
105 |
106 | const assistantMessage: Message = {
107 | id: uuidv4(),
108 | content: responseContent,
109 | role: "assistant",
110 | timestamp: Date.now(),
111 | };
112 |
113 | const finalMessages = [...newMessages, assistantMessage];
114 | updateSession(sessionId, finalMessages);
115 | queryClient.setQueryData(['chatSessions', sessionId], finalMessages);
116 |
117 | console.log('Message sent successfully');
118 | return true;
119 | } catch (error) {
120 | console.error('Error in webhook request:', error);
121 | toast.error("Failed to send message. Please try again.");
122 | return false;
123 | } finally {
124 | setIsLoading(false);
125 | setIsTyping(false);
126 | }
127 | };
128 |
129 | return {
130 | sendMessage,
131 | isLoading,
132 | isTyping
133 | };
134 | };
135 |
--------------------------------------------------------------------------------
/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | import * as SheetPrimitive from "@radix-ui/react-dialog"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 | import { X } from "lucide-react"
4 | import * as React from "react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Sheet = SheetPrimitive.Root
9 |
10 | const SheetTrigger = SheetPrimitive.Trigger
11 |
12 | const SheetClose = SheetPrimitive.Close
13 |
14 | const SheetPortal = SheetPrimitive.Portal
15 |
16 | const SheetOverlay = React.forwardRef<
17 | React.ElementRef,
18 | React.ComponentPropsWithoutRef
19 | >(({ className, ...props }, ref) => (
20 |
28 | ))
29 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
30 |
31 | const sheetVariants = cva(
32 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
33 | {
34 | variants: {
35 | side: {
36 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
37 | bottom:
38 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
39 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
40 | right:
41 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
42 | },
43 | },
44 | defaultVariants: {
45 | side: "right",
46 | },
47 | }
48 | )
49 |
50 | interface SheetContentProps
51 | extends React.ComponentPropsWithoutRef,
52 | VariantProps { }
53 |
54 | const SheetContent = React.forwardRef<
55 | React.ElementRef,
56 | SheetContentProps
57 | >(({ side = "right", className, children, ...props }, ref) => (
58 |
59 |
60 |
65 | {children}
66 |
67 |
68 | Close
69 |
70 |
71 |
72 | ))
73 | SheetContent.displayName = SheetPrimitive.Content.displayName
74 |
75 | const SheetHeader = ({
76 | className,
77 | ...props
78 | }: React.HTMLAttributes) => (
79 |
86 | )
87 | SheetHeader.displayName = "SheetHeader"
88 |
89 | const SheetFooter = ({
90 | className,
91 | ...props
92 | }: React.HTMLAttributes) => (
93 |
100 | )
101 | SheetFooter.displayName = "SheetFooter"
102 |
103 | const SheetTitle = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | SheetTitle.displayName = SheetPrimitive.Title.displayName
114 |
115 | const SheetDescription = React.forwardRef<
116 | React.ElementRef,
117 | React.ComponentPropsWithoutRef
118 | >(({ className, ...props }, ref) => (
119 |
124 | ))
125 | SheetDescription.displayName = SheetPrimitive.Description.displayName
126 |
127 | export {
128 | Sheet, SheetClose,
129 | SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger
130 | }
131 |
132 |
--------------------------------------------------------------------------------
/src/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import type {
4 | ToastActionElement,
5 | ToastProps,
6 | } from "@/components/ui/toast"
7 |
8 | const TOAST_LIMIT = 1
9 | const TOAST_REMOVE_DELAY = 1000000
10 |
11 | type ToasterToast = ToastProps & {
12 | id: string
13 | title?: React.ReactNode
14 | description?: React.ReactNode
15 | action?: ToastActionElement
16 | }
17 |
18 | const actionTypes = {
19 | ADD_TOAST: "ADD_TOAST",
20 | UPDATE_TOAST: "UPDATE_TOAST",
21 | DISMISS_TOAST: "DISMISS_TOAST",
22 | REMOVE_TOAST: "REMOVE_TOAST",
23 | } as const
24 |
25 | let count = 0
26 |
27 | function genId() {
28 | count = (count + 1) % Number.MAX_SAFE_INTEGER
29 | return count.toString()
30 | }
31 |
32 | type ActionType = typeof actionTypes
33 |
34 | type Action =
35 | | {
36 | type: ActionType["ADD_TOAST"]
37 | toast: ToasterToast
38 | }
39 | | {
40 | type: ActionType["UPDATE_TOAST"]
41 | toast: Partial
42 | }
43 | | {
44 | type: ActionType["DISMISS_TOAST"]
45 | toastId?: ToasterToast["id"]
46 | }
47 | | {
48 | type: ActionType["REMOVE_TOAST"]
49 | toastId?: ToasterToast["id"]
50 | }
51 |
52 | interface State {
53 | toasts: ToasterToast[]
54 | }
55 |
56 | const toastTimeouts = new Map>()
57 |
58 | const addToRemoveQueue = (toastId: string) => {
59 | if (toastTimeouts.has(toastId)) {
60 | return
61 | }
62 |
63 | const timeout = setTimeout(() => {
64 | toastTimeouts.delete(toastId)
65 | dispatch({
66 | type: "REMOVE_TOAST",
67 | toastId: toastId,
68 | })
69 | }, TOAST_REMOVE_DELAY)
70 |
71 | toastTimeouts.set(toastId, timeout)
72 | }
73 |
74 | export const reducer = (state: State, action: Action): State => {
75 | switch (action.type) {
76 | case "ADD_TOAST":
77 | return {
78 | ...state,
79 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
80 | }
81 |
82 | case "UPDATE_TOAST":
83 | return {
84 | ...state,
85 | toasts: state.toasts.map((t) =>
86 | t.id === action.toast.id ? { ...t, ...action.toast } : t
87 | ),
88 | }
89 |
90 | case "DISMISS_TOAST": {
91 | const { toastId } = action
92 |
93 | // ! Side effects ! - This could be extracted into a dismissToast() action,
94 | // but I'll keep it here for simplicity
95 | if (toastId) {
96 | addToRemoveQueue(toastId)
97 | } else {
98 | state.toasts.forEach((toast) => {
99 | addToRemoveQueue(toast.id)
100 | })
101 | }
102 |
103 | return {
104 | ...state,
105 | toasts: state.toasts.map((t) =>
106 | t.id === toastId || toastId === undefined
107 | ? {
108 | ...t,
109 | open: false,
110 | }
111 | : t
112 | ),
113 | }
114 | }
115 | case "REMOVE_TOAST":
116 | if (action.toastId === undefined) {
117 | return {
118 | ...state,
119 | toasts: [],
120 | }
121 | }
122 | return {
123 | ...state,
124 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
125 | }
126 | }
127 | }
128 |
129 | const listeners: Array<(state: State) => void> = []
130 |
131 | let memoryState: State = { toasts: [] }
132 |
133 | function dispatch(action: Action) {
134 | memoryState = reducer(memoryState, action)
135 | listeners.forEach((listener) => {
136 | listener(memoryState)
137 | })
138 | }
139 |
140 | type Toast = Omit
141 |
142 | function toast({ ...props }: Toast) {
143 | const id = genId()
144 |
145 | const update = (props: ToasterToast) =>
146 | dispatch({
147 | type: "UPDATE_TOAST",
148 | toast: { ...props, id },
149 | })
150 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
151 |
152 | dispatch({
153 | type: "ADD_TOAST",
154 | toast: {
155 | ...props,
156 | id,
157 | open: true,
158 | onOpenChange: (open) => {
159 | if (!open) dismiss()
160 | },
161 | },
162 | })
163 |
164 | return {
165 | id: id,
166 | dismiss,
167 | update,
168 | }
169 | }
170 |
171 | function useToast() {
172 | const [state, setState] = React.useState(memoryState)
173 |
174 | React.useEffect(() => {
175 | listeners.push(setState)
176 | return () => {
177 | const index = listeners.indexOf(setState)
178 | if (index > -1) {
179 | listeners.splice(index, 1)
180 | }
181 | }
182 | }, [state])
183 |
184 | return {
185 | ...state,
186 | toast,
187 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
188 | }
189 | }
190 |
191 | export { useToast, toast }
192 |
--------------------------------------------------------------------------------
/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { Slot } from "@radix-ui/react-slot"
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form"
12 |
13 | import { cn } from "@/lib/utils"
14 | import { Label } from "@/components/ui/label"
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const { getFieldState, formState } = useFormContext()
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ")
51 | }
52 |
53 | const { id } = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue
71 | )
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
82 |
83 | )
84 | })
85 | FormItem.displayName = "FormItem"
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField()
92 |
93 | return (
94 |
100 | )
101 | })
102 | FormLabel.displayName = "FormLabel"
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/src/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
3 |
4 | import { cn } from "@/lib/utils"
5 | import { buttonVariants } from "@/components/ui/button"
6 |
7 | const AlertDialog = AlertDialogPrimitive.Root
8 |
9 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
10 |
11 | const AlertDialogPortal = AlertDialogPrimitive.Portal
12 |
13 | const AlertDialogOverlay = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef
16 | >(({ className, ...props }, ref) => (
17 |
25 | ))
26 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
27 |
28 | const AlertDialogContent = React.forwardRef<
29 | React.ElementRef,
30 | React.ComponentPropsWithoutRef
31 | >(({ className, ...props }, ref) => (
32 |
33 |
34 |
42 |
43 | ))
44 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
45 |
46 | const AlertDialogHeader = ({
47 | className,
48 | ...props
49 | }: React.HTMLAttributes) => (
50 |
57 | )
58 | AlertDialogHeader.displayName = "AlertDialogHeader"
59 |
60 | const AlertDialogFooter = ({
61 | className,
62 | ...props
63 | }: React.HTMLAttributes) => (
64 |
71 | )
72 | AlertDialogFooter.displayName = "AlertDialogFooter"
73 |
74 | const AlertDialogTitle = React.forwardRef<
75 | React.ElementRef,
76 | React.ComponentPropsWithoutRef
77 | >(({ className, ...props }, ref) => (
78 |
83 | ))
84 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
85 |
86 | const AlertDialogDescription = React.forwardRef<
87 | React.ElementRef,
88 | React.ComponentPropsWithoutRef
89 | >(({ className, ...props }, ref) => (
90 |
95 | ))
96 | AlertDialogDescription.displayName =
97 | AlertDialogPrimitive.Description.displayName
98 |
99 | const AlertDialogAction = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
110 |
111 | const AlertDialogCancel = React.forwardRef<
112 | React.ElementRef,
113 | React.ComponentPropsWithoutRef
114 | >(({ className, ...props }, ref) => (
115 |
124 | ))
125 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
126 |
127 | export {
128 | AlertDialog,
129 | AlertDialogPortal,
130 | AlertDialogOverlay,
131 | AlertDialogTrigger,
132 | AlertDialogContent,
133 | AlertDialogHeader,
134 | AlertDialogFooter,
135 | AlertDialogTitle,
136 | AlertDialogDescription,
137 | AlertDialogAction,
138 | AlertDialogCancel,
139 | }
140 |
--------------------------------------------------------------------------------
/src/components/ChatMessage.tsx:
--------------------------------------------------------------------------------
1 |
2 | import { Message } from '@/types/chat';
3 | import { cn } from '@/lib/utils';
4 | import { Copy, Bot } from 'lucide-react';
5 | import { Button } from './ui/button';
6 | import { useToast } from '@/hooks/use-toast';
7 | import { format } from 'date-fns';
8 | import { MarkdownRenderer } from './chat/MarkdownRenderer';
9 | import {
10 | Dialog,
11 | DialogContent,
12 | DialogTrigger,
13 | } from "@/components/ui/dialog";
14 |
15 | interface ChatMessageProps {
16 | message: Message;
17 | }
18 |
19 | export const ChatMessage = ({ message }: ChatMessageProps) => {
20 | const isAssistant = message.role === 'assistant';
21 | const { toast } = useToast();
22 | const formattedTime = format(new Date(message.timestamp), 'MMM d, yyyy h:mm a');
23 | const assistantName = window.env?.VITE_ASSISTANT_NAME || import.meta.env.VITE_ASSISTANT_NAME || "Lovable";
24 |
25 | const handleCopy = async () => {
26 | try {
27 | const tempDiv = document.createElement('div');
28 | tempDiv.innerHTML = message.content;
29 |
30 | const cleanText = tempDiv.innerText
31 | .replace(/\n{3,}/g, '\n\n')
32 | .trim();
33 |
34 | await navigator.clipboard.writeText(cleanText);
35 |
36 | toast({
37 | description: "Message copied to clipboard",
38 | duration: 2000,
39 | });
40 | } catch (err) {
41 | console.error('Failed to copy text:', err);
42 | toast({
43 | description: "Failed to copy text",
44 | variant: "destructive",
45 | duration: 2000,
46 | });
47 | }
48 | };
49 |
50 | return (
51 |
57 |
72 | {isAssistant && (
73 |
74 |
75 |
76 |
77 |
78 | {assistantName}
79 |
80 |
81 | )}
82 |
83 | {message.imageData && (
84 |
85 |
101 |
102 | )}
103 |
108 |
109 |
110 |
111 | {formattedTime}
112 |
113 | {isAssistant && (
114 |
123 | )}
124 |
125 |
126 |
127 | );
128 | };
129 |
--------------------------------------------------------------------------------
/src/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToastPrimitives from "@radix-ui/react-toast"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 | import { X } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ToastProvider = ToastPrimitives.Provider
9 |
10 | const ToastViewport = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
24 |
25 | const toastVariants = cva(
26 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
27 | {
28 | variants: {
29 | variant: {
30 | default: "border bg-background text-foreground",
31 | destructive:
32 | "destructive group border-destructive bg-destructive text-destructive-foreground",
33 | },
34 | },
35 | defaultVariants: {
36 | variant: "default",
37 | },
38 | }
39 | )
40 |
41 | const Toast = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef &
44 | VariantProps
45 | >(({ className, variant, ...props }, ref) => {
46 | return (
47 |
52 | )
53 | })
54 | Toast.displayName = ToastPrimitives.Root.displayName
55 |
56 | const ToastAction = React.forwardRef<
57 | React.ElementRef,
58 | React.ComponentPropsWithoutRef
59 | >(({ className, ...props }, ref) => (
60 |
68 | ))
69 | ToastAction.displayName = ToastPrimitives.Action.displayName
70 |
71 | const ToastClose = React.forwardRef<
72 | React.ElementRef,
73 | React.ComponentPropsWithoutRef
74 | >(({ className, ...props }, ref) => (
75 |
84 |
85 |
86 | ))
87 | ToastClose.displayName = ToastPrimitives.Close.displayName
88 |
89 | const ToastTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ))
99 | ToastTitle.displayName = ToastPrimitives.Title.displayName
100 |
101 | const ToastDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | ToastDescription.displayName = ToastPrimitives.Description.displayName
112 |
113 | type ToastProps = React.ComponentPropsWithoutRef
114 |
115 | type ToastActionElement = React.ReactElement
116 |
117 | export {
118 | type ToastProps,
119 | type ToastActionElement,
120 | ToastProvider,
121 | ToastViewport,
122 | Toast,
123 | ToastTitle,
124 | ToastDescription,
125 | ToastClose,
126 | ToastAction,
127 | }
128 |
--------------------------------------------------------------------------------
/src/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { type DialogProps } from "@radix-ui/react-dialog"
3 | import { Command as CommandPrimitive } from "cmdk"
4 | import { Search } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { Dialog, DialogContent } from "@/components/ui/dialog"
8 |
9 | const Command = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 | ))
22 | Command.displayName = CommandPrimitive.displayName
23 |
24 | interface CommandDialogProps extends DialogProps {}
25 |
26 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
27 | return (
28 |
35 | )
36 | }
37 |
38 | const CommandInput = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
43 |
44 |
52 |
53 | ))
54 |
55 | CommandInput.displayName = CommandPrimitive.Input.displayName
56 |
57 | const CommandList = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
66 | ))
67 |
68 | CommandList.displayName = CommandPrimitive.List.displayName
69 |
70 | const CommandEmpty = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >((props, ref) => (
74 |
79 | ))
80 |
81 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
82 |
83 | const CommandGroup = React.forwardRef<
84 | React.ElementRef,
85 | React.ComponentPropsWithoutRef
86 | >(({ className, ...props }, ref) => (
87 |
95 | ))
96 |
97 | CommandGroup.displayName = CommandPrimitive.Group.displayName
98 |
99 | const CommandSeparator = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
110 |
111 | const CommandItem = React.forwardRef<
112 | React.ElementRef,
113 | React.ComponentPropsWithoutRef
114 | >(({ className, ...props }, ref) => (
115 |
123 | ))
124 |
125 | CommandItem.displayName = CommandPrimitive.Item.displayName
126 |
127 | const CommandShortcut = ({
128 | className,
129 | ...props
130 | }: React.HTMLAttributes) => {
131 | return (
132 |
139 | )
140 | }
141 | CommandShortcut.displayName = "CommandShortcut"
142 |
143 | export {
144 | Command,
145 | CommandDialog,
146 | CommandInput,
147 | CommandList,
148 | CommandEmpty,
149 | CommandGroup,
150 | CommandItem,
151 | CommandShortcut,
152 | CommandSeparator,
153 | }
154 |
--------------------------------------------------------------------------------
/src/components/ui/navigation-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
3 | import { cva } from "class-variance-authority"
4 | import { ChevronDown } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const NavigationMenu = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
20 | {children}
21 |
22 |
23 | ))
24 | NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
25 |
26 | const NavigationMenuList = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, ...props }, ref) => (
30 |
38 | ))
39 | NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
40 |
41 | const NavigationMenuItem = NavigationMenuPrimitive.Item
42 |
43 | const navigationMenuTriggerStyle = cva(
44 | "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
45 | )
46 |
47 | const NavigationMenuTrigger = React.forwardRef<
48 | React.ElementRef,
49 | React.ComponentPropsWithoutRef
50 | >(({ className, children, ...props }, ref) => (
51 |
56 | {children}{" "}
57 |
61 |
62 | ))
63 | NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
64 |
65 | const NavigationMenuContent = React.forwardRef<
66 | React.ElementRef,
67 | React.ComponentPropsWithoutRef
68 | >(({ className, ...props }, ref) => (
69 |
77 | ))
78 | NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
79 |
80 | const NavigationMenuLink = NavigationMenuPrimitive.Link
81 |
82 | const NavigationMenuViewport = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
87 |
95 |
96 | ))
97 | NavigationMenuViewport.displayName =
98 | NavigationMenuPrimitive.Viewport.displayName
99 |
100 | const NavigationMenuIndicator = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
112 |
113 |
114 | ))
115 | NavigationMenuIndicator.displayName =
116 | NavigationMenuPrimitive.Indicator.displayName
117 |
118 | export {
119 | navigationMenuTriggerStyle,
120 | NavigationMenu,
121 | NavigationMenuList,
122 | NavigationMenuItem,
123 | NavigationMenuContent,
124 | NavigationMenuTrigger,
125 | NavigationMenuLink,
126 | NavigationMenuIndicator,
127 | NavigationMenuViewport,
128 | }
129 |
--------------------------------------------------------------------------------
/src/components/chat/ChatInput.tsx:
--------------------------------------------------------------------------------
1 |
2 | import { Textarea } from "@/components/ui/textarea";
3 | import { Button } from "@/components/ui/button";
4 | import { useRef, useState } from "react";
5 | import { Loader2, Send, Mic, X } from "lucide-react";
6 | import { useSpeechRecognition } from "@/hooks/useSpeechRecognition";
7 | import { ImageUpload } from "./ImageUpload";
8 | import { useToast } from "@/hooks/use-toast";
9 |
10 | interface ChatInputProps {
11 | input: string;
12 | isLoading: boolean;
13 | onInputChange: (value: string) => void;
14 | onSend: (e: React.FormEvent, file?: File) => Promise;
15 | onImageSelect?: (file: File) => void;
16 | }
17 |
18 | export const ChatInput = ({
19 | input,
20 | isLoading,
21 | onInputChange,
22 | onSend,
23 | onImageSelect,
24 | }: ChatInputProps) => {
25 | const textareaRef = useRef(null);
26 | const { toast } = useToast();
27 | const [previewImage, setPreviewImage] = useState<{ file: File; url: string } | null>(null);
28 | const { isListening, startListening } = useSpeechRecognition({
29 | onTranscript: (transcript) => onInputChange(input + transcript)
30 | });
31 |
32 | const handleKeyDown = (e: React.KeyboardEvent) => {
33 | if (e.key === "Enter" && !e.shiftKey) {
34 | e.preventDefault();
35 | handleSubmit(e);
36 | }
37 | };
38 |
39 | const handleSubmit = async (e: React.FormEvent) => {
40 | e.preventDefault();
41 |
42 | if (isLoading) return;
43 |
44 | try {
45 | const result = await onSend(e, previewImage?.file);
46 |
47 | if (result !== false) {
48 | if (previewImage) {
49 | URL.revokeObjectURL(previewImage.url);
50 | setPreviewImage(null);
51 | }
52 | }
53 | } catch (err) {
54 | console.error('Failed to send message:', err);
55 | toast({
56 | description: "Failed to send message",
57 | variant: "destructive",
58 | });
59 | }
60 | };
61 |
62 | const handlePaste = async (e: React.ClipboardEvent) => {
63 | if (!onImageSelect) return;
64 |
65 | const items = e.clipboardData?.items;
66 | const imageItem = Array.from(items).find(item => item.type.indexOf('image') !== -1);
67 |
68 | if (imageItem) {
69 | e.preventDefault();
70 | const file = imageItem.getAsFile();
71 |
72 | if (!file) return;
73 |
74 | if (file.size > 5 * 1024 * 1024) {
75 | toast({
76 | description: "Image must be less than 5MB",
77 | variant: "destructive",
78 | });
79 | return;
80 | }
81 |
82 | handleImageSelection(file);
83 | }
84 | };
85 |
86 | const handleImageSelection = (file: File) => {
87 | const imageUrl = URL.createObjectURL(file);
88 | setPreviewImage({ file, url: imageUrl });
89 |
90 | if (onImageSelect) {
91 | onImageSelect(file);
92 | }
93 | };
94 |
95 | const clearPreviewImage = () => {
96 | if (previewImage) {
97 | URL.revokeObjectURL(previewImage.url);
98 | setPreviewImage(null);
99 | }
100 | };
101 |
102 | const isInputEmpty = !input.trim() && !previewImage?.file;
103 |
104 | return (
105 |
178 | );
179 | };
180 |
--------------------------------------------------------------------------------
/src/components/CodePlayground.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import Editor from '@monaco-editor/react';
3 | import { Card, CardContent, CardHeader } from './ui/card';
4 | import { ResizablePanel, ResizablePanelGroup, ResizableHandle } from './ui/resizable';
5 | import { executeJavaScript, executeHTML } from '@/utils/codeExecutor';
6 | import { useToast } from '@/hooks/use-toast';
7 | import { EditorHeader } from './playground/EditorHeader';
8 | import { PlaygroundOutput } from './playground/PlaygroundOutput';
9 | import { usePopoutWindow } from '@/hooks/usePopoutWindow';
10 | import { SUPPORTED_LANGUAGES } from './playground/constants';
11 |
12 | interface CodePlaygroundProps {
13 | defaultLanguage?: string;
14 | defaultValue?: string;
15 | }
16 |
17 | const CodePlayground: React.FC = ({
18 | defaultLanguage = 'javascript',
19 | defaultValue = '// Write your code here\nconsole.log("Hello, World!");',
20 | }) => {
21 | const [code, setCode] = useState(defaultValue);
22 | const [language, setLanguage] = useState(defaultLanguage);
23 | const [output, setOutput] = useState('');
24 | const [isOutputPopped, setIsOutputPopped] = useState(false);
25 | const { toast } = useToast();
26 | const outputRef = useRef(null);
27 | const iframeRef = useRef(null);
28 | const resizeTimeoutRef = useRef();
29 |
30 | const currentLanguage = SUPPORTED_LANGUAGES.find(lang => lang.value === language);
31 | const canRunInBrowser = currentLanguage?.canRunInBrowser ?? false;
32 |
33 | const { handlePopOutput } = usePopoutWindow(
34 | isOutputPopped,
35 | setIsOutputPopped,
36 | language,
37 | code,
38 | output
39 | );
40 |
41 | useEffect(() => {
42 | const savedCode = localStorage.getItem('playground-code');
43 | const savedLanguage = localStorage.getItem('playground-language');
44 |
45 | if (savedCode) {
46 | setCode(savedCode);
47 | localStorage.removeItem('playground-code');
48 | }
49 |
50 | if (savedLanguage) {
51 | setLanguage(savedLanguage);
52 | localStorage.removeItem('playground-language');
53 | }
54 |
55 | return () => {
56 | if (resizeTimeoutRef.current) {
57 | clearTimeout(resizeTimeoutRef.current);
58 | }
59 | };
60 | }, []);
61 |
62 | const handleRun = async () => {
63 | try {
64 | if (language === 'html') {
65 | if (iframeRef.current) {
66 | const iframe = executeHTML(code);
67 | iframeRef.current.innerHTML = '';
68 | iframeRef.current.appendChild(iframe);
69 | }
70 | return;
71 | }
72 |
73 | const { result, error, logs = [] } = await executeJavaScript(code);
74 | const outputText = [
75 | ...(logs.length > 0 ? logs : []),
76 | ...(result !== undefined ? [result] : []),
77 | ...(error ? [`Error: ${error}`] : [])
78 | ].join('\n');
79 |
80 | setOutput(outputText);
81 |
82 | toast({
83 | description: error ? "Execution failed" : "Code executed successfully",
84 | variant: error ? "destructive" : "default",
85 | });
86 | } catch (err) {
87 | toast({
88 | description: "Failed to execute code",
89 | variant: "destructive",
90 | });
91 | }
92 | };
93 |
94 | const handleResize = () => {
95 | if (resizeTimeoutRef.current) {
96 | clearTimeout(resizeTimeoutRef.current);
97 | }
98 | resizeTimeoutRef.current = setTimeout(() => {
99 | window.dispatchEvent(new Event('resize'));
100 | }, 100);
101 | };
102 |
103 | return (
104 |
105 |
106 |
114 |
115 |
116 |
122 |
127 |
128 | setCode(value || '')}
133 | theme="vs-dark"
134 | options={{
135 | minimap: { enabled: false },
136 | fontSize: 14,
137 | lineNumbers: 'on',
138 | roundedSelection: false,
139 | scrollBeyondLastLine: false,
140 | automaticLayout: true,
141 | padding: { top: 16, bottom: 16 },
142 | fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
143 | }}
144 | />
145 |
146 |
147 | {canRunInBrowser && !isOutputPopped && (
148 | <>
149 |
150 |
155 |
164 |
165 | >
166 | )}
167 |
168 |
169 |
170 | );
171 | };
172 |
173 | export default CodePlayground;
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SelectPrimitive from "@radix-ui/react-select"
3 | import { Check, ChevronDown, ChevronUp } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Select = SelectPrimitive.Root
8 |
9 | const SelectGroup = SelectPrimitive.Group
10 |
11 | const SelectValue = SelectPrimitive.Value
12 |
13 | const SelectTrigger = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef
16 | >(({ className, children, ...props }, ref) => (
17 | span]:line-clamp-1",
21 | className
22 | )}
23 | {...props}
24 | >
25 | {children}
26 |
27 |
28 |
29 |
30 | ))
31 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
32 |
33 | const SelectScrollUpButton = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef
36 | >(({ className, ...props }, ref) => (
37 |
45 |
46 |
47 | ))
48 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
49 |
50 | const SelectScrollDownButton = React.forwardRef<
51 | React.ElementRef,
52 | React.ComponentPropsWithoutRef
53 | >(({ className, ...props }, ref) => (
54 |
62 |
63 |
64 | ))
65 | SelectScrollDownButton.displayName =
66 | SelectPrimitive.ScrollDownButton.displayName
67 |
68 | const SelectContent = React.forwardRef<
69 | React.ElementRef,
70 | React.ComponentPropsWithoutRef
71 | >(({ className, children, position = "popper", ...props }, ref) => (
72 |
73 |
84 |
85 |
92 | {children}
93 |
94 |
95 |
96 |
97 | ))
98 | SelectContent.displayName = SelectPrimitive.Content.displayName
99 |
100 | const SelectLabel = React.forwardRef<
101 | React.ElementRef,
102 | React.ComponentPropsWithoutRef
103 | >(({ className, ...props }, ref) => (
104 |
109 | ))
110 | SelectLabel.displayName = SelectPrimitive.Label.displayName
111 |
112 | const SelectItem = React.forwardRef<
113 | React.ElementRef,
114 | React.ComponentPropsWithoutRef
115 | >(({ className, children, ...props }, ref) => (
116 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | {children}
131 |
132 | ))
133 | SelectItem.displayName = SelectPrimitive.Item.displayName
134 |
135 | const SelectSeparator = React.forwardRef<
136 | React.ElementRef,
137 | React.ComponentPropsWithoutRef
138 | >(({ className, ...props }, ref) => (
139 |
144 | ))
145 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
146 |
147 | export {
148 | Select,
149 | SelectGroup,
150 | SelectValue,
151 | SelectTrigger,
152 | SelectContent,
153 | SelectLabel,
154 | SelectItem,
155 | SelectSeparator,
156 | SelectScrollUpButton,
157 | SelectScrollDownButton,
158 | }
159 |
--------------------------------------------------------------------------------
/src/hooks/useChatSessions.ts:
--------------------------------------------------------------------------------
1 |
2 | import { useState, useEffect } from "react";
3 | import { Message, ChatSession } from "@/types/chat";
4 | import { v4 as uuidv4 } from 'uuid';
5 | import { useMessageSender } from "./useMessageSender";
6 | import { useQueryClient } from "@tanstack/react-query";
7 | import { toast } from "sonner";
8 |
9 | const STORAGE_VERSION = "v1";
10 | const STORAGE_KEY = `chat_sessions_${STORAGE_VERSION}`;
11 |
12 | // Fallback to import.meta.env if window.env is not available
13 | console.log('Configuration Sources:');
14 | console.log('window.env:', {
15 | VITE_N8N_WEBHOOK_URL: window.env?.VITE_N8N_WEBHOOK_URL ? '[CONFIGURED]' : '[NOT SET]',
16 | VITE_WELCOME_MESSAGE: window.env?.VITE_WELCOME_MESSAGE,
17 | VITE_SITE_TITLE: window.env?.VITE_SITE_TITLE,
18 | VITE_N8N_WEBHOOK_USERNAME: window.env?.VITE_N8N_WEBHOOK_USERNAME ? '[CONFIGURED]' : '[NOT SET]',
19 | VITE_N8N_WEBHOOK_SECRET: window.env?.VITE_N8N_WEBHOOK_SECRET ? '[CONFIGURED]' : '[NOT SET]'
20 | });
21 | console.log('import.meta.env:', {
22 | VITE_N8N_WEBHOOK_URL: import.meta.env.VITE_N8N_WEBHOOK_URL ? '[CONFIGURED]' : '[NOT SET]',
23 | VITE_WELCOME_MESSAGE: import.meta.env.VITE_WELCOME_MESSAGE,
24 | VITE_SITE_TITLE: import.meta.env.VITE_SITE_TITLE,
25 | VITE_N8N_WEBHOOK_USERNAME: import.meta.env.VITE_N8N_WEBHOOK_USERNAME ? '[CONFIGURED]' : '[NOT SET]',
26 | VITE_N8N_WEBHOOK_SECRET: import.meta.env.VITE_N8N_WEBHOOK_SECRET ? '[CONFIGURED]' : '[NOT SET]'
27 | });
28 | console.log('DEFAULT_WELCOME_MESSAGE:', "Welcome to the chat!.");
29 |
30 | const WELCOME_MESSAGE = window.env?.VITE_WELCOME_MESSAGE || import.meta.env.VITE_WELCOME_MESSAGE || "Welcome to the chat!";
31 |
32 | console.log('WELCOME_MESSAGE sources:');
33 | console.log('- window.env.VITE_WELCOME_MESSAGE:', window.env?.VITE_WELCOME_MESSAGE);
34 | console.log('- import.meta.env.VITE_WELCOME_MESSAGE:', import.meta.env.VITE_WELCOME_MESSAGE);
35 | console.log('- DEFAULT_WELCOME_MESSAGE:', "Welcome to the chat!.");
36 | console.log('Selected WELCOME_MESSAGE:', WELCOME_MESSAGE);
37 |
38 | export const useChatSessions = () => {
39 | const [sessions, setSessions] = useState([]);
40 | const [currentSessionId, setCurrentSessionId] = useState("");
41 | const queryClient = useQueryClient();
42 |
43 | const updateSession = (sessionId: string, messages: Message[]) => {
44 | setSessions(prev => {
45 | const updatedSessions = prev.map(session =>
46 | session.id === sessionId
47 | ? { ...session, messages, lastUpdated: Date.now() }
48 | : session
49 | );
50 | // Immediately persist to localStorage
51 | localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedSessions));
52 | return updatedSessions;
53 | });
54 | queryClient.setQueryData(['chatSessions', sessionId], messages);
55 | };
56 |
57 | const { sendMessage: sendMessageToWebhook, isLoading, isTyping } = useMessageSender(
58 | updateSession,
59 | queryClient
60 | );
61 |
62 | useEffect(() => {
63 | try {
64 | const savedSessions = localStorage.getItem(STORAGE_KEY);
65 | if (savedSessions) {
66 | const parsed = JSON.parse(savedSessions);
67 | if (Array.isArray(parsed) && parsed.length > 0) {
68 | setSessions(parsed);
69 | setCurrentSessionId(parsed[0].id);
70 | } else {
71 | createNewSession();
72 | }
73 | } else {
74 | createNewSession();
75 | }
76 | } catch (error) {
77 | console.error('Error loading chat sessions:', error);
78 | createNewSession();
79 | }
80 | }, []);
81 |
82 | const createNewSession = () => {
83 | const newSession: ChatSession = {
84 | id: uuidv4(),
85 | messages: [{
86 | id: uuidv4(),
87 | content: WELCOME_MESSAGE,
88 | role: "assistant",
89 | timestamp: Date.now(),
90 | }],
91 | createdAt: Date.now(),
92 | lastUpdated: Date.now(),
93 | };
94 | setSessions(prev => {
95 | const updatedSessions = [newSession, ...prev];
96 | localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedSessions));
97 | return updatedSessions;
98 | });
99 | setCurrentSessionId(newSession.id);
100 | return newSession.id;
101 | };
102 |
103 | const getCurrentSession = () => {
104 | return sessions.find(s => s.id === currentSessionId);
105 | };
106 |
107 | const deleteSession = (sessionId: string) => {
108 | setSessions(prev => {
109 | const remainingSessions = prev.filter(session => session.id !== sessionId);
110 | localStorage.setItem(STORAGE_KEY, JSON.stringify(remainingSessions));
111 |
112 | if (sessionId === currentSessionId) {
113 | if (remainingSessions.length > 0) {
114 | setCurrentSessionId(remainingSessions[0].id);
115 | } else {
116 | // If no sessions remain, create a new one
117 | setTimeout(createNewSession, 0);
118 | }
119 | }
120 |
121 | return remainingSessions;
122 | });
123 | toast.success("Chat deleted successfully");
124 | };
125 |
126 | const renameSession = (sessionId: string, newName: string) => {
127 | setSessions(prev => {
128 | const updatedSessions = prev.map(session =>
129 | session.id === sessionId
130 | ? { ...session, name: newName }
131 | : session
132 | );
133 | localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedSessions));
134 | return updatedSessions;
135 | });
136 | toast.success("Chat renamed successfully");
137 | };
138 |
139 | const sendMessage = async (input: string, file?: File) => {
140 | const currentSession = getCurrentSession();
141 | if (!currentSession) {
142 | console.error('No current session found');
143 | return;
144 | }
145 |
146 | queryClient.setQueryData(['chatSessions', currentSession.id], currentSession.messages);
147 |
148 | await sendMessageToWebhook(
149 | input,
150 | currentSession.id,
151 | currentSession.messages,
152 | file
153 | );
154 | };
155 |
156 | const toggleFavorite = (sessionId: string) => {
157 | setSessions(prev => {
158 | const updatedSessions = prev.map(session =>
159 | session.id === sessionId
160 | ? { ...session, favorite: !session.favorite }
161 | : session
162 | );
163 | localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedSessions));
164 | return updatedSessions;
165 | });
166 | const session = sessions.find(s => s.id === sessionId);
167 | toast.success(session?.favorite ? "Chat removed from favorites" : "Chat added to favorites");
168 | };
169 |
170 | return {
171 | sessions,
172 | currentSessionId,
173 | isLoading,
174 | isTyping,
175 | getCurrentSession,
176 | createNewSession,
177 | deleteSession,
178 | renameSession,
179 | sendMessage,
180 | setCurrentSessionId,
181 | toggleFavorite,
182 | };
183 | };
184 |
--------------------------------------------------------------------------------