56 | {/* Container */}
57 |
58 |
Start a New Chat
59 |
60 | Please select your model and system propmt first. Then, you can{" "}
61 |
62 | ask your first question
63 | {" "}
64 | to start a new chat.
65 |
66 |
67 | {/* Settings */}
68 |
69 |
Model
70 |
{
72 | setCurrentChat((prev) =>
73 | prev ? { ...prev, model: value } : prev
74 | );
75 | // Save to supabase
76 | await supabase
77 | .from("chats")
78 | .update({
79 | model: currentChat?.model,
80 | })
81 | .eq("id", currentChat?.id);
82 | }}
83 | value={currentChat?.model as string}
84 | >
85 |
86 |
87 |
88 |
89 | GPT-3.5 Turbo
90 | GPT 4
91 |
92 |
93 | {currentChat?.model === "gpt-4" && (
94 |
95 |
96 |
97 | GPT-4 is almost{" "}
98 | 10x expensive {" "}
99 | than the previous model.
100 |
101 |
102 | )}
103 |
104 | History Type
105 | {
107 | setCurrentChat((prev) =>
108 | prev ? { ...prev, history_type: value } : prev
109 | );
110 | // Save to supabase
111 | await supabase
112 | .from("chats")
113 | .update({
114 | history_type: currentChat?.history_type,
115 | })
116 | .eq("id", currentChat?.id);
117 | }}
118 | value={currentChat?.history_type}
119 | >
120 |
121 |
122 |
123 |
124 | Global
125 | Chat
126 |
127 |
128 |
129 |
130 |
131 | System Propmt
132 |
136 | Reset to Default
137 |
138 |
139 |
{
142 | const value = e.target.value;
143 | setCurrentChat((prev) =>
144 | prev
145 | ? {
146 | ...prev,
147 | system_prompt: value ? value : defaultSystemPropmt,
148 | }
149 | : prev
150 | );
151 | await debouncedSendSupabase(
152 | value ? value : defaultSystemPropmt,
153 | currentChat?.id as string
154 | );
155 | }}
156 | className="mt-3"
157 | />
158 |
159 |
160 |
161 |
162 | );
163 | };
164 |
165 | export default NewChatCurrent;
166 |
--------------------------------------------------------------------------------
/components/chat/new-chat.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { openAISettingsAtom } from "@/atoms/chat";
3 | import { useAtom } from "jotai";
4 | import { focusAtom } from "jotai-optics";
5 | import { Info } from "lucide-react";
6 | import { Label } from "../ui/label";
7 | import {
8 | Select,
9 | SelectContent,
10 | SelectItem,
11 | SelectTrigger,
12 | SelectValue,
13 | } from "../ui/select";
14 | import { TextareaDefault } from "../ui/textarea-default";
15 | const focusedModelAtom = focusAtom(openAISettingsAtom, (optic) =>
16 | optic.prop("model")
17 | );
18 | const focusedSystemPropmtAtom = focusAtom(openAISettingsAtom, (optic) =>
19 | optic.prop("system_prompt")
20 | );
21 |
22 | const focusedHistoryTypeAtom = focusAtom(openAISettingsAtom, (optic) =>
23 | optic.prop("history_type")
24 | );
25 |
26 | const NewChat = () => {
27 | const [model, setModel] = useAtom(focusedModelAtom);
28 | const [systemPropmt, setSystemPropmt] = useAtom(focusedSystemPropmtAtom);
29 | const [historyType, setHistoryType] = useAtom(focusedHistoryTypeAtom);
30 |
31 | return (
32 |
33 | {/* Container */}
34 |
35 |
Start a New Chat
36 |
37 | Please select your model and system propmt first. Then, you can{" "}
38 |
39 | ask your first question
40 | {" "}
41 | to start a new chat.
42 |
43 | {/* Settings */}
44 |
45 |
Model
46 |
48 | setModel(value)
49 | }
50 | >
51 |
52 |
53 |
54 |
55 | GPT-3.5 Turbo
56 | GPT 4
57 |
58 |
59 | {model === "gpt-4" && (
60 |
61 |
62 |
63 | GPT-4 is almost{" "}
64 | 10x expensive {" "}
65 | than the previous model.
66 |
67 |
68 | )}
69 |
70 | History Type
71 | {
73 | setHistoryType(value);
74 | }}
75 | value={historyType}
76 | >
77 |
78 |
79 |
80 |
81 | Global
82 | Chat
83 |
84 |
85 |
86 |
87 | System Propmt
88 | {
91 | setSystemPropmt(e.target.value);
92 | }}
93 | className="mt-3"
94 | />
95 |
96 |
97 |
98 |
99 | );
100 | };
101 |
102 | export default NewChat;
103 |
--------------------------------------------------------------------------------
/components/navigation/mobile-menu-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { currentChatAtom } from "@/atoms/chat";
3 | import { mobileMenuAtom } from "@/atoms/navigation";
4 | import { useAtomValue, useSetAtom } from "jotai";
5 | import { Menu } from "lucide-react";
6 | import { Button } from "../ui/button";
7 |
8 | const MobileMenuButton = () => {
9 | const showMobileMenu = useSetAtom(mobileMenuAtom);
10 | const currentChat = useAtomValue(currentChatAtom);
11 | return (
12 | setIsMobileMenuOpen(false)}
11 | className={`fixed inset-0 z-30 md:hidden transition-transform dark:bg-neutral-950/60 bg-white/60 duration-75 ${
12 | isMobileMenuOpen ? "translate-x-0" : "-translate-x-full"
13 | }`}
14 | />
15 | );
16 | };
17 |
18 | export default SiderbarOverlay;
19 |
--------------------------------------------------------------------------------
/components/navigation/sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { mobileMenuAtom } from "@/atoms/navigation";
3 | import useChats from "@/hooks/useChats";
4 | import { useAtom } from "jotai";
5 | import { Plus } from "lucide-react";
6 | import Logo from "../brand/logo";
7 | import Chats from "../chat/chats";
8 | import { Button } from "../ui/button";
9 | import ProfileMenu from "./profile-menu";
10 |
11 | const Sidebar = () => {
12 | const [isMobileMenuOpen, showMobileMenu] = useAtom(mobileMenuAtom);
13 | const { addChatHandler } = useChats();
14 |
15 | return (
16 |
21 |
22 | {/* Header */}
23 |
24 |
25 | {/* New Chat Button */}
26 |
{
28 | addChatHandler();
29 | showMobileMenu(false);
30 | }}
31 | variant="subtle"
32 | className="flex-shrink-0 w-full mt-8 sm:mt-16"
33 | >
34 | New Chat
35 |
36 |
37 |
38 | {/* Footer */}
39 |
42 |
43 |
44 | );
45 | };
46 |
47 | export default Sidebar;
48 |
--------------------------------------------------------------------------------
/components/navigation/sidelink.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import {
3 | Tooltip,
4 | TooltipContent,
5 | TooltipProvider,
6 | TooltipTrigger,
7 | } from "@/components/ui/tooltip";
8 | import Link from "next/link";
9 |
10 | const Sidelink = ({
11 | children,
12 | title,
13 | href,
14 | active,
15 | }: {
16 | children: React.ReactNode;
17 | title: string;
18 | href: string;
19 | active: boolean;
20 | }) => {
21 | return (
22 |
23 |
24 |
25 |
26 | {children}
27 |
28 |
29 |
30 | {title}
31 |
32 |
33 |
34 | );
35 | };
36 |
37 | export default Sidelink;
38 |
--------------------------------------------------------------------------------
/components/providers/jotai-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Provider } from "jotai";
4 |
5 | const JotaiProvider = ({ children }: { children: React.ReactNode }) => {
6 | return
{children} ;
7 | };
8 |
9 | export default JotaiProvider;
10 |
--------------------------------------------------------------------------------
/components/providers/openai-key-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { openAIAPIKeyAtom, openAPIKeyHandlerAtom } from "@/atoms/chat";
4 | import { useAuth } from "@/lib/supabase/supabase-auth-provider";
5 | import { useAtom } from "jotai";
6 | import { useHydrateAtoms } from "jotai/utils";
7 | import { Key } from "lucide-react";
8 | import { useEffect, useRef, useState } from "react";
9 | import { Button } from "../ui/button";
10 | import { Input } from "../ui/input";
11 | import { Label } from "../ui/label";
12 |
13 | const OpenAIKeyProvider = ({
14 | children,
15 | serverKey,
16 | }: {
17 | children: React.ReactNode;
18 | serverKey?: string;
19 | }) => {
20 | useHydrateAtoms([[openAIAPIKeyAtom, serverKey] as const]);
21 | const [openAIKey, keyHandler] = useAtom(openAPIKeyHandlerAtom);
22 | const inputRef = useRef
(null);
23 | const [isHandling, setIsHandling] = useState(false);
24 | const [isChecked, setIsChecked] = useState(false);
25 | const { user } = useAuth();
26 |
27 | // Save Handler
28 | const saveHandler = async (e: React.FormEvent) => {
29 | e.preventDefault();
30 | const value = inputRef.current?.value;
31 | if (value && value.length > 0) {
32 | setIsHandling(true);
33 | // Check if the key is valid
34 | const response = await fetch("/api/openai/check", {
35 | method: "POST",
36 | headers: {
37 | "Content-Type": "application/json",
38 | },
39 | body: JSON.stringify({ key: value }),
40 | });
41 | const data = await response.json();
42 | if (response!.ok) {
43 | keyHandler({ action: "set", key: value });
44 | } else {
45 | console.log(data.message); // TODO: Add Toast
46 | }
47 | setIsHandling(false);
48 | }
49 | };
50 |
51 | // Set the OpenAI Key on First Render
52 | useEffect(() => {
53 | if (!openAIKey) {
54 | keyHandler({ action: "get" });
55 | setIsChecked(true);
56 | }
57 | }, [openAIKey, keyHandler]);
58 |
59 | return (
60 | <>
61 | {!openAIKey && isChecked && user && (
62 |
66 |
67 |
68 |
69 |
70 |
Set Your OpenAI API Key
71 |
72 | We do{" "}
73 |
74 | not store your OpenAI API keys on our servers
75 |
76 | . Instead,{" "}
77 |
78 | we store them in your local storage,
79 |
80 | which means that{" "}
81 |
82 | only you have access to your API keys.
83 | {" "}
84 | You can be confident that your key is safe and secure as long as
85 | you do not share your device with others.
86 |
87 | {/* Form Container */}
88 |
104 |
105 |
106 | )}
107 | {openAIKey && children}
108 | >
109 | );
110 | };
111 |
112 | export default OpenAIKeyProvider;
113 |
--------------------------------------------------------------------------------
/components/providers/openai-serverkey-provider.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import OpenAIKeyProvider from "./openai-key-provider";
3 |
4 | const OpenAIServerKeyProvider = ({
5 | children,
6 | }: {
7 | children: React.ReactNode;
8 | }) => {
9 | return (
10 | <>
11 |
12 | {children}
13 |
14 | >
15 | );
16 | };
17 |
18 | export default OpenAIServerKeyProvider;
19 |
--------------------------------------------------------------------------------
/components/providers/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ThemeProvider } from "next-themes";
4 |
5 | export function ThemeProviderClient({
6 | children,
7 | }: {
8 | children: React.ReactNode;
9 | }) {
10 | return (
11 |
12 | {children}
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as AvatarPrimitive from "@radix-ui/react-avatar";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/utils/cn";
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ));
21 | Avatar.displayName = AvatarPrimitive.Root.displayName;
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ));
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ));
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
49 |
50 | export { Avatar, AvatarImage, AvatarFallback };
51 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import { cva, VariantProps } from "class-variance-authority";
2 | import * as React from "react";
3 |
4 | import { cn } from "@/utils/cn";
5 |
6 | const buttonVariants = cva(
7 | "active:scale-95 inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 dark:hover:bg-slate-800 dark:hover:text-slate-100 disabled:opacity-50 dark:focus:ring-slate-400 disabled:pointer-events-none dark:focus:ring-offset-slate-900 data-[state=open]:bg-slate-100 dark:data-[state=open]:bg-slate-800",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "bg-slate-900 text-white hover:bg-slate-700 dark:bg-slate-50 dark:text-slate-900",
13 | destructive:
14 | "bg-red-500 text-white hover:bg-red-600 dark:hover:bg-red-600",
15 | outline:
16 | "bg-transparent border border-slate-200 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-100",
17 | subtle:
18 | "bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-900 dark:text-slate-100",
19 | ghost:
20 | "bg-transparent hover:bg-slate-100 dark:hover:bg-slate-800 dark:text-slate-100 dark:hover:text-slate-100 data-[state=open]:bg-transparent dark:data-[state=open]:bg-transparent",
21 | link: "bg-transparent dark:bg-transparent underline-offset-4 hover:underline text-slate-900 dark:text-slate-100 hover:bg-transparent dark:hover:bg-transparent",
22 | },
23 | size: {
24 | default: "h-10 py-2 px-4",
25 | sm: "h-9 px-2 rounded-md",
26 | lg: "h-11 px-8 rounded-md",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | );
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {}
39 |
40 | const Button = React.forwardRef(
41 | ({ className, variant, size, ...props }, ref) => {
42 | return (
43 |
48 | );
49 | }
50 | );
51 | Button.displayName = "Button";
52 |
53 | export { Button, buttonVariants };
54 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as DialogPrimitive from "@radix-ui/react-dialog";
4 | import { X } from "lucide-react";
5 | import * as React from "react";
6 |
7 | import { cn } from "@/utils/cn";
8 |
9 | const Dialog = DialogPrimitive.Root;
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger;
12 |
13 | const DialogPortal = ({
14 | className,
15 | children,
16 | ...props
17 | }: DialogPrimitive.DialogPortalProps) => (
18 |
19 |
20 | {children}
21 |
22 |
23 | );
24 | DialogPortal.displayName = DialogPrimitive.Portal.displayName;
25 |
26 | const DialogOverlay = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, children, ...props }, ref) => (
30 |
38 | ));
39 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
40 |
41 | const DialogContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, children, ...props }, ref) => (
45 |
46 |
47 |
56 | {children}
57 |
58 |
59 | Close
60 |
61 |
62 |
63 | ));
64 | DialogContent.displayName = DialogPrimitive.Content.displayName;
65 |
66 | const DialogHeader = ({
67 | className,
68 | ...props
69 | }: React.HTMLAttributes) => (
70 |
77 | );
78 | DialogHeader.displayName = "DialogHeader";
79 |
80 | const DialogFooter = ({
81 | className,
82 | ...props
83 | }: React.HTMLAttributes) => (
84 |
91 | );
92 | DialogFooter.displayName = "DialogFooter";
93 |
94 | const DialogTitle = React.forwardRef<
95 | React.ElementRef,
96 | React.ComponentPropsWithoutRef
97 | >(({ className, ...props }, ref) => (
98 |
107 | ));
108 | DialogTitle.displayName = DialogPrimitive.Title.displayName;
109 |
110 | const DialogDescription = React.forwardRef<
111 | React.ElementRef,
112 | React.ComponentPropsWithoutRef
113 | >(({ className, ...props }, ref) => (
114 |
119 | ));
120 | DialogDescription.displayName = DialogPrimitive.Description.displayName;
121 |
122 | export {
123 | Dialog,
124 | DialogTrigger,
125 | DialogContent,
126 | DialogHeader,
127 | DialogFooter,
128 | DialogTitle,
129 | DialogDescription,
130 | };
131 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
4 | import { Check, ChevronRight, Circle } from "lucide-react";
5 | import * as React from "react";
6 |
7 | import { cn } from "@/utils/cn";
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root;
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group;
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub;
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean;
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ));
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName;
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ));
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName;
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ));
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean;
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ));
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ));
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName;
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ));
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean;
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ));
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ));
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
183 | );
184 | };
185 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
186 |
187 | export {
188 | DropdownMenu,
189 | DropdownMenuTrigger,
190 | DropdownMenuContent,
191 | DropdownMenuItem,
192 | DropdownMenuCheckboxItem,
193 | DropdownMenuRadioItem,
194 | DropdownMenuLabel,
195 | DropdownMenuSeparator,
196 | DropdownMenuShortcut,
197 | DropdownMenuGroup,
198 | DropdownMenuPortal,
199 | DropdownMenuSub,
200 | DropdownMenuSubContent,
201 | DropdownMenuSubTrigger,
202 | DropdownMenuRadioGroup,
203 | };
204 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/utils/cn";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | );
20 | }
21 | );
22 | Input.displayName = "Input";
23 |
24 | export { Input };
25 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as LabelPrimitive from "@radix-ui/react-label";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/utils/cn";
7 |
8 | const Label = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ));
21 | Label.displayName = LabelPrimitive.Root.displayName;
22 |
23 | export { Label };
24 |
--------------------------------------------------------------------------------
/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/utils/cn";
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ));
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ));
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
47 |
48 | export { ScrollArea, ScrollBar };
49 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as SelectPrimitive from "@radix-ui/react-select";
4 | import { Check, ChevronDown } from "lucide-react";
5 | import * as React from "react";
6 |
7 | import { cn } from "@/utils/cn";
8 |
9 | const Select = SelectPrimitive.Root;
10 |
11 | const SelectGroup = SelectPrimitive.Group;
12 |
13 | const SelectValue = SelectPrimitive.Value;
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 |
27 | {children}
28 |
29 |
30 | ));
31 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
32 |
33 | const SelectContent = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef
36 | >(({ className, children, ...props }, ref) => (
37 |
38 |
46 |
47 | {children}
48 |
49 |
50 |
51 | ));
52 | SelectContent.displayName = SelectPrimitive.Content.displayName;
53 |
54 | const SelectLabel = React.forwardRef<
55 | React.ElementRef,
56 | React.ComponentPropsWithoutRef
57 | >(({ className, ...props }, ref) => (
58 |
66 | ));
67 | SelectLabel.displayName = SelectPrimitive.Label.displayName;
68 |
69 | const SelectItem = React.forwardRef<
70 | React.ElementRef,
71 | React.ComponentPropsWithoutRef
72 | >(({ className, children, ...props }, ref) => (
73 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | {children}
88 |
89 | ));
90 | SelectItem.displayName = SelectPrimitive.Item.displayName;
91 |
92 | const SelectSeparator = React.forwardRef<
93 | React.ElementRef,
94 | React.ComponentPropsWithoutRef
95 | >(({ className, ...props }, ref) => (
96 |
101 | ));
102 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
103 |
104 | export {
105 | Select,
106 | SelectGroup,
107 | SelectValue,
108 | SelectTrigger,
109 | SelectContent,
110 | SelectLabel,
111 | SelectItem,
112 | SelectSeparator,
113 | };
114 |
--------------------------------------------------------------------------------
/components/ui/seperator.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as SeparatorPrimitive from "@radix-ui/react-separator";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/utils/cn";
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | );
29 | Separator.displayName = SeparatorPrimitive.Root.displayName;
30 |
31 | export { Separator };
32 |
--------------------------------------------------------------------------------
/components/ui/spinner.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/utils/cn";
2 | import { cva, VariantProps } from "class-variance-authority";
3 | import * as React from "react";
4 |
5 | const spinnerVariants = cva(
6 | "animate-spin fill-neutral-800 text-neutral-200 dark:text-neutral-400",
7 | {
8 | variants: {
9 | size: {
10 | default: "w-6 h-6",
11 | sm: "w-4 h-4",
12 | lg: "w-10 h-10",
13 | },
14 | },
15 | defaultVariants: {
16 | size: "default",
17 | },
18 | }
19 | );
20 |
21 | export interface SpinnerProps
22 | extends React.ButtonHTMLAttributes,
23 | VariantProps {}
24 |
25 | const Spinner = ({ size }: SpinnerProps) => {
26 | return (
27 |
28 |
35 |
39 |
43 |
44 |
Loading...
45 |
46 | );
47 | };
48 |
49 | export default Spinner;
50 |
--------------------------------------------------------------------------------
/components/ui/textarea-default.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/utils/cn";
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const TextareaDefault = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | );
20 | }
21 | );
22 | TextareaDefault.displayName = "TextareaDefault";
23 |
24 | export { TextareaDefault };
25 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import * as React from "react";
3 |
4 | import { cn } from "@/utils/cn";
5 | import { useEffect, useState } from "react";
6 |
7 | export interface TextareaProps
8 | extends React.TextareaHTMLAttributes {}
9 |
10 | const Textarea = React.forwardRef(
11 | ({ className, onChange, ...props }, ref) => {
12 | const inputValue = props.value;
13 | const defaultRow = 1;
14 | const maxRow = 10;
15 | const [rows, setRows] = useState(defaultRow);
16 |
17 | const handleChange = (e: React.ChangeEvent) => {
18 | // Call regular handler function
19 | onChange?.(e);
20 |
21 | // Calculate number of rows
22 | const textareaLineHeight = 24;
23 | const previousRows = e.target.rows;
24 | e.target.rows = defaultRow; // reset number of rows in textarea
25 | const currentRows = ~~(e.target.scrollHeight / textareaLineHeight);
26 | if (currentRows === previousRows) {
27 | e.target.rows = currentRows;
28 | }
29 | if (currentRows >= maxRow) {
30 | e.target.rows = maxRow;
31 | e.target.scrollTop = e.target.scrollHeight;
32 | }
33 | setRows(currentRows < maxRow ? currentRows : maxRow);
34 | };
35 |
36 | // Set Input Height When Value is Empty
37 | useEffect(() => {
38 | if (inputValue === "") {
39 | setRows(defaultRow);
40 | }
41 | }, [inputValue]);
42 |
43 | return (
44 |
55 | );
56 | }
57 | );
58 | Textarea.displayName = "Textarea";
59 |
60 | export { Textarea };
61 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/utils/cn";
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider;
9 |
10 | const Tooltip = ({ ...props }) => ;
11 | Tooltip.displayName = TooltipPrimitive.Tooltip.displayName;
12 |
13 | const TooltipTrigger = TooltipPrimitive.Trigger;
14 |
15 | const TooltipContent = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, sideOffset = 4, ...props }, ref) => (
19 |
28 | ));
29 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
30 |
31 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
32 |
--------------------------------------------------------------------------------
/hooks/useChat.ts:
--------------------------------------------------------------------------------
1 | import {
2 | addMessageAtom,
3 | currentChatAtom,
4 | currentChatHasMessagesAtom,
5 | messagesAtom,
6 | } from "@/atoms/chat";
7 | import { ChatWithMessageCountAndSettings, MessageT } from "@/types/collections";
8 | import { useAtomValue, useSetAtom } from "jotai";
9 | import { useRouter, useSearchParams } from "next/navigation";
10 | import { useEffect, useMemo } from "react";
11 |
12 | const useChat = ({
13 | currentChat,
14 | initialMessages,
15 | }: {
16 | currentChat: ChatWithMessageCountAndSettings;
17 | initialMessages: MessageT[];
18 | }) => {
19 | const chatID = currentChat?.id;
20 | const addMessageHandler = useSetAtom(addMessageAtom);
21 | const hasChatMessages = useAtomValue(currentChatHasMessagesAtom);
22 | const setMessages = useSetAtom(messagesAtom);
23 | const setCurrentChat = useSetAtom(currentChatAtom);
24 |
25 | // Set Initial Chat
26 | useEffect(() => {
27 | setCurrentChat(currentChat);
28 | setMessages(initialMessages);
29 | }, [currentChat, initialMessages, setCurrentChat, setMessages]);
30 |
31 | const router = useRouter();
32 | const searchParams = useSearchParams();
33 | const writableParams = useMemo(
34 | () => new URLSearchParams(searchParams.toString()),
35 | [searchParams]
36 | );
37 | const isChatNew = writableParams.get("new") === "true";
38 |
39 | // Send First Message if it's a new chat
40 | useEffect(() => {
41 | if (isChatNew && chatID) {
42 | addMessageHandler("generate").then(async () => {
43 | writableParams.delete("new");
44 | router.replace(`/chat/${chatID}`);
45 | });
46 | }
47 | }, [addMessageHandler, chatID, isChatNew, router, writableParams]);
48 |
49 | return {
50 | hasChatMessages,
51 | };
52 | };
53 |
54 | export default useChat;
55 |
--------------------------------------------------------------------------------
/hooks/useChats.ts:
--------------------------------------------------------------------------------
1 | import { chatsAtom, openAISettingsAtom } from "@/atoms/chat";
2 | import { useAuth } from "@/lib/supabase/supabase-auth-provider";
3 | import { useSupabase } from "@/lib/supabase/supabase-provider";
4 | import { ChatWithMessageCountAndSettings } from "@/types/collections";
5 | import { useAtom, useAtomValue } from "jotai";
6 | import { useRouter } from "next/navigation";
7 | import { useEffect } from "react";
8 | import useSWR from "swr";
9 |
10 | const useChats = () => {
11 | // Auth & Supabase
12 | const { user } = useAuth();
13 | const { supabase } = useSupabase();
14 |
15 | // States
16 | const openAISettings = useAtomValue(openAISettingsAtom);
17 | const [chats, setChats] = useAtom(chatsAtom);
18 |
19 | const router = useRouter();
20 |
21 | const fetcher = async () => {
22 | const { data, error } = await supabase
23 | .from("chats")
24 | .select(`*, messages(count)`)
25 | .eq("owner", user?.id)
26 | .order("created_at", { ascending: false });
27 |
28 | if (error) throw error;
29 | return data.map((chat) => {
30 | return {
31 | ...chat,
32 | advanced_settings: JSON.parse(chat.advanced_settings as string),
33 | };
34 | }) as ChatWithMessageCountAndSettings[];
35 | };
36 |
37 | const { data, error, isLoading, mutate } = useSWR(
38 | user ? ["chats", user.id] : null,
39 | fetcher
40 | );
41 |
42 | // Add New Chat Handler
43 | const addChatHandler = async () => {
44 | const { data: newChat, error } = await supabase
45 | .from("chats")
46 | .insert({
47 | owner: user?.id,
48 | model: openAISettings.model,
49 | system_prompt: openAISettings.system_prompt,
50 | advanced_settings: JSON.stringify(openAISettings.advanced_settings),
51 | history_type: openAISettings.history_type,
52 | title: "New Conversation",
53 | })
54 | .select(`*`)
55 | .returns()
56 | .single();
57 | if (error && !newChat) {
58 | console.log(error);
59 | return;
60 | }
61 |
62 | // Add it to the top of the list
63 | mutate((prev: any) => {
64 | if (prev && prev.length > 0) {
65 | return [newChat, ...prev];
66 | } else {
67 | return [newChat];
68 | }
69 | });
70 |
71 | // Redirect to the new chat
72 | router.push(`/chat/${newChat.id}?new=true`);
73 | };
74 |
75 | // Set Chats
76 | useEffect(() => {
77 | setChats(data ?? []);
78 | }, [data, setChats]);
79 |
80 | return {
81 | chats,
82 | isLoading,
83 | error,
84 | mutate,
85 | addChatHandler,
86 | };
87 | };
88 |
89 | export default useChats;
90 |
--------------------------------------------------------------------------------
/lib/openai-stream.ts:
--------------------------------------------------------------------------------
1 | import { OpenAIStreamPayload } from "@/types/openai";
2 | import {
3 | createParser,
4 | ParsedEvent,
5 | ReconnectInterval,
6 | } from "eventsource-parser";
7 |
8 | export async function OpenAIStream(payload: OpenAIStreamPayload) {
9 | const encoder = new TextEncoder();
10 | const decoder = new TextDecoder();
11 |
12 | let counter = 0;
13 |
14 | const res = await fetch("https://api.openai.com/v1/chat/completions", {
15 | headers: {
16 | "Content-Type": "application/json",
17 | Authorization: `Bearer ${payload.apiKey!!}`,
18 | },
19 | method: "POST",
20 | body: JSON.stringify({
21 | model: payload.model,
22 | messages: payload.messages,
23 | max_tokens: payload.max_tokens,
24 | temperature: payload.temperature,
25 | top_p: payload.top_p,
26 | presence_penalty: payload.presence_penalty,
27 | frequency_penalty: payload.frequency_penalty,
28 | n: payload.n,
29 | stream: true,
30 | }),
31 | });
32 |
33 | const stream = new ReadableStream({
34 | async start(controller) {
35 | // callback
36 | function onParse(event: ParsedEvent | ReconnectInterval) {
37 | if (event.type === "event") {
38 | const data = event.data;
39 | // https://beta.openai.com/docs/api-reference/completions/create#completions/create-stream
40 | if (data === "[DONE]") {
41 | controller.close();
42 | return;
43 | }
44 | try {
45 | const json = JSON.parse(data);
46 | const text = json.choices[0].delta?.content || "";
47 | if (counter < 2 && (text.match(/\n/) || []).length) {
48 | // this is a prefix character (i.e., "\n\n"), do nothing
49 | return;
50 | }
51 | const queue = encoder.encode(text);
52 | controller.enqueue(queue);
53 | counter++;
54 | } catch (e) {
55 | // maybe parse error
56 | controller.error(e);
57 | }
58 | }
59 | }
60 |
61 | // stream response (SSE) from OpenAI may be fragmented into multiple chunks
62 | // this ensures we properly read chunks and invoke an event for each SSE event stream
63 | const parser = createParser(onParse);
64 | // https://web.dev/streams/#asynchronous-iteration
65 | for await (const chunk of res.body as any) {
66 | parser.feed(decoder.decode(chunk));
67 | }
68 | },
69 | });
70 |
71 | return stream;
72 | }
73 |
--------------------------------------------------------------------------------
/lib/openai.ts:
--------------------------------------------------------------------------------
1 | import { Configuration, OpenAIApi } from "openai";
2 |
3 | const openai = (apiKey: string) => {
4 | const configuration = new Configuration({
5 | apiKey,
6 | });
7 | return new OpenAIApi(configuration);
8 | };
9 |
10 | export default openai;
11 |
--------------------------------------------------------------------------------
/lib/supabase/supabase-auth-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ownerIDAtom } from "@/atoms/chat";
4 | import { ProfileT } from "@/types/collections";
5 | import { Session } from "@supabase/supabase-js";
6 | import { useSetAtom } from "jotai";
7 | import { useRouter } from "next/navigation";
8 | import { createContext, useContext, useEffect } from "react";
9 | import useSWR from "swr";
10 | import { useSupabase } from "./supabase-provider";
11 | interface ContextI {
12 | user: ProfileT | null | undefined;
13 | error: any;
14 | isLoading: boolean;
15 | mutate: any;
16 | signOut: () => Promise;
17 | signInWithGithub: () => Promise;
18 | }
19 | const Context = createContext({
20 | user: null,
21 | error: null,
22 | isLoading: true,
23 | mutate: null,
24 | signOut: async () => {},
25 | signInWithGithub: async () => {},
26 | });
27 |
28 | export default function SupabaseAuthProvider({
29 | serverSession,
30 | children,
31 | }: {
32 | serverSession?: Session | null;
33 | children: React.ReactNode;
34 | }) {
35 | // THROW ERROR IF AUTH_REDIRECT IS NOT SET
36 | if (
37 | !process.env.NEXT_PUBLIC_AUTH_REDIRECT_URL &&
38 | (process.env.NEXT_PUBLIC_VERCEL_ENV === "production" ||
39 | process.env.NEXT_PUBLIC_VERCEL_ENV === "preview")
40 | ) {
41 | throw new Error("NEXT_PUBLIC_AUTH_REDIRECT_URL must be set in .env");
42 | }
43 |
44 | const { supabase } = useSupabase();
45 | const router = useRouter();
46 | const setOwnerID = useSetAtom(ownerIDAtom);
47 |
48 | // Get USER
49 | const getUser = async () => {
50 | const { data: user, error } = await supabase
51 | .from("profiles")
52 | .select("*")
53 | .eq("id", serverSession?.user?.id)
54 | .single();
55 | if (error) {
56 | console.log(error);
57 | return null;
58 | } else {
59 | return user;
60 | }
61 | };
62 |
63 | const {
64 | data: user,
65 | error,
66 | isLoading,
67 | mutate,
68 | } = useSWR(serverSession ? "profile-context" : null, getUser);
69 |
70 | // Sign Out
71 | const signOut = async () => {
72 | await supabase.auth.signOut();
73 | router.push("/login");
74 | console.log("Signed Out! (from supabase-auth-provider.tsx)");
75 | };
76 |
77 | // Sign-In with Github
78 | const signInWithGithub = async () => {
79 | await supabase.auth.signInWithOAuth({
80 | provider: "github",
81 | options: {
82 | redirectTo:
83 | process.env.NEXT_PUBLIC_VERCEL_ENV === "production" ||
84 | process.env.NEXT_PUBLIC_VERCEL_ENV === "preview"
85 | ? process.env.NEXT_PUBLIC_AUTH_REDIRECT_URL
86 | : "http://localhost:3000/chat",
87 | },
88 | });
89 | };
90 |
91 | // Set Owner ID
92 | useEffect(() => {
93 | if (user) {
94 | setOwnerID(user.id);
95 | }
96 | }, [setOwnerID, user]);
97 |
98 | // Refresh the Page to Sync Server and Client
99 | useEffect(() => {
100 | const {
101 | data: { subscription },
102 | } = supabase.auth.onAuthStateChange((_, session) => {
103 | if (session?.access_token !== serverSession?.access_token) {
104 | router.refresh();
105 | }
106 | });
107 |
108 | return () => {
109 | subscription.unsubscribe();
110 | };
111 | }, [router, supabase, serverSession?.access_token]);
112 |
113 | const exposed: ContextI = {
114 | user,
115 | error,
116 | isLoading,
117 | mutate,
118 | signOut,
119 | signInWithGithub,
120 | };
121 |
122 | return {children} ;
123 | }
124 |
125 | export const useAuth = () => {
126 | let context = useContext(Context);
127 | if (context === undefined) {
128 | throw new Error("useAuth must be used inside SupabaseAuthProvider");
129 | } else {
130 | return context;
131 | }
132 | };
133 |
--------------------------------------------------------------------------------
/lib/supabase/supabase-browser.ts:
--------------------------------------------------------------------------------
1 | import { Database } from "@/types/supabase";
2 | import { createBrowserSupabaseClient } from "@supabase/auth-helpers-nextjs";
3 |
4 | export const createClient = () => createBrowserSupabaseClient();
5 |
--------------------------------------------------------------------------------
/lib/supabase/supabase-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { createClient } from "@/lib/supabase/supabase-browser";
4 | import { createContext, useContext, useState } from "react";
5 |
6 | import type { Database } from "@/types/supabase";
7 | import type { SupabaseClient } from "@supabase/auth-helpers-nextjs";
8 |
9 | type SupabaseContext = {
10 | supabase: SupabaseClient;
11 | };
12 |
13 | const Context = createContext(undefined);
14 |
15 | export default function SupabaseProvider({
16 | children,
17 | }: {
18 | children: React.ReactNode;
19 | }) {
20 | const [supabase] = useState(() => createClient());
21 |
22 | return (
23 |
24 | <>{children}>
25 |
26 | );
27 | }
28 |
29 | export const useSupabase = () => {
30 | const context = useContext(Context);
31 | if (!context) {
32 | throw new Error("useSupabase must be used within a SupabaseProvider");
33 | } else {
34 | return context;
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/lib/supabase/supabase-server.ts:
--------------------------------------------------------------------------------
1 | import { createServerComponentSupabaseClient } from "@supabase/auth-helpers-nextjs";
2 | import { cookies, headers } from "next/headers";
3 | import "server-only";
4 |
5 | import type { Database } from "@/types/supabase";
6 |
7 | export const createClient = () =>
8 | createServerComponentSupabaseClient({
9 | headers,
10 | cookies,
11 | });
12 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { createMiddlewareSupabaseClient } from "@supabase/auth-helpers-nextjs";
2 | import { NextResponse } from "next/server";
3 |
4 | import type { Database } from "@/types/supabase";
5 | import type { NextRequest } from "next/server";
6 |
7 | export async function middleware(req: NextRequest) {
8 | const res = NextResponse.next();
9 | const pathname = req.nextUrl.pathname;
10 |
11 | const supabase = createMiddlewareSupabaseClient({ req, res });
12 |
13 | const {
14 | data: { session },
15 | } = await supabase.auth.getSession();
16 |
17 | if (!session && pathname.startsWith("/chat")) {
18 | const url = new URL(req.url);
19 | url.pathname = "/login";
20 | return NextResponse.redirect(url);
21 | }
22 |
23 | if (session && (pathname === "/" || pathname === "/#")) {
24 | const url = new URL(req.url);
25 | url.pathname = "/chat";
26 | return NextResponse.redirect(url);
27 | }
28 |
29 | return res;
30 | }
31 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | experimental: {
4 | appDir: true,
5 | },
6 | };
7 |
8 | module.exports = nextConfig;
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "makr-ai",
3 | "version": "0.1.0",
4 | "private": false,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "gen-types": "supabase gen types typescript --linked > types/supabase.ts"
11 | },
12 | "dependencies": {
13 | "@nem035/gpt-3-encoder": "^1.1.7",
14 | "@radix-ui/react-avatar": "^1.0.2",
15 | "@radix-ui/react-dialog": "^1.0.3",
16 | "@radix-ui/react-dropdown-menu": "^2.0.4",
17 | "@radix-ui/react-label": "^2.0.1",
18 | "@radix-ui/react-scroll-area": "^1.0.3",
19 | "@radix-ui/react-select": "^1.2.1",
20 | "@radix-ui/react-separator": "^1.0.2",
21 | "@radix-ui/react-tooltip": "^1.0.5",
22 | "@supabase/auth-helpers-nextjs": "0.6.0",
23 | "@supabase/supabase-js": "2.15.0",
24 | "@types/luxon": "^3.2.2",
25 | "@types/node": "18.15.11",
26 | "@types/react": "18.0.31",
27 | "@types/react-copy-to-clipboard": "^5.0.4",
28 | "@types/react-dom": "18.0.11",
29 | "assert": "^2.0.0",
30 | "autoprefixer": "^10.4.14",
31 | "axios": "^1.3.4",
32 | "class-variance-authority": "^0.4.0",
33 | "clsx": "^1.2.1",
34 | "eslint": "8.37.0",
35 | "eslint-config-next": "13.2.4",
36 | "eventsource-parser": "^1.0.0",
37 | "gpt-3-encoder": "^1.1.4",
38 | "highlight.js": "^11.7.0",
39 | "jotai": "^2.0.3",
40 | "jotai-optics": "^0.3.0",
41 | "jotai-utils": "^0.0.0",
42 | "lodash.debounce": "^4.0.8",
43 | "lucide-react": "^0.129.0",
44 | "luxon": "^3.3.0",
45 | "next": "13.3.1-canary.7",
46 | "next-themes": "^0.2.1",
47 | "openai": "^3.2.1",
48 | "optics-ts": "^2.4.0",
49 | "postcss": "^8.4.21",
50 | "react": "18.2.0",
51 | "react-copy-to-clipboard": "^5.1.0",
52 | "react-dom": "18.2.0",
53 | "react-markdown": "^8.0.6",
54 | "rehype-highlight": "^6.0.0",
55 | "remark-gfm": "^3.0.1",
56 | "server-only": "^0.0.1",
57 | "swr": "^2.1.2",
58 | "tailwind-merge": "^1.11.0",
59 | "tailwindcss": "^3.3.1",
60 | "tailwindcss-animate": "^1.0.5",
61 | "typescript": "5.0.3",
62 | "uuid": "^9.0.0"
63 | },
64 | "devDependencies": {
65 | "@tailwindcss/typography": "^0.5.9",
66 | "@types/lodash.debounce": "^4.0.7",
67 | "@types/uuid": "^9.0.1",
68 | "encoding": "^0.1.13",
69 | "fs": "^0.0.1-security"
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/batuhanbilginn/makr-ai/f3faac67c93caf6cc8850c715ec070f5f41b5e92/public/favicon.png
--------------------------------------------------------------------------------
/public/login-gradient.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/batuhanbilginn/makr-ai/f3faac67c93caf6cc8850c715ec070f5f41b5e92/public/login-gradient.jpg
--------------------------------------------------------------------------------
/public/makr-logo-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/public/makr-logo-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/public/makr.-avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/batuhanbilginn/makr-ai/f3faac67c93caf6cc8850c715ec070f5f41b5e92/public/makr.-avatar.png
--------------------------------------------------------------------------------
/public/readme-hero.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/batuhanbilginn/makr-ai/f3faac67c93caf6cc8850c715ec070f5f41b5e92/public/readme-hero.jpg
--------------------------------------------------------------------------------
/public/supabase_schema.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/batuhanbilginn/makr-ai/f3faac67c93caf6cc8850c715ec070f5f41b5e92/public/supabase_schema.png
--------------------------------------------------------------------------------
/public/user-avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/batuhanbilginn/makr-ai/f3faac67c93caf6cc8850c715ec070f5f41b5e92/public/user-avatar.png
--------------------------------------------------------------------------------
/sql/create-index.sql:
--------------------------------------------------------------------------------
1 | create index on messages
2 | using ivfflat (embedding vector_cosine_ops)
3 | with (lists = 100);
--------------------------------------------------------------------------------
/sql/create-profile.sql:
--------------------------------------------------------------------------------
1 | -- inserts a row into public.users
2 | create function public.handle_new_user()
3 | returns trigger
4 | language plpgsql
5 | security definer set search_path = public
6 | as $$
7 | begin
8 | insert into public.profiles (id, full_name, avatar_url)
9 | values (new.id, new.raw_user_meta_data ->> 'user_name', new.raw_user_meta_data ->> 'avatar_url');
10 | return new;
11 | end;
12 | $$;
13 |
14 | -- trigger the function every time a user is created
15 | create trigger on_auth_user_created
16 | after insert on auth.users
17 | for each row execute procedure public.handle_new_user();
--------------------------------------------------------------------------------
/sql/create-tables.sql:
--------------------------------------------------------------------------------
1 | create table profiles (
2 | id uuid default uuid_generate_v4() primary key,
3 | updated_at timestamp default now(),
4 | full_name text,
5 | avatar_url text
6 | );
7 |
8 | create table chats (
9 | id uuid default uuid_generate_v4() primary key,
10 | created_at timestamp default now(),
11 | title text,
12 | owner uuid references profiles (id),
13 | model text,
14 | system_prompt text,
15 | advanced_settings jsonb,
16 | history_type text
17 | );
18 |
19 | create table messages (
20 | id uuid default uuid_generate_v4() primary key,
21 | created_at timestamp default now(),
22 | content text,
23 | role text,
24 | chat uuid references chats (id),
25 | owner uuid references profiles (id),
26 | embedding public.vector(1536),
27 | token_size integer
28 | );
29 |
30 |
--------------------------------------------------------------------------------
/sql/search-messages.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION search_messages (
2 | query_embedding vector(1536),
3 | similarity_threshold float,
4 | match_count int,
5 | owner_id uuid,
6 | chat_id uuid DEFAULT NULL
7 | )
8 | RETURNS TABLE (
9 | content text,
10 | role text,
11 | created_at timestamp with time zone
12 | )
13 | LANGUAGE plpgsql
14 | AS $$
15 | BEGIN
16 | RETURN QUERY
17 | SELECT
18 | messages.content,
19 | messages.role,
20 | messages.created_at::timestamp with time zone
21 | FROM messages
22 | WHERE
23 | messages.owner = owner_id AND
24 | (chat_id IS NULL OR messages.chat = chat_id) AND
25 | 1 - (messages.embedding <=> query_embedding) > similarity_threshold
26 | ORDER BY
27 | 1 - (messages.embedding <=> query_embedding) DESC,
28 | messages.created_at
29 | LIMIT match_count;
30 | END;
31 | $$;
--------------------------------------------------------------------------------
/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 |
--------------------------------------------------------------------------------
/supabase/config.toml:
--------------------------------------------------------------------------------
1 | # A string used to distinguish different Supabase projects on the same host. Defaults to the working
2 | # directory name when running `supabase init`.
3 | project_id = "makr-gpt"
4 |
5 | [api]
6 | # Port to use for the API URL.
7 | port = 54321
8 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
9 | # endpoints. public and storage are always included.
10 | schemas = ["public", "storage", "graphql_public"]
11 | # Extra schemas to add to the search_path of every request. public is always included.
12 | extra_search_path = ["public", "extensions"]
13 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
14 | # for accidental or malicious requests.
15 | max_rows = 1000
16 |
17 | [db]
18 | # Port to use for the local database URL.
19 | port = 54322
20 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW
21 | # server_version;` on the remote database to check.
22 | major_version = 15
23 |
24 | [studio]
25 | # Port to use for Supabase Studio.
26 | port = 54323
27 |
28 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
29 | # are monitored, and you can view the emails that would have been sent from the web interface.
30 | [inbucket]
31 | # Port to use for the email testing server web interface.
32 | port = 54324
33 | smtp_port = 54325
34 | pop3_port = 54326
35 |
36 | [storage]
37 | # The maximum file size allowed (e.g. "5MB", "500KB").
38 | file_size_limit = "50MiB"
39 |
40 | [auth]
41 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
42 | # in emails.
43 | site_url = "http://localhost:3000"
44 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
45 | additional_redirect_urls = ["https://localhost:3000"]
46 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one
47 | # week).
48 | jwt_expiry = 3600
49 | # Allow/disallow new user signups to your project.
50 | enable_signup = true
51 |
52 | [auth.email]
53 | # Allow/disallow new user signups via email to your project.
54 | enable_signup = true
55 | # If enabled, a user will be required to confirm any email change on both the old, and new email
56 | # addresses. If disabled, only the new email is required to confirm.
57 | double_confirm_changes = true
58 | # If enabled, users need to confirm their email address before signing in.
59 | enable_confirmations = false
60 |
61 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
62 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`, `twitch`,
63 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`.
64 | [auth.external.apple]
65 | enabled = false
66 | client_id = ""
67 | secret = ""
68 | # Overrides the default auth redirectUrl.
69 | redirect_uri = ""
70 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
71 | # or any other third-party OIDC providers.
72 | url = ""
73 |
--------------------------------------------------------------------------------
/supabase/seed.sql:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/batuhanbilginn/makr-ai/f3faac67c93caf6cc8850c715ec070f5f41b5e92/supabase/seed.sql
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const { fontFamily } = require("tailwindcss/defaultTheme");
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | darkMode: ["class", '[data-theme="dark"]'],
6 | content: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"],
7 | theme: {
8 | extend: {
9 | fontFamily: {
10 | sans: ["var(--font-sans)", ...fontFamily.sans],
11 | },
12 | keyframes: {
13 | "accordion-down": {
14 | from: { height: 0 },
15 | to: { height: "var(--radix-accordion-content-height)" },
16 | },
17 | "accordion-up": {
18 | from: { height: "var(--radix-accordion-content-height)" },
19 | to: { height: 0 },
20 | },
21 | },
22 | animation: {
23 | "accordion-down": "accordion-down 0.2s ease-out",
24 | "accordion-up": "accordion-up 0.2s ease-out",
25 | },
26 | colors: {
27 | slate: {
28 | 50: "#fafafa",
29 | 100: "#f5f5f5",
30 | 200: "#e5e5e5",
31 | 300: "#d4d4d4",
32 | 400: "#a3a3a3",
33 | 500: "#737373",
34 | 600: "#525252",
35 | 700: "#404040",
36 | 800: "#262626",
37 | 900: "#171717",
38 | 950: "#0a0a0a",
39 | },
40 | },
41 | },
42 | },
43 | plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
44 | };
45 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "strictNullChecks": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "noEmit": true,
11 | "esModuleInterop": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "jsx": "preserve",
17 | "incremental": true,
18 | "plugins": [
19 | {
20 | "name": "next"
21 | }
22 | ],
23 | "paths": {
24 | "@/*": ["./*"]
25 | }
26 | },
27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
28 | "exclude": ["node_modules"]
29 | }
30 |
--------------------------------------------------------------------------------
/types/collections.ts:
--------------------------------------------------------------------------------
1 | import { ChatGPTMessage, OpenAISettings } from "./openai";
2 | import { Database } from "./supabase";
3 |
4 | export interface MessageI extends ChatGPTMessage {
5 | id: string;
6 | createdAt: Date;
7 | }
8 |
9 | type MessageFromSchema = Database["public"]["Tables"]["messages"]["Row"];
10 |
11 | export type ProfileT = Database["public"]["Tables"]["profiles"]["Row"];
12 | export type ChatT = Database["public"]["Tables"]["chats"]["Row"];
13 | export interface MessageT
14 | extends Omit {
15 | index?: number;
16 | owner?: string;
17 | embedding?: string;
18 | pair?: string;
19 | }
20 | export interface ChatWithMessageCountAndSettings
21 | extends Omit,
22 | Omit {
23 | messages: [{ count: number }];
24 | advanced_settings: OpenAISettings["advanced_settings"];
25 | history_type: "chat" | "global";
26 | }
27 |
--------------------------------------------------------------------------------
/types/openai.ts:
--------------------------------------------------------------------------------
1 | export type ChatGPTAgent = "user" | "system" | "assistant";
2 |
3 | export interface OpenAIKeyOptional {
4 | action: "remove" | "get";
5 | key?: string;
6 | }
7 |
8 | export interface OpenAIKeyRequired {
9 | action: "set";
10 | key: string;
11 | }
12 |
13 | export interface ChatGPTMessage {
14 | role: ChatGPTAgent;
15 | content: string;
16 | }
17 |
18 | export interface OpenAIStreamPayload {
19 | apiKey: string;
20 | model: "gpt-3.5-turbo" | "gpt-4";
21 | messages: ChatGPTMessage[];
22 | temperature: number;
23 | top_p: number;
24 | frequency_penalty: number;
25 | presence_penalty: number;
26 | max_tokens: number;
27 | stream: boolean;
28 | n: number;
29 | }
30 |
31 | export interface OpenAISettings {
32 | model: "gpt-3.5-turbo" | "gpt-4";
33 | history_type: "chat" | "global";
34 | system_prompt: string;
35 | advanced_settings: {
36 | temperature: number;
37 | top_p: number;
38 | frequency_penalty: number;
39 | presence_penalty: number;
40 | max_tokens: number;
41 | stream: boolean;
42 | n: number;
43 | };
44 | }
45 |
--------------------------------------------------------------------------------
/types/supabase.ts:
--------------------------------------------------------------------------------
1 | export type Json =
2 | | string
3 | | number
4 | | boolean
5 | | null
6 | | { [key: string]: Json }
7 | | Json[]
8 |
9 | export interface Database {
10 | graphql_public: {
11 | Tables: {
12 | [_ in never]: never
13 | }
14 | Views: {
15 | [_ in never]: never
16 | }
17 | Functions: {
18 | graphql: {
19 | Args: {
20 | operationName?: string
21 | query?: string
22 | variables?: Json
23 | extensions?: Json
24 | }
25 | Returns: Json
26 | }
27 | }
28 | Enums: {
29 | [_ in never]: never
30 | }
31 | CompositeTypes: {
32 | [_ in never]: never
33 | }
34 | }
35 | public: {
36 | Tables: {
37 | chats: {
38 | Row: {
39 | advanced_settings: Json | null
40 | created_at: string | null
41 | history_type: string | null
42 | id: string
43 | model: string | null
44 | owner: string | null
45 | system_prompt: string | null
46 | title: string | null
47 | }
48 | Insert: {
49 | advanced_settings?: Json | null
50 | created_at?: string | null
51 | history_type?: string | null
52 | id?: string
53 | model?: string | null
54 | owner?: string | null
55 | system_prompt?: string | null
56 | title?: string | null
57 | }
58 | Update: {
59 | advanced_settings?: Json | null
60 | created_at?: string | null
61 | history_type?: string | null
62 | id?: string
63 | model?: string | null
64 | owner?: string | null
65 | system_prompt?: string | null
66 | title?: string | null
67 | }
68 | }
69 | messages: {
70 | Row: {
71 | chat: string | null
72 | content: string | null
73 | created_at: string | null
74 | embedding: string | null
75 | id: string
76 | index: number
77 | owner: string | null
78 | pair: string | null
79 | role: string | null
80 | token_size: number | null
81 | }
82 | Insert: {
83 | chat?: string | null
84 | content?: string | null
85 | created_at?: string | null
86 | embedding?: string | null
87 | id?: string
88 | index?: number
89 | owner?: string | null
90 | pair?: string | null
91 | role?: string | null
92 | token_size?: number | null
93 | }
94 | Update: {
95 | chat?: string | null
96 | content?: string | null
97 | created_at?: string | null
98 | embedding?: string | null
99 | id?: string
100 | index?: number
101 | owner?: string | null
102 | pair?: string | null
103 | role?: string | null
104 | token_size?: number | null
105 | }
106 | }
107 | profiles: {
108 | Row: {
109 | avatar_url: string | null
110 | full_name: string | null
111 | id: string
112 | updated_at: string | null
113 | }
114 | Insert: {
115 | avatar_url?: string | null
116 | full_name?: string | null
117 | id: string
118 | updated_at?: string | null
119 | }
120 | Update: {
121 | avatar_url?: string | null
122 | full_name?: string | null
123 | id?: string
124 | updated_at?: string | null
125 | }
126 | }
127 | }
128 | Views: {
129 | [_ in never]: never
130 | }
131 | Functions: {
132 | ivfflathandler: {
133 | Args: {
134 | "": unknown
135 | }
136 | Returns: unknown
137 | }
138 | search_messages: {
139 | Args: {
140 | query_embedding: string
141 | similarity_threshold: number
142 | match_count: number
143 | owner_id: string
144 | chat_id?: string
145 | }
146 | Returns: {
147 | id: string
148 | content: string
149 | role: string
150 | created_at: string
151 | token_size: number
152 | index: number
153 | }[]
154 | }
155 | vector_avg: {
156 | Args: {
157 | "": number[]
158 | }
159 | Returns: string
160 | }
161 | vector_dims: {
162 | Args: {
163 | "": string
164 | }
165 | Returns: number
166 | }
167 | vector_norm: {
168 | Args: {
169 | "": string
170 | }
171 | Returns: number
172 | }
173 | vector_out: {
174 | Args: {
175 | "": string
176 | }
177 | Returns: unknown
178 | }
179 | vector_send: {
180 | Args: {
181 | "": string
182 | }
183 | Returns: string
184 | }
185 | vector_typmod_in: {
186 | Args: {
187 | "": unknown[]
188 | }
189 | Returns: number
190 | }
191 | }
192 | Enums: {
193 | [_ in never]: never
194 | }
195 | CompositeTypes: {
196 | [_ in never]: never
197 | }
198 | }
199 | storage: {
200 | Tables: {
201 | buckets: {
202 | Row: {
203 | allowed_mime_types: string[] | null
204 | avif_autodetection: boolean | null
205 | created_at: string | null
206 | file_size_limit: number | null
207 | id: string
208 | name: string
209 | owner: string | null
210 | public: boolean | null
211 | updated_at: string | null
212 | }
213 | Insert: {
214 | allowed_mime_types?: string[] | null
215 | avif_autodetection?: boolean | null
216 | created_at?: string | null
217 | file_size_limit?: number | null
218 | id: string
219 | name: string
220 | owner?: string | null
221 | public?: boolean | null
222 | updated_at?: string | null
223 | }
224 | Update: {
225 | allowed_mime_types?: string[] | null
226 | avif_autodetection?: boolean | null
227 | created_at?: string | null
228 | file_size_limit?: number | null
229 | id?: string
230 | name?: string
231 | owner?: string | null
232 | public?: boolean | null
233 | updated_at?: string | null
234 | }
235 | }
236 | migrations: {
237 | Row: {
238 | executed_at: string | null
239 | hash: string
240 | id: number
241 | name: string
242 | }
243 | Insert: {
244 | executed_at?: string | null
245 | hash: string
246 | id: number
247 | name: string
248 | }
249 | Update: {
250 | executed_at?: string | null
251 | hash?: string
252 | id?: number
253 | name?: string
254 | }
255 | }
256 | objects: {
257 | Row: {
258 | bucket_id: string | null
259 | created_at: string | null
260 | id: string
261 | last_accessed_at: string | null
262 | metadata: Json | null
263 | name: string | null
264 | owner: string | null
265 | path_tokens: string[] | null
266 | updated_at: string | null
267 | version: string | null
268 | }
269 | Insert: {
270 | bucket_id?: string | null
271 | created_at?: string | null
272 | id?: string
273 | last_accessed_at?: string | null
274 | metadata?: Json | null
275 | name?: string | null
276 | owner?: string | null
277 | path_tokens?: string[] | null
278 | updated_at?: string | null
279 | version?: string | null
280 | }
281 | Update: {
282 | bucket_id?: string | null
283 | created_at?: string | null
284 | id?: string
285 | last_accessed_at?: string | null
286 | metadata?: Json | null
287 | name?: string | null
288 | owner?: string | null
289 | path_tokens?: string[] | null
290 | updated_at?: string | null
291 | version?: string | null
292 | }
293 | }
294 | }
295 | Views: {
296 | [_ in never]: never
297 | }
298 | Functions: {
299 | can_insert_object: {
300 | Args: {
301 | bucketid: string
302 | name: string
303 | owner: string
304 | metadata: Json
305 | }
306 | Returns: undefined
307 | }
308 | extension: {
309 | Args: {
310 | name: string
311 | }
312 | Returns: string
313 | }
314 | filename: {
315 | Args: {
316 | name: string
317 | }
318 | Returns: string
319 | }
320 | foldername: {
321 | Args: {
322 | name: string
323 | }
324 | Returns: unknown
325 | }
326 | get_size_by_bucket: {
327 | Args: Record
328 | Returns: {
329 | size: number
330 | bucket_id: string
331 | }[]
332 | }
333 | search: {
334 | Args: {
335 | prefix: string
336 | bucketname: string
337 | limits?: number
338 | levels?: number
339 | offsets?: number
340 | search?: string
341 | sortcolumn?: string
342 | sortorder?: string
343 | }
344 | Returns: {
345 | name: string
346 | id: string
347 | updated_at: string
348 | created_at: string
349 | last_accessed_at: string
350 | metadata: Json
351 | }[]
352 | }
353 | }
354 | Enums: {
355 | [_ in never]: never
356 | }
357 | CompositeTypes: {
358 | [_ in never]: never
359 | }
360 | }
361 | }
362 |
--------------------------------------------------------------------------------
/utils/cn.ts:
--------------------------------------------------------------------------------
1 | import { ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/utils/helpers.ts:
--------------------------------------------------------------------------------
1 | export const titleCase = (str: string): string => {
2 | return str
3 | .toLowerCase()
4 | .split(" ")
5 | .map((word) => {
6 | return word.replace(word[0], word[0].toUpperCase());
7 | })
8 | .join(" ");
9 | };
10 |
11 | // Put dot every 3 digits
12 | export const dottedNumber = (num: number): string => {
13 | return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
14 | };
15 |
--------------------------------------------------------------------------------