87 | >(({ className, ...props }, ref) => (
88 | | [role=checkbox]]:translate-y-[2px]",
92 | className
93 | )}
94 | {...props}
95 | />
96 | ))
97 | TableCell.displayName = "TableCell"
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | TableCaption.displayName = "TableCaption"
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | }
121 |
--------------------------------------------------------------------------------
/utils/diff.ts:
--------------------------------------------------------------------------------
1 | import { createTwoFilesPatch } from 'diff';
2 | import type { FileMap } from '@/lib/stores/files';
3 | import { MODIFICATIONS_TAG_NAME } from './constants';
4 |
5 | export const modificationsRegex = new RegExp(
6 | `^<${MODIFICATIONS_TAG_NAME}>[\\s\\S]*?<\\/${MODIFICATIONS_TAG_NAME}>\\s+`,
7 | 'g',
8 | );
9 |
10 | interface ModifiedFile {
11 | type: 'diff' | 'file';
12 | content: string;
13 | }
14 |
15 | type FileModifications = Record;
16 |
17 | export function computeFileModifications(files: FileMap, modifiedFiles: Map) {
18 | const modifications: FileModifications = {};
19 |
20 | let hasModifiedFiles = false;
21 |
22 | for (const [filePath, originalContent] of modifiedFiles) {
23 | const file = files[filePath];
24 |
25 | if (file?.type !== 'file') {
26 | continue;
27 | }
28 |
29 | const unifiedDiff = diffFiles(filePath, originalContent, file.content);
30 |
31 | if (!unifiedDiff) {
32 | // files are identical
33 | continue;
34 | }
35 |
36 | hasModifiedFiles = true;
37 |
38 | if (unifiedDiff.length > file.content.length) {
39 | // if there are lots of changes we simply grab the current file content since it's smaller than the diff
40 | modifications[filePath] = { type: 'file', content: file.content };
41 | } else {
42 | // otherwise we use the diff since it's smaller
43 | modifications[filePath] = { type: 'diff', content: unifiedDiff };
44 | }
45 | }
46 |
47 | if (!hasModifiedFiles) {
48 | return undefined;
49 | }
50 |
51 | return modifications;
52 | }
53 |
54 | /**
55 | * Computes a diff in the unified format. The only difference is that the header is omitted
56 | * because it will always assume that you're comparing two versions of the same file and
57 | * it allows us to avoid the extra characters we send back to the llm.
58 | *
59 | * @see https://www.gnu.org/software/diffutils/manual/html_node/Unified-Format.html
60 | */
61 | export function diffFiles(fileName: string, oldFileContent: string, newFileContent: string) {
62 | let unifiedDiff = createTwoFilesPatch(fileName, fileName, oldFileContent, newFileContent);
63 |
64 | const patchHeaderEnd = `--- ${fileName}\n+++ ${fileName}\n`;
65 | const headerEndIndex = unifiedDiff.indexOf(patchHeaderEnd);
66 |
67 | if (headerEndIndex >= 0) {
68 | unifiedDiff = unifiedDiff.slice(headerEndIndex + patchHeaderEnd.length);
69 | }
70 |
71 | if (unifiedDiff === '') {
72 | return undefined;
73 | }
74 |
75 | return unifiedDiff;
76 | }
77 |
78 | /**
79 | * Converts the unified diff to HTML.
80 | *
81 | * Example:
82 | *
83 | * ```html
84 | *
85 | *
86 | * - console.log('Hello, World!');
87 | * + console.log('Hello, BoltNext!');
88 | *
89 | *
90 | * ```
91 | */
92 | export function fileModificationsToHTML(modifications: FileModifications) {
93 | const entries = Object.entries(modifications);
94 |
95 | if (entries.length === 0) {
96 | return undefined;
97 | }
98 |
99 | const result: string[] = [`<${MODIFICATIONS_TAG_NAME}>`];
100 |
101 | for (const [filePath, { type, content }] of entries) {
102 | result.push(`<${type} path=${JSON.stringify(filePath)}>`, content, `${type}>`);
103 | }
104 |
105 | result.push(`${MODIFICATIONS_TAG_NAME}>`);
106 |
107 | return result.join('\n');
108 | }
109 |
--------------------------------------------------------------------------------
/components/workbench/terminal/Terminal.tsx:
--------------------------------------------------------------------------------
1 | import { FitAddon } from '@xterm/addon-fit';
2 | import { WebLinksAddon } from '@xterm/addon-web-links';
3 | import { Terminal as XTerm } from '@xterm/xterm';
4 | import { forwardRef, memo, useEffect, useImperativeHandle, useRef, useState } from 'react';
5 | import type { Theme } from '@/lib/stores/theme';
6 | import { createScopedLogger } from '@/utils/logger';
7 | import { getTerminalTheme } from './theme';
8 |
9 | const logger = createScopedLogger('Terminal');
10 |
11 | export interface TerminalRef {
12 | reloadStyles: () => void;
13 | }
14 |
15 | export interface TerminalProps {
16 | className?: string;
17 | theme: Theme;
18 | readonly?: boolean;
19 | onTerminalReady?: (terminal: XTerm) => void;
20 | onTerminalResize?: (cols: number, rows: number) => void;
21 | }
22 |
23 | export const Terminal = memo(
24 | forwardRef(({ className, theme, readonly, onTerminalReady, onTerminalResize }, ref) => {
25 | const terminalElementRef = useRef(null);
26 | const [term, setTerm] = useState(null);
27 | const fitAddonRef = useRef(null);
28 | const webLinksAddonRef = useRef(null);
29 |
30 | useEffect(() => {
31 | const terminal = new XTerm({
32 | cursorBlink: true,
33 | convertEol: true,
34 | disableStdin: readonly,
35 | theme: getTerminalTheme(readonly ? { cursor: '#00000000' } : {}),
36 | fontSize: 12,
37 | fontFamily: 'Menlo, courier-new, courier, monospace',
38 | });
39 | const fitAddon = new FitAddon();
40 | const webLinksAddon = new WebLinksAddon();
41 |
42 | fitAddonRef.current = fitAddon;
43 | webLinksAddonRef.current = webLinksAddon;
44 |
45 | terminal.loadAddon(fitAddon);
46 | terminal.loadAddon(webLinksAddon);
47 |
48 | setTerm(terminal);
49 |
50 | return () => {
51 | if (terminal) {
52 | terminal.dispose();
53 | }
54 | };
55 | }, []);
56 |
57 | useEffect(() => {
58 | const element = terminalElementRef.current;
59 | if (term && element) {
60 | term.open(element);
61 | setTimeout(() => {
62 | fitAddonRef.current?.fit();
63 | onTerminalResize?.(term.cols, term.rows);
64 | }, 50);
65 |
66 | const resizeObserver = new ResizeObserver(() => {
67 | fitAddonRef.current?.fit();
68 | onTerminalResize?.(term.cols, term.rows);
69 | });
70 |
71 | resizeObserver.observe(element);
72 |
73 | logger.info('Attach terminal');
74 |
75 | onTerminalReady?.(term);
76 |
77 | return () => {
78 | resizeObserver.disconnect();
79 | };
80 | }
81 | }, [term]);
82 |
83 |
84 | useEffect(() => {
85 | if (term) {
86 | term.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {});
87 | term.options.disableStdin = readonly;
88 | }
89 | }, [theme, readonly, term]);
90 |
91 | useImperativeHandle(ref, () => {
92 | return {
93 | reloadStyles: () => {
94 | if (term) {
95 | term.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {});
96 | } else {
97 | logger.warn('Terminal instance not found');
98 | }
99 | },
100 | };
101 | }, [readonly, term]);
102 |
103 | return ;
104 | }),
105 | );
--------------------------------------------------------------------------------
/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { Drawer as DrawerPrimitive } from "vaul"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Drawer = ({
9 | shouldScaleBackground = true,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
16 | )
17 | Drawer.displayName = "Drawer"
18 |
19 | const DrawerTrigger = DrawerPrimitive.Trigger
20 |
21 | const DrawerPortal = DrawerPrimitive.Portal
22 |
23 | const DrawerClose = DrawerPrimitive.Close
24 |
25 | const DrawerOverlay = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
34 | ))
35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
36 |
37 | const DrawerContent = React.forwardRef<
38 | React.ElementRef,
39 | Omit, 'tabClassName'> & { tabClassName?: string }
40 | >(({ className, tabClassName, children, ...props }, ref) => (
41 |
42 |
43 |
51 |
52 | {children}
53 |
54 |
55 | ))
56 | DrawerContent.displayName = "DrawerContent"
57 |
58 | const DrawerHeader = ({
59 | className,
60 | ...props
61 | }: React.HTMLAttributes) => (
62 |
66 | )
67 | DrawerHeader.displayName = "DrawerHeader"
68 |
69 | const DrawerFooter = ({
70 | className,
71 | ...props
72 | }: React.HTMLAttributes) => (
73 |
77 | )
78 | DrawerFooter.displayName = "DrawerFooter"
79 |
80 | const DrawerTitle = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef
83 | >(({ className, ...props }, ref) => (
84 |
92 | ))
93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName
94 |
95 | const DrawerDescription = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, ...props }, ref) => (
99 |
104 | ))
105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName
106 |
107 | export {
108 | Drawer,
109 | DrawerPortal,
110 | DrawerOverlay,
111 | DrawerTrigger,
112 | DrawerClose,
113 | DrawerContent,
114 | DrawerHeader,
115 | DrawerFooter,
116 | DrawerTitle,
117 | DrawerDescription,
118 | }
119 |
--------------------------------------------------------------------------------
/components/sidebar/nav-user.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | BadgeCheck,
5 | Bell,
6 | ChevronsUpDown,
7 | CreditCard,
8 | LogOut,
9 | Sparkles,
10 | } from "lucide-react"
11 |
12 | import {
13 | Avatar,
14 | AvatarFallback,
15 | AvatarImage,
16 | } from "@/components/ui/avatar"
17 | import {
18 | DropdownMenu,
19 | DropdownMenuContent,
20 | DropdownMenuGroup,
21 | DropdownMenuItem,
22 | DropdownMenuLabel,
23 | DropdownMenuSeparator,
24 | DropdownMenuTrigger,
25 | } from "@/components/ui/dropdown-menu"
26 | import {
27 | SidebarMenu,
28 | SidebarMenuButton,
29 | SidebarMenuItem,
30 | useSidebar,
31 | } from "@/components/ui/sidebar"
32 | import { MagicWand, Sparkle } from "@phosphor-icons/react"
33 |
34 | export function NavUser({
35 | user,
36 | }: {
37 | user: {
38 | name: string
39 | email: string
40 | avatar: string
41 | }
42 | }) {
43 | const { isMobile } = useSidebar()
44 |
45 | return (
46 |
47 |
48 |
49 |
50 |
54 |
55 |
56 | BN
57 |
58 |
59 | {user.name}
60 | {user.email}
61 |
62 |
63 |
64 |
65 |
71 |
72 |
73 |
74 |
75 | UF
76 |
77 |
78 | {user.name}
79 | {user.email}
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | Upgrade to Pro
88 |
89 |
90 |
91 |
92 |
93 |
94 | Account
95 |
96 |
97 |
98 | Billing
99 |
100 |
101 |
102 | Notifications
103 |
104 |
105 |
106 |
107 |
108 | Log out
109 |
110 |
111 |
112 |
113 |
114 | )
115 | }
116 |
--------------------------------------------------------------------------------
/persistance/useChatHistory.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef } from 'react';
2 | import { atom } from 'nanostores';
3 | import type { Message } from 'ai';
4 | import { toast } from 'react-toastify';
5 | import { useRouter } from 'next/navigation';
6 | import { workbenchStore } from '@/lib/stores/workbench';
7 | import { getMessages, getNextId, getUrlId, openDatabase, setMessages } from './db';
8 |
9 |
10 | export interface ChatHistoryItem {
11 | id: string;
12 | urlId?: string;
13 | description?: string;
14 | messages: Message[];
15 | timestamp: string;
16 | }
17 |
18 | const persistenceEnabled = !process.env.NEXT_PUBLIC_DISABLE_PERSISTENCE;
19 |
20 | export const chatId = atom(undefined);
21 | export const description = atom(undefined);
22 |
23 | export function useChatHistory(mixedId?: string) {
24 | const router = useRouter();
25 | const [initialMessages, setInitialMessages] = useState([]);
26 |
27 | console.log("initialMessages", initialMessages);
28 | const [ready, setReady] = useState(false);
29 | const [urlId, setUrlId] = useState();
30 | const [db, setDb] = useState(undefined);
31 | const dbPromiseRef = useRef | undefined>(undefined);
32 |
33 | const getDB = async () => {
34 | if (!persistenceEnabled) {
35 | return undefined;
36 | }
37 |
38 | if (!dbPromiseRef.current) {
39 | dbPromiseRef.current = openDatabase();
40 | }
41 |
42 | return dbPromiseRef.current;
43 | };
44 |
45 | useEffect(() => {
46 | const initializeDb = async () => {
47 | const database = await getDB();
48 | setDb(database);
49 |
50 | if (!database) {
51 | setReady(true);
52 |
53 | if (persistenceEnabled) {
54 | toast.error(`Chat persistence is unavailable`);
55 | }
56 |
57 | return;
58 | }
59 |
60 | if (mixedId) {
61 | getMessages(database, mixedId)
62 | .then((storedMessages) => {
63 | if (storedMessages && storedMessages.messages.length > 0) {
64 | setInitialMessages(storedMessages.messages);
65 | setUrlId(storedMessages.urlId);
66 | description.set(storedMessages.description);
67 | chatId.set(storedMessages.id);
68 | } else {
69 | router.replace(`/`);
70 | }
71 | setReady(true);
72 | })
73 | .catch((error) => {
74 | toast.error(error.message);
75 | });
76 | } else {
77 | setReady(true);
78 | }
79 | };
80 |
81 | initializeDb();
82 | }, [mixedId, router]);
83 |
84 | return {
85 | ready: !mixedId || ready,
86 | initialMessages,
87 | storeMessageHistory: async (messages: Message[]) => {
88 | if (!db || messages.length === 0) {
89 | return;
90 | }
91 |
92 | const { firstArtifact } = workbenchStore;
93 |
94 | if (!urlId && firstArtifact?.id) {
95 | const urlId = await getUrlId(db, firstArtifact.id);
96 |
97 | navigateChat(urlId);
98 | setUrlId(urlId);
99 | }
100 |
101 | if (!description.get() && firstArtifact?.title) {
102 | description.set(firstArtifact?.title);
103 | }
104 |
105 | if (initialMessages.length === 0 && !chatId.get()) {
106 | const nextId = await getNextId(db);
107 |
108 | chatId.set(nextId);
109 |
110 | if (!urlId) {
111 | navigateChat(nextId);
112 | }
113 | }
114 |
115 | await setMessages(db, chatId.get() as string, messages, urlId, description.get());
116 | },
117 | };
118 | }
119 |
120 | function navigateChat(nextId: string) {
121 | /**
122 | * FIXME: Using the intended navigate function causes a rerender for that breaks the app.
123 | *
124 | * `navigate(`/chat/${nextId}`, { replace: true });`
125 | */
126 | const url = new URL(window.location.href);
127 | url.pathname = `/chat/${nextId}`;
128 |
129 | window.history.replaceState({}, '', url);
130 | }
--------------------------------------------------------------------------------
/components/ui/OldDialog.tsx:
--------------------------------------------------------------------------------
1 | import * as RadixDialog from '@radix-ui/react-dialog';
2 | import { motion, type Variants } from 'framer-motion';
3 | import React, { memo, type ReactNode } from 'react';
4 | import { cn } from '@/lib/utils';
5 | import { cubicEasingFn } from '@/utils/easings';
6 | import { IconButton } from './IconButton';
7 | import { X } from '@phosphor-icons/react';
8 |
9 | export { Close as DialogClose, Root as DialogRoot } from '@radix-ui/react-dialog';
10 |
11 | const transition = {
12 | duration: 0.15,
13 | ease: cubicEasingFn,
14 | };
15 |
16 | export const dialogBackdropVariants = {
17 | closed: {
18 | opacity: 0,
19 | transition,
20 | },
21 | open: {
22 | opacity: 1,
23 | transition,
24 | },
25 | } satisfies Variants;
26 |
27 | export const dialogVariants = {
28 | closed: {
29 | x: '-50%',
30 | y: '-40%',
31 | scale: 0.96,
32 | opacity: 0,
33 | transition,
34 | },
35 | open: {
36 | x: '-50%',
37 | y: '-50%',
38 | scale: 1,
39 | opacity: 1,
40 | transition,
41 | },
42 | } satisfies Variants;
43 |
44 | interface DialogButtonProps {
45 | type: 'primary' | 'secondary' | 'danger';
46 | children: ReactNode;
47 | onClick?: (event: React.UIEvent) => void;
48 | }
49 |
50 | export const DialogButton = memo(({ type, children, onClick }: DialogButtonProps) => {
51 | return (
52 |
68 | );
69 | });
70 |
71 | export const DialogTitle = memo(({ className, children, ...props }: RadixDialog.DialogTitleProps) => {
72 | return (
73 |
80 | {children}
81 |
82 | );
83 | });
84 |
85 | export const DialogDescription = memo(({ className, children, ...props }: RadixDialog.DialogDescriptionProps) => {
86 | return (
87 |
91 | {children}
92 |
93 | );
94 | });
95 |
96 | interface DialogProps {
97 | children: ReactNode | ReactNode[];
98 | className?: string;
99 | onBackdrop?: (event: React.UIEvent) => void;
100 | onClose?: (event: React.UIEvent) => void;
101 | }
102 |
103 | export const Dialog = memo(({ className, children, onBackdrop, onClose }: DialogProps) => {
104 | return (
105 |
106 |
107 |
114 |
115 |
116 |
126 | {children}
127 |
128 |
129 |
130 |
131 |
132 |
133 | );
134 | });
135 |
--------------------------------------------------------------------------------
/components/chat/ProviderSelector.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useStore } from '@nanostores/react';
3 | import {
4 | providerStore,
5 | setProvider,
6 | type Provider,
7 | ProviderType,
8 | togetherModels,
9 | anthropicModels,
10 | googleModels,
11 | xAIModels,
12 | } from '@/lib/stores/provider';
13 | import {
14 | DropdownMenu,
15 | DropdownMenuTrigger,
16 | DropdownMenuContent,
17 | DropdownMenuRadioGroup,
18 | DropdownMenuRadioItem,
19 | } from '@/components/ui/dropdown-menu';
20 | import { Button } from '@/components/ui/button';
21 | import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
22 | import { DropdownMenuLabel } from '@radix-ui/react-dropdown-menu';
23 |
24 | export function ProviderSelector() {
25 | const currentProvider = useStore(providerStore);
26 | const [isProviderMenuOpen, setIsProviderMenuOpen] = React.useState(false);
27 |
28 | const handleProviderChange = (value: Provider) => {
29 | console.log('handleProviderChange called with:', value);
30 | setProvider(value);
31 | };
32 |
33 | return (
34 | setIsProviderMenuOpen(!open)}>
35 |
36 |
50 |
51 |
52 | handleProviderChange(JSON.parse(value))}
55 | >
56 | Anthropic Models
57 | {anthropicModels.map((model) => (
58 |
63 | {model.displayName}
64 |
65 | ))}
66 | Google Models
67 | {googleModels.map((model) => (
68 |
73 | {model.displayName}
74 |
75 | ))}
76 | xAI Models
77 | {xAIModels.map((model) => (
78 |
83 | {model.displayName}
84 |
85 | ))}
86 | TogetherAI Models
87 | {togetherModels.map((model) => (
88 |
93 | {model.displayName}
94 |
95 | ))}
96 |
97 |
98 |
99 | );
100 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "generate-ui",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build --no-lint",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@ai-sdk/anthropic": "^1.0.5",
13 | "@ai-sdk/google": "^1.0.10",
14 | "@ai-sdk/openai": "^1.0.8",
15 | "@codemirror/autocomplete": "^6.18.3",
16 | "@codemirror/commands": "^6.7.1",
17 | "@codemirror/lang-cpp": "^6.0.2",
18 | "@codemirror/lang-css": "^6.3.1",
19 | "@codemirror/lang-html": "^6.4.9",
20 | "@codemirror/lang-javascript": "^6.2.2",
21 | "@codemirror/lang-json": "^6.0.1",
22 | "@codemirror/lang-markdown": "^6.3.1",
23 | "@codemirror/lang-python": "^6.1.6",
24 | "@codemirror/lang-sass": "^6.0.2",
25 | "@codemirror/lang-wast": "^6.0.2",
26 | "@codemirror/search": "^6.5.8",
27 | "@codemirror/state": "^6.5.0",
28 | "@codemirror/view": "^6.35.3",
29 | "@hookform/resolvers": "^3.9.1",
30 | "@nanostores/react": "^0.8.2",
31 | "@phosphor-icons/react": "^2.1.7",
32 | "@radix-ui/react-accordion": "^1.2.2",
33 | "@radix-ui/react-alert-dialog": "^1.1.3",
34 | "@radix-ui/react-aspect-ratio": "^1.1.1",
35 | "@radix-ui/react-avatar": "^1.1.2",
36 | "@radix-ui/react-checkbox": "^1.1.3",
37 | "@radix-ui/react-collapsible": "^1.1.2",
38 | "@radix-ui/react-context-menu": "^2.2.3",
39 | "@radix-ui/react-dialog": "^1.1.3",
40 | "@radix-ui/react-dropdown-menu": "^2.1.3",
41 | "@radix-ui/react-hover-card": "^1.1.3",
42 | "@radix-ui/react-label": "^2.1.1",
43 | "@radix-ui/react-menubar": "^1.1.3",
44 | "@radix-ui/react-navigation-menu": "^1.2.2",
45 | "@radix-ui/react-popover": "^1.1.3",
46 | "@radix-ui/react-progress": "^1.1.1",
47 | "@radix-ui/react-radio-group": "^1.2.2",
48 | "@radix-ui/react-scroll-area": "^1.2.2",
49 | "@radix-ui/react-select": "^2.1.3",
50 | "@radix-ui/react-separator": "^1.1.1",
51 | "@radix-ui/react-slider": "^1.2.2",
52 | "@radix-ui/react-slot": "^1.1.1",
53 | "@radix-ui/react-switch": "^1.1.2",
54 | "@radix-ui/react-tabs": "^1.1.2",
55 | "@radix-ui/react-toast": "^1.2.3",
56 | "@radix-ui/react-toggle": "^1.1.1",
57 | "@radix-ui/react-toggle-group": "^1.1.1",
58 | "@radix-ui/react-tooltip": "^1.1.5",
59 | "@uiw/codemirror-theme-vscode": "^4.23.6",
60 | "@webcontainer/api": "^1.5.1-internal.5",
61 | "@xterm/addon-fit": "^0.10.0",
62 | "@xterm/addon-web-links": "^0.11.0",
63 | "@xterm/xterm": "^5.5.0",
64 | "ai": "^4.0.18",
65 | "buffer": "^6.0.3",
66 | "class-variance-authority": "^0.7.1",
67 | "clsx": "^2.1.1",
68 | "cmdk": "^1.0.0",
69 | "codemirror": "^5.65.18",
70 | "date-fns": "^3.6.0",
71 | "diff": "^7.0.0",
72 | "embla-carousel-react": "^8.5.1",
73 | "framer-motion": "^11.14.4",
74 | "input-otp": "^1.4.1",
75 | "lucide-react": "^0.468.0",
76 | "nanostores": "^0.11.3",
77 | "next": "14.2.16",
78 | "next-themes": "^0.4.4",
79 | "openai-edge": "^1.2.2",
80 | "path-browserify": "^1.0.1",
81 | "react": "^18",
82 | "react-day-picker": "^8.10.1",
83 | "react-dom": "^18",
84 | "react-hook-form": "^7.54.1",
85 | "react-markdown": "^9.0.1",
86 | "react-resizable-panels": "^2.1.7",
87 | "react-toastify": "^10.0.6",
88 | "recharts": "^2.15.0",
89 | "rehype-raw": "^7.0.0",
90 | "rehype-sanitize": "^6.0.0",
91 | "remark-gfm": "^4.0.0",
92 | "sass": "^1.83.0",
93 | "shiki": "^1.24.2",
94 | "sonner": "^1.7.1",
95 | "tailwind-merge": "^2.5.5",
96 | "tailwindcss-animate": "^1.0.7",
97 | "thememirror": "^2.0.1",
98 | "unified": "^11.0.5",
99 | "unist-util-visit": "^5.0.0",
100 | "vaul": "^1.1.2",
101 | "zod": "^3.24.1",
102 | "zustand": "^5.0.2"
103 | },
104 | "devDependencies": {
105 | "@types/diff": "^6.0.0",
106 | "@types/istextorbinary": "^2.3.4",
107 | "@types/node": "^20",
108 | "@types/path-browserify": "^1.0.3",
109 | "@types/react": "^18",
110 | "@types/react-dom": "^18",
111 | "eslint": "^8",
112 | "eslint-config-next": "14.2.16",
113 | "istextorbinary": "^9.5.0",
114 | "postcss": "^8",
115 | "tailwindcss": "^3.4.1",
116 | "typescript": "^5"
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/components/chat/Markdown.module.css:
--------------------------------------------------------------------------------
1 | .MarkdownContent {
2 | line-height: 1.6;
3 | color: var(--foreground);
4 | }
5 |
6 | .MarkdownContent > *:not(:last-child) {
7 | margin-block-end: 16px;
8 | }
9 |
10 | .MarkdownContent :global(.artifact) {
11 | margin: 1.5em 0;
12 | }
13 |
14 | .MarkdownContent :is(h1, h2, h3, h4, h5, h6) {
15 | margin-block-start: 24px;
16 | margin-block-end: 16px;
17 | font-weight: 600;
18 | line-height: 1.25;
19 | color: var(--foreground);
20 | }
21 |
22 | .MarkdownContent :not(:has(:global(.actions)), :global(.actions *)) :is(h1, h2, h3, h4, h5, h6) {
23 | margin-block-start: 24px;
24 | margin-block-end: 16px;
25 | font-weight: 600;
26 | line-height: 1.25;
27 | color: var(--foreground);
28 | }
29 |
30 | .MarkdownContent h1 {
31 | font-size: 2em;
32 | border-bottom: 1px solid var(--border);
33 | padding-bottom: 0.3em;
34 | }
35 |
36 | .MarkdownContent h2 {
37 | font-size: 1.5em;
38 | border-bottom: 1px solid var(--border);
39 | padding-bottom: 0.3em;
40 | }
41 |
42 | .MarkdownContent h3 {
43 | font-size: 1.25em;
44 | }
45 |
46 | .MarkdownContent h4 {
47 | font-size: 1em;
48 | }
49 |
50 | .MarkdownContent h5 {
51 | font-size: 0.875em;
52 | }
53 |
54 | .MarkdownContent h6 {
55 | font-size: 0.85em;
56 | color: #6a737d;
57 | }
58 |
59 | .MarkdownContent p {
60 | white-space: pre-wrap;
61 | }
62 |
63 | .MarkdownContent p:not(:last-of-type) {
64 | margin-block-start: 0;
65 | margin-block-end: 16px;
66 | }
67 |
68 | .MarkdownContent a {
69 | color: var(--foreground);
70 | text-decoration: none;
71 | cursor: pointer;
72 | }
73 |
74 | .MarkdownContent a:hover {
75 | text-decoration: underline;
76 | }
77 |
78 | .MarkdownContent :not(pre) > code {
79 | font-family: ui-monospace, 'Fira Code', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
80 | font-size: 13px;
81 | }
82 |
83 | .MarkdownContent :not(:has(:global(.actions)), :global(.actions *)) :not(pre) > code {
84 | border-radius: 6px;
85 | padding: 0.2em 0.4em;
86 | background-color: var(--accent);
87 | color: var(--foreground);
88 | }
89 |
90 | .MarkdownContent pre {
91 | padding: 20px 16px;
92 | border-radius: 6px;
93 | }
94 |
95 | .MarkdownContent pre:has(> code) {
96 | font-family: ui-monospace, 'Fira Code', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
97 | font-size: 13px;
98 | background: transparent;
99 | overflow-x: auto;
100 | min-width: 0;
101 | }
102 |
103 | .MarkdownContent blockquote {
104 | margin: 0;
105 | padding: 0 1em;
106 | color: var(--muted);
107 | border-left: 0.25em solid var(--border);
108 | }
109 |
110 | .MarkdownContent :is(ul, ol) {
111 | padding-left: 0em;
112 | margin-block-start: 0;
113 | margin-block-end: 16px;
114 | }
115 |
116 | .MarkdownContent :not(:has(:global(.actions)), :global(.actions *)) :is(ul, ol) {
117 | padding-left: 0em;
118 | margin-block-start: 0;
119 | margin-block-end: 16px;
120 | }
121 |
122 | .MarkdownContent ul {
123 | list-style-type: disc;
124 | }
125 |
126 | .MarkdownContent :not(:has(:global(.actions)), :global(.actions *)) ul {
127 | list-style-type: disc;
128 | }
129 |
130 | .MarkdownContent ol {
131 | list-style-type: decimal;
132 | }
133 |
134 | .MarkdownContent :not(:has(:global(.actions)), :global(.actions *)) ol {
135 | list-style-type: decimal;
136 | }
137 |
138 | .MarkdownContent li {
139 | list-style-type: none;
140 | }
141 |
142 | .MarkdownContent :not(:has(:global(.actions)), :global(.actions *)) li {
143 | list-style-type: none;
144 | }
145 |
146 | .MarkdownContent li + li {
147 | margin-block-start: 8px;
148 | }
149 |
150 | .MarkdownContent :not(:has(:global(.actions)), :global(.actions *)) li + li {
151 | margin-block-start: 8px;
152 | }
153 |
154 | .MarkdownContent li > *:not(:last-child) {
155 | margin-block-end: 16px;
156 | }
157 |
158 | .MarkdownContent :not(:has(:global(.actions)), :global(.actions *)) li > *:not(:last-child) {
159 | margin-block-end: 16px;
160 | }
161 |
162 | .MarkdownContent img {
163 | max-width: 100%;
164 | box-sizing: border-box;
165 | }
166 |
167 | .MarkdownContent hr {
168 | height: 0.25em;
169 | padding: 0;
170 | margin: 24px 0;
171 | background-color: var(--border);
172 | border: 0;
173 | }
174 |
175 | .MarkdownContent table {
176 | border-collapse: collapse;
177 | width: 100%;
178 | margin-block-end: 16px;
179 | }
180 |
181 | .MarkdownContent table :is(th, td) {
182 | padding: 6px 13px;
183 | border: 1px solid #dfe2e5;
184 | }
185 |
186 | .MarkdownContent table tr:nth-child(2n) {
187 | background-color: #f6f8fa;
188 | }
--------------------------------------------------------------------------------
/components/workbench/Preview.tsx:
--------------------------------------------------------------------------------
1 | import { useStore } from '@nanostores/react';
2 | import { memo, useCallback, useEffect, useRef, useState } from 'react';
3 | import { IconButton } from '@/components/ui/IconButton';
4 | import { workbenchStore } from '@/lib/stores/workbench';
5 | import { PortDropdown } from './PortDropdown';
6 | import { ArrowClockwise } from '@phosphor-icons/react';
7 |
8 | export const Preview = memo(() => {
9 | const iframeRef = useRef(null);
10 | const inputRef = useRef(null);
11 | const [activePreviewIndex, setActivePreviewIndex] = useState(0);
12 | const [isPortDropdownOpen, setIsPortDropdownOpen] = useState(false);
13 | const hasSelectedPreview = useRef(false);
14 | const previews = useStore(workbenchStore.previews);
15 | const activePreview = previews[activePreviewIndex];
16 |
17 | const [url, setUrl] = useState('');
18 | const [iframeUrl, setIframeUrl] = useState();
19 |
20 | useEffect(() => {
21 | if (!activePreview) {
22 | setUrl('');
23 | setIframeUrl(undefined);
24 |
25 | return;
26 | }
27 |
28 | const { baseUrl } = activePreview;
29 |
30 | setUrl(baseUrl);
31 | setIframeUrl(baseUrl);
32 | }, [activePreview, iframeUrl]);
33 |
34 | const validateUrl = useCallback(
35 | (value: string) => {
36 | if (!activePreview) {
37 | return false;
38 | }
39 |
40 | const { baseUrl } = activePreview;
41 |
42 | if (value === baseUrl) {
43 | return true;
44 | } else if (value.startsWith(baseUrl)) {
45 | return ['/', '?', '#'].includes(value.charAt(baseUrl.length));
46 | }
47 |
48 | return false;
49 | },
50 | [activePreview],
51 | );
52 |
53 | const findMinPortIndex = useCallback(
54 | (minIndex: number, preview: { port: number }, index: number, array: { port: number }[]) => {
55 | return preview.port < array[minIndex].port ? index : minIndex;
56 | },
57 | [],
58 | );
59 |
60 | // when previews change, display the lowest port if user hasn't selected a preview
61 | useEffect(() => {
62 | if (previews.length > 1 && !hasSelectedPreview.current) {
63 | const minPortIndex = previews.reduce(findMinPortIndex, 0);
64 |
65 | setActivePreviewIndex(minPortIndex);
66 | }
67 | }, [previews]);
68 |
69 | const reloadPreview = () => {
70 | if (iframeRef.current) {
71 | iframeRef.current.src = iframeRef.current.src;
72 | }
73 | };
74 |
75 | return (
76 |
77 | {isPortDropdownOpen && (
78 | setIsPortDropdownOpen(false)} />
79 | )}
80 |
81 |
82 |
86 | {
92 | setUrl(event.target.value);
93 | }}
94 | onKeyDown={(event) => {
95 | if (event.key === 'Enter' && validateUrl(url)) {
96 | setIframeUrl(url);
97 |
98 | if (inputRef.current) {
99 | inputRef.current.blur();
100 | }
101 | }
102 | }}
103 | />
104 |
105 | {previews.length > 1 && (
106 | (hasSelectedPreview.current = value)}
111 | setIsDropdownOpen={setIsPortDropdownOpen}
112 | previews={previews}
113 | />
114 | )}
115 |
116 |
117 | {activePreview ? (
118 |
119 | ) : (
120 | No preview available
121 | )}
122 |
123 |
124 | );
125 | });
126 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | font-family: Arial, Helvetica, sans-serif;
7 | }
8 |
9 | .font-paytone {
10 | font-family: 'Paytone One', sans-serif;
11 | }
12 |
13 | .hide-scrollbar {
14 | overflow: auto; /* or overflow-y: auto for vertical scrolling only */
15 | scrollbar-width: none; /* Firefox */
16 | -ms-overflow-style: none; /* IE and Edge */
17 | }
18 |
19 | .hide-scrollbar::-webkit-scrollbar {
20 | display: none; /* Chrome, Safari, and Opera */
21 | }
22 |
23 | .gradBorder {
24 | position: relative; /* the thickness of the border */
25 | }
26 |
27 | .gradBorder::before {
28 | content: '';
29 | position: absolute;
30 | z-index: -1;
31 | inset: 0;
32 | border-radius: 0.65rem; /* Equivalent to rounded-lg */
33 | padding: 0.8px;
34 | background: linear-gradient(to bottom right,
35 | hsl(var(--primary)) 10%,
36 | hsl(var(--accent)) 15%,
37 | transparent 25%,
38 | transparent 100%
39 | );
40 | transition: transform 0.7s linear;
41 | mask:
42 | linear-gradient(#fff 0 0) content-box,
43 | linear-gradient(#fff 0 0);
44 | -webkit-mask-composite: xor;
45 | mask-composite: exclude;
46 | }
47 |
48 | @layer utilities {
49 | .text-balance {
50 | text-wrap: balance;
51 | }
52 |
53 | .glass {
54 | background-color: rgba(255, 255, 255, 0.05);
55 | backdrop-filter: blur(4px);
56 | }
57 | }
58 |
59 | @layer base {
60 | :root {
61 | --background: 74 0% 100%;
62 | --foreground: 74 0% 10%;
63 | --card: 74 0% 100%;
64 | --card-foreground: 74 0% 15%;
65 | --popover: 74 0% 100%;
66 | --popover-foreground: 74 95% 10%;
67 | --primary: 74 100% 53%;
68 | --primary-foreground: 0 0% 100%;
69 | --secondary: 74 10% 90%;
70 | --secondary-foreground: 0 0% 0%;
71 | --muted: 74 0% 10%;
72 | --muted-foreground: 74 0% 40%;
73 | --accent: 36 10% 90%;
74 | --accent-foreground: 74 0% 15%;
75 | --destructive: 0 50% 50%;
76 | --destructive-foreground: 74 0% 100%;
77 | --border: 74 20% 82%;
78 | --input: 74 20% 50%;
79 | --ring: 74 100% 53%;
80 | --chart-1: 12 76% 61%;
81 | --chart-2: 173 58% 39%;
82 | --chart-3: 197 37% 24%;
83 | --chart-4: 43 74% 66%;
84 | --chart-5: 27 87% 67%;
85 | --radius: 0.5rem;
86 | --sidebar-background: 0 0% 98%;
87 | --sidebar-foreground: 240 5.3% 26.1%;
88 | --sidebar-primary: 240 5.9% 10%;
89 | --sidebar-primary-foreground: 0 0% 98%;
90 | --sidebar-accent: 240 4.8% 95.9%;
91 | --sidebar-accent-foreground: 240 5.9% 10%;
92 | --sidebar-border: 220 13% 91%;
93 | --sidebar-ring: 217.2 91.2% 59.8%;
94 |
95 | /* constraints */
96 | --header-height: 54px;
97 | --chat-max-width: 32rem;
98 | --chat-min-width: 545px;
99 | --workbench-width: min(calc(100% - var(--chat-min-width)), 1536px);
100 | --workbench-inner-width: var(--workbench-width);
101 | --workbench-left: calc(100% - var(--workbench-width));
102 | }
103 | .dark {
104 | --background: 240 5.9% 10%;
105 | --foreground: 297 0% 100%;
106 | --card: 297 0% 10%;
107 | --card-foreground: 297 0% 100%;
108 | --popover: 297 10% 5%;
109 | --popover-foreground: 297 0% 100%;
110 | --primary: 297 100% 65%;
111 | --primary-foreground: 0 0% 0%;
112 | --secondary: 297 10% 20%;
113 | --secondary-foreground: 0 0% 100%;
114 | --muted: 292 0% 45.38%;
115 | --muted-foreground: 292 0% 65.38%;
116 | --accent: 292 100% 83.55%;
117 | --accent-foreground: 292 1.79% 9.82%;
118 | --destructive: 0 50% 50%;
119 | --destructive-foreground: 297 0% 100%;
120 | --border: 292 0% 25.38%;
121 | --input: 297 20% 50%;
122 | --ring: 297 100% 65%;
123 | --radius: 0.5rem;
124 | --chart-1: 220 70% 50%;
125 | --chart-2: 160 60% 45%;
126 | --chart-3: 30 80% 55%;
127 | --chart-4: 280 65% 60%;
128 | --chart-5: 340 75% 55%;
129 | --sidebar-background: 240 5.9% 10%;
130 | --sidebar-foreground: 240 4.8% 95.9%;
131 | --sidebar-primary: 157 100% 68%;
132 | --sidebar-primary-foreground: 0 0% 0%;
133 | --sidebar-accent: 292 100% 83.55%;
134 | --sidebar-accent-foreground: 292 1.79% 9.82%;
135 | --sidebar-border: 240 3.7% 15.9%;
136 | --sidebar-ring: 217.2 91.2% 59.8%;
137 |
138 | --header-height: 54px;
139 | --chat-max-width: 32rem;
140 | --chat-min-width: 545px;
141 | --workbench-width: min(calc(100% - var(--chat-min-width)));
142 | --workbench-inner-width: var(--workbench-width);
143 | --workbench-left: calc(100% - var(--workbench-width));
144 | }
145 | }
146 |
147 | @layer base {
148 | * {
149 | @apply border-border;
150 | }
151 | body {
152 | @apply bg-background text-foreground;
153 | }
154 |
155 | }
156 |
--------------------------------------------------------------------------------
/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react"
5 |
6 | import type {
7 | ToastActionElement,
8 | ToastProps,
9 | } from "@/components/ui/toast"
10 |
11 | const TOAST_LIMIT = 1
12 | const TOAST_REMOVE_DELAY = 1000000
13 |
14 | type ToasterToast = ToastProps & {
15 | id: string
16 | title?: React.ReactNode
17 | description?: React.ReactNode
18 | action?: ToastActionElement
19 | }
20 |
21 | const actionTypes = {
22 | ADD_TOAST: "ADD_TOAST",
23 | UPDATE_TOAST: "UPDATE_TOAST",
24 | DISMISS_TOAST: "DISMISS_TOAST",
25 | REMOVE_TOAST: "REMOVE_TOAST",
26 | } as const
27 |
28 | let count = 0
29 |
30 | function genId() {
31 | count = (count + 1) % Number.MAX_SAFE_INTEGER
32 | return count.toString()
33 | }
34 |
35 | type ActionType = typeof actionTypes
36 |
37 | type Action =
38 | | {
39 | type: ActionType["ADD_TOAST"]
40 | toast: ToasterToast
41 | }
42 | | {
43 | type: ActionType["UPDATE_TOAST"]
44 | toast: Partial
45 | }
46 | | {
47 | type: ActionType["DISMISS_TOAST"]
48 | toastId?: ToasterToast["id"]
49 | }
50 | | {
51 | type: ActionType["REMOVE_TOAST"]
52 | toastId?: ToasterToast["id"]
53 | }
54 |
55 | interface State {
56 | toasts: ToasterToast[]
57 | }
58 |
59 | const toastTimeouts = new Map>()
60 |
61 | const addToRemoveQueue = (toastId: string) => {
62 | if (toastTimeouts.has(toastId)) {
63 | return
64 | }
65 |
66 | const timeout = setTimeout(() => {
67 | toastTimeouts.delete(toastId)
68 | dispatch({
69 | type: "REMOVE_TOAST",
70 | toastId: toastId,
71 | })
72 | }, TOAST_REMOVE_DELAY)
73 |
74 | toastTimeouts.set(toastId, timeout)
75 | }
76 |
77 | export const reducer = (state: State, action: Action): State => {
78 | switch (action.type) {
79 | case "ADD_TOAST":
80 | return {
81 | ...state,
82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83 | }
84 |
85 | case "UPDATE_TOAST":
86 | return {
87 | ...state,
88 | toasts: state.toasts.map((t) =>
89 | t.id === action.toast.id ? { ...t, ...action.toast } : t
90 | ),
91 | }
92 |
93 | case "DISMISS_TOAST": {
94 | const { toastId } = action
95 |
96 | // ! Side effects ! - This could be extracted into a dismissToast() action,
97 | // but I'll keep it here for simplicity
98 | if (toastId) {
99 | addToRemoveQueue(toastId)
100 | } else {
101 | state.toasts.forEach((toast) => {
102 | addToRemoveQueue(toast.id)
103 | })
104 | }
105 |
106 | return {
107 | ...state,
108 | toasts: state.toasts.map((t) =>
109 | t.id === toastId || toastId === undefined
110 | ? {
111 | ...t,
112 | open: false,
113 | }
114 | : t
115 | ),
116 | }
117 | }
118 | case "REMOVE_TOAST":
119 | if (action.toastId === undefined) {
120 | return {
121 | ...state,
122 | toasts: [],
123 | }
124 | }
125 | return {
126 | ...state,
127 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
128 | }
129 | }
130 | }
131 |
132 | const listeners: Array<(state: State) => void> = []
133 |
134 | let memoryState: State = { toasts: [] }
135 |
136 | function dispatch(action: Action) {
137 | memoryState = reducer(memoryState, action)
138 | listeners.forEach((listener) => {
139 | listener(memoryState)
140 | })
141 | }
142 |
143 | type Toast = Omit
144 |
145 | function toast({ ...props }: Toast) {
146 | const id = genId()
147 |
148 | const update = (props: ToasterToast) =>
149 | dispatch({
150 | type: "UPDATE_TOAST",
151 | toast: { ...props, id },
152 | })
153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
154 |
155 | dispatch({
156 | type: "ADD_TOAST",
157 | toast: {
158 | ...props,
159 | id,
160 | open: true,
161 | onOpenChange: (open) => {
162 | if (!open) dismiss()
163 | },
164 | },
165 | })
166 |
167 | return {
168 | id: id,
169 | dismiss,
170 | update,
171 | }
172 | }
173 |
174 | function useToast() {
175 | const [state, setState] = React.useState(memoryState)
176 |
177 | React.useEffect(() => {
178 | listeners.push(setState)
179 | return () => {
180 | const index = listeners.indexOf(setState)
181 | if (index > -1) {
182 | listeners.splice(index, 1)
183 | }
184 | }
185 | }, [state])
186 |
187 | return {
188 | ...state,
189 | toast,
190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
191 | }
192 | }
193 |
194 | export { useToast, toast }
195 |
--------------------------------------------------------------------------------
/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | 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",
42 | right:
43 | "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",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 |
68 |
69 | Close
70 |
71 | {children}
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
|