From ca709c2a68d7998dc2a1f40c28d6edc5b125f228 Mon Sep 17 00:00:00 2001 From: Yasir Ekinci Date: Thu, 28 Mar 2024 00:03:51 +0100 Subject: [PATCH] iterate --- README.md | 4 +- package.json | 5 - src/app/api/chat/route.ts | 177 +++++++++--- src/app/api/models/route.ts | 34 ++- src/app/layout.tsx | 4 +- src/app/page.tsx | 66 ++--- src/components/chat/chat-bottombar.tsx | 4 +- src/components/chat/chat-layout.tsx | 6 +- src/components/chat/chat-list.tsx | 40 +-- src/components/chat/chat-topbar.tsx | 104 +++---- src/components/edit-username-form.tsx | 90 ------ src/components/mode-toggle.tsx | 47 --- src/components/settings-clear-chats.tsx | 63 +++++ src/components/settings-theme-toggle.tsx | 27 ++ src/components/settings.tsx | 23 ++ src/components/sidebar-skeleton.tsx | 20 +- src/components/sidebar.tsx | 227 +++++++++------ src/components/system-prompt-form.tsx | 7 +- src/components/system-prompt.tsx | 10 +- src/components/ui/avatar.tsx | 17 +- src/components/ui/button.tsx | 16 +- src/components/ui/card.tsx | 2 +- src/components/ui/dropdown-menu.tsx | 205 -------------- src/components/ui/input.tsx | 2 +- src/components/ui/popover.tsx | 33 --- src/components/ui/select.tsx | 4 +- src/components/ui/skeleton.tsx | 2 +- src/components/ui/textarea.tsx | 2 +- src/components/ui/tooltip.tsx | 2 +- src/components/user-settings.tsx | 106 ------- src/components/username-form.tsx | 67 ----- yarn.lock | 345 +---------------------- 32 files changed, 544 insertions(+), 1217 deletions(-) delete mode 100644 src/components/edit-username-form.tsx delete mode 100644 src/components/mode-toggle.tsx create mode 100644 src/components/settings-clear-chats.tsx create mode 100644 src/components/settings-theme-toggle.tsx create mode 100644 src/components/settings.tsx delete mode 100644 src/components/ui/dropdown-menu.tsx delete mode 100644 src/components/ui/popover.tsx delete mode 100644 src/components/user-settings.tsx delete mode 100644 src/components/username-form.tsx diff --git a/README.md b/README.md index 3c46030..e905e7b 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ The easiest way to get started is to use the pre-built Docker image. docker run --rm -d -p 3000:3000 -e VLLM_URL=http://host.docker.internal:8000 ghcr.io/yoziru/nextjs-vllm-ui:latest ``` -Then go to [localhost:3000](http://localhost:3000/vllm) and start chatting with your favourite model! +Then go to [localhost:3000](http://localhost:3000) and start chatting with your favourite model! # Development 📖 @@ -67,7 +67,7 @@ cd nextjs-ollama-llm-ui mv .example.env .env ``` -**4. If your instance of Ollama is NOT running on the default ip-address and port, change the variable in the .env file to fit your usecase:** +**4. If your instance of vLLM is NOT running on the default ip-address and port, change the variable in the .env file to fit your usecase:** ``` VLLM_URL="http://localhost:8000" diff --git a/package.json b/package.json index 488fdca..f49f91a 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,10 @@ }, "dependencies": { "@hookform/resolvers": "^3.3.4", - "@langchain/community": "^0.0.43", - "@langchain/core": "^0.1.51", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", - "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", @@ -25,7 +21,6 @@ "ai": "^3.0.14", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", - "langchain": "^0.1.30", "lucide-react": "^0.363.0", "next": "14.1.4", "next-themes": "^0.3.0", diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index dd366d9..6ca0965 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -1,13 +1,15 @@ -import { StreamingTextResponse, Message } from "ai"; import { - AIMessage, - HumanMessage, - SystemMessage, -} from "@langchain/core/messages"; -import { BytesOutputParser } from "@langchain/core/output_parsers"; -import { ChatOpenAI } from "@langchain/openai"; + createParser, + ParsedEvent, + ReconnectInterval, +} from "eventsource-parser"; import { NextRequest, NextResponse } from "next/server"; +export interface Message { + role: "user" | "assistant" | "system"; + content: string; +} + const addSystemMessage = (messages: Message[], systemPrompt?: string) => { // early exit if system prompt is empty if (!systemPrompt || systemPrompt === "") { @@ -20,7 +22,6 @@ const addSystemMessage = (messages: Message[], systemPrompt?: string) => { // if there are no messages, add the system prompt as the first message messages = [ { - id: "1", content: systemPrompt, role: "system", }, @@ -28,7 +29,6 @@ const addSystemMessage = (messages: Message[], systemPrompt?: string) => { } else if (messages.length === 0) { // if there are no messages, add the system prompt as the first message messages.push({ - id: "1", content: systemPrompt, role: "system", }); @@ -40,7 +40,6 @@ const addSystemMessage = (messages: Message[], systemPrompt?: string) => { } else { // if the first message is not a system prompt, add the system prompt as the first message messages.unshift({ - id: "1", content: systemPrompt, role: "system", }); @@ -49,42 +48,150 @@ const addSystemMessage = (messages: Message[], systemPrompt?: string) => { return messages; }; -const formatMessages = (messages: Message[]) => { +const formatMessages = (messages: Message[]): Message[] => { return messages.map((m) => { if (m.role === "system") { - return new SystemMessage(m.content); + return { role: "system", content: m.content } as Message; } else if (m.role === "user") { - return new HumanMessage(m.content); + return { role: "user", content: m.content } as Message; } else { - return new AIMessage(m.content); + return { role: "assistant", content: m.content } as Message; } }); }; +// export async function POST(req: NextRequest) { +// const { messages, chatOptions } = await req.json(); +// if (!chatOptions.selectedModel || chatOptions.selectedModel === "") { +// throw new Error("Selected model is required"); +// } + +// const baseUrl = process.env.VLLM_URL + "/v1"; +// const model = new ChatOpenAI({ +// openAIApiKey: "foo", +// configuration: { +// baseURL: baseUrl, +// }, +// modelName: chatOptions.selectedModel, +// temperature: chatOptions.temperature, +// }); + +// const parser = new BytesOutputParser(); +// const formattedMessages = formatMessages( +// addSystemMessage(messages, chatOptions.systemPrompt) +// ); +// try { +// const stream = await model.pipe(parser).stream(formattedMessages); +// return new StreamingTextResponse(stream); +// } catch (e: any) { +// return NextResponse.json({ error: e.message }, { status: e.status ?? 500 }); +// } +// } + export async function POST(req: NextRequest) { - const { messages, chatOptions } = await req.json(); - if (!chatOptions.selectedModel || chatOptions.selectedModel === "") { - throw new Error("Selected model is required"); + try { + const { messages, chatOptions } = await req.json(); + if (!chatOptions.selectedModel || chatOptions.selectedModel === "") { + throw new Error("Selected model is required"); + } + + const baseUrl = process.env.VLLM_URL; + if (!baseUrl) { + throw new Error("VLLM_URL is not set"); + } + const formattedMessages = formatMessages( + addSystemMessage(messages, chatOptions.systemPrompt) + ); + + const stream = await getOpenAIStream( + baseUrl, + chatOptions.selectedModel, + formattedMessages, + chatOptions.temperature + ); + return new NextResponse(stream, { + headers: { "Content-Type": "text/event-stream" }, + }); + } catch (error) { + console.error(error); + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + }, + { status: 500 } + ); } +} - const baseUrl = process.env.VLLM_URL + "/v1"; - const model = new ChatOpenAI({ - openAIApiKey: "foo", - configuration: { - baseURL: baseUrl, - }, - modelName: chatOptions.selectedModel, - temperature: chatOptions.temperature, +const getOpenAIStream = async ( + apiUrl: string, + model: string, + messages: Message[], + temperature?: number, + apiKey?: string +) => { + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + const headers = new Headers(); + headers.set("Content-Type", "application/json"); + if (apiKey !== undefined) { + headers.set("Authorization", `Bearer ${apiKey}`); + headers.set("api-key", apiKey); + } + const res = await fetch(apiUrl + "/v1/chat/completions",{ + headers: headers, + method: "POST", + body: JSON.stringify({ + model: model, + // frequency_penalty: 0, + // max_tokens: 2000, + messages: messages, + // presence_penalty: 0, + stream: true, + temperature: temperature ?? 0.5, + // top_p: 0.95, + }), }); - const parser = new BytesOutputParser(); - const formattedMessages = formatMessages( - addSystemMessage(messages, chatOptions.systemPrompt) - ); - try { - const stream = await model.pipe(parser).stream(formattedMessages); - return new StreamingTextResponse(stream); - } catch (e: any) { - return NextResponse.json({ error: e.message }, { status: e.status ?? 500 }); + if (res.status !== 200) { + const statusText = res.statusText; + const responseBody = await res.text(); + console.error(`vLLM API response error: ${responseBody}`); + throw new Error( + `The vLLM API has encountered an error with a status code of ${res.status} ${statusText}: ${responseBody}` + ); } -} + + return new ReadableStream({ + async start(controller) { + const onParse = (event: ParsedEvent | ReconnectInterval) => { + if (event.type === "event") { + const data = event.data; + + if (data === "[DONE]") { + controller.close(); + return; + } + + try { + const json = JSON.parse(data); + const text = json.choices[0].delta.content; + const queue = encoder.encode(text); + controller.enqueue(queue); + } catch (e) { + controller.error(e); + } + } + }; + + const parser = createParser(onParse); + + for await (const chunk of res.body as any) { + // An extra newline is required to make AzureOpenAI work. + const str = decoder.decode(chunk).replace("[DONE]\n", "[DONE]\n\n"); + parser.feed(str); + } + }, + }); +}; diff --git a/src/app/api/models/route.ts b/src/app/api/models/route.ts index a6372db..7db2ddd 100644 --- a/src/app/api/models/route.ts +++ b/src/app/api/models/route.ts @@ -1,7 +1,29 @@ -export async function GET(req: Request) { - const res = await fetch(process.env.VLLM_URL + "/v1/models"); - return new Response(res.body, res); -} +import { NextRequest, NextResponse } from "next/server"; -// forces the route handler to be dynamic -export const dynamic = "force-dynamic"; +export async function GET(req: NextRequest) { + try { + const res = await fetch(process.env.VLLM_URL + "/v1/models"); + if (res.status !== 200) { + const statusText = res.statusText; + const responseBody = await res.text(); + console.error(`vLLM /api/models response error: ${responseBody}`); + return NextResponse.json( + { + success: false, + error: statusText, + }, + { status: res.status } + ); + } + return new Response(res.body, res); + } catch (error) { + console.error(error); + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + }, + { status: 500 } + ); + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 654efae..220157f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -8,8 +8,8 @@ export const runtime = "edge"; // 'nodejs' (default) | 'edge' const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { - title: "Ollama UI", - description: "Ollama chatbot web interface", + title: "vLLM UI", + description: "vLLM chatbot web interface", }; export const viewport = { diff --git a/src/app/page.tsx b/src/app/page.tsx index 009dabf..295563c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,21 +1,14 @@ "use client"; import { ChatLayout } from "@/components/chat/chat-layout"; -import { - Dialog, - DialogDescription, - DialogHeader, - DialogTitle, - DialogContent, -} from "@/components/ui/dialog"; -import UsernameForm from "@/components/username-form"; import { ChatRequestOptions } from "ai"; import { useChat } from "ai/react"; -import React, { useEffect } from "react"; +import React from "react"; import { v4 as uuidv4 } from "uuid"; import useLocalStorageState from "use-local-storage-state"; import { ChatOptions } from "@/components/chat/chat-options"; import { basePath } from "@/lib/utils"; +import { toast } from "sonner"; export default function Home() { const { @@ -27,7 +20,12 @@ export default function Home() { error, stop, setMessages, - } = useChat({ api: basePath + "/api/chat" }); + } = useChat({ + api: basePath + "/api/chat", + onError: (error) => { + toast.error("Something went wrong: " + error); + }, + }); const [chatId, setChatId] = React.useState(""); const [chatOptions, setChatOptions] = useLocalStorageState( "chatOptions", @@ -40,8 +38,6 @@ export default function Home() { } ); - const [open, setOpen] = React.useState(false); - React.useEffect(() => { if (!isLoading && !error && chatId && messages.length > 0) { // Save messages to local storage @@ -51,12 +47,6 @@ export default function Home() { } }, [messages, chatId, isLoading, error]); - useEffect(() => { - if (!localStorage.getItem("ollama_user")) { - setOpen(true); - } - }, []); - const onSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -83,32 +73,20 @@ export default function Home() { return (
- - - - - Welcome to Ollama! - - Enter your name to get started. This is just to personalize your - experience. - - - - - +
); } diff --git a/src/components/chat/chat-bottombar.tsx b/src/components/chat/chat-bottombar.tsx index f15a0ab..f76eccf 100644 --- a/src/components/chat/chat-bottombar.tsx +++ b/src/components/chat/chat-bottombar.tsx @@ -57,8 +57,8 @@ export default function ChatBottombar({ onKeyDown={handleKeyPress} onChange={handleInputChange} name="message" - placeholder="Ask Ollama anything..." - className="border-input max-h-20 px-5 py-4 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 w-full border rounded-full flex items-center h-14 resize-none overflow-hidden dark:bg-card/35" + placeholder="Ask vLLM anything..." + className="border-input max-h-20 px-5 py-4 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 w-full border rounded-md flex items-center h-14 resize-none overflow-hidden dark:bg-card/35" /> {!isLoading ? ( - - - {models.length > 0 ? ( - models.map((model) => ( - - )) - ) : ( - - )} - - - +
+ {currentModel !== undefined && ( + <> + {isLoading ? ( + + ) : ( + + )} + + {isLoading ? "Generating.." : "Connected to vLLM server"} + + + )} + {currentModel === undefined && ( + <> + + Connection to vLLM server failed + + )} +
); diff --git a/src/components/edit-username-form.tsx b/src/components/edit-username-form.tsx deleted file mode 100644 index 5cc0151..0000000 --- a/src/components/edit-username-form.tsx +++ /dev/null @@ -1,90 +0,0 @@ -"use client"; - -import { set, z } from "zod"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import { Button } from "@/components/ui/button"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import React, { useEffect, useState } from "react"; -import { ModeToggle } from "./mode-toggle"; -import { toast } from "sonner" - - -const formSchema = z.object({ - username: z.string().min(2, { - message: "Name must be at least 2 characters.", - }), -}); - -interface EditUsernameFormProps { - setOpen: React.Dispatch>; -} - -export default function EditUsernameForm({ setOpen }: EditUsernameFormProps) { - const [name, setName] = useState(""); - - useEffect(() => { - setName(localStorage.getItem("ollama_user") || "Anonymous"); - }, []); - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - username: "", - }, - }); - - function onSubmit(values: z.infer) { - localStorage.setItem("ollama_user", values.username); - window.dispatchEvent(new Event("storage")); - toast.success("Name updated successfully"); - } - - const handleChange = (e: React.ChangeEvent) => { - e.preventDefault(); - form.setValue("username", e.currentTarget.value); - setName(e.currentTarget.value); - }; - - return ( -
-
- Theme - -
- - ( - - Name - -
- handleChange(e)} - /> - -
-
- -
- )} - /> - - - ); -} diff --git a/src/components/mode-toggle.tsx b/src/components/mode-toggle.tsx deleted file mode 100644 index 04eff39..0000000 --- a/src/components/mode-toggle.tsx +++ /dev/null @@ -1,47 +0,0 @@ -"use client"; - -import * as React from "react"; -import { ChevronDownIcon } from "@radix-ui/react-icons"; -import { useTheme } from "next-themes"; - -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; - -export function ModeToggle() { - const { setTheme, theme } = useTheme(); - - return ( - - - - - - setTheme("light")}> - Light mode - - setTheme("dark")}> - Dark mode - - - - ); -} diff --git a/src/components/settings-clear-chats.tsx b/src/components/settings-clear-chats.tsx new file mode 100644 index 0000000..add6e0b --- /dev/null +++ b/src/components/settings-clear-chats.tsx @@ -0,0 +1,63 @@ +"use client"; + +import * as React from "react"; +import { Button } from "./ui/button"; +import { TrashIcon } from "@radix-ui/react-icons"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogTrigger, +} from "@radix-ui/react-dialog"; +import { DialogHeader } from "./ui/dialog"; +import { useRouter } from "next/navigation"; + +export default function ClearChatsButton() { + const chats = Object.keys(localStorage).filter((key) => + key.startsWith("chat_") + ); + + const disabled = chats.length === 0; + const router = useRouter(); + + const clearChats = () => { + chats.forEach((key) => { + localStorage.removeItem(key); + }); + window.dispatchEvent(new Event("storage")); + router.push("/") + }; + + return ( + + + + + + + + Are you sure you want to delete all chats? This action cannot be + undone. + +
+ + +
+
+
+
+ ); +} diff --git a/src/components/settings-theme-toggle.tsx b/src/components/settings-theme-toggle.tsx new file mode 100644 index 0000000..61913c0 --- /dev/null +++ b/src/components/settings-theme-toggle.tsx @@ -0,0 +1,27 @@ +"use client"; + +import * as React from "react"; +import { SunIcon, MoonIcon } from "@radix-ui/react-icons"; +import { useTheme } from "next-themes"; +import { Button } from "./ui/button"; + +export default function SettingsThemeToggle() { + const { setTheme, theme } = useTheme(); + const nextTheme = theme === "light" ? "dark" : "light"; + + return ( + + ); +} diff --git a/src/components/settings.tsx b/src/components/settings.tsx new file mode 100644 index 0000000..5c9d242 --- /dev/null +++ b/src/components/settings.tsx @@ -0,0 +1,23 @@ +"use client"; + +import dynamic from "next/dynamic"; +import ClearChatsButton from "./settings-clear-chats"; +import SystemPrompt, { SystemPromptProps } from "./system-prompt"; + +const SettingsThemeToggle = dynamic(() => import("./settings-theme-toggle"), { + ssr: false, +}); + +export default function Settings({ + chatOptions, + setChatOptions, +}: SystemPromptProps) { + return ( + <> + + + + + + ); +} diff --git a/src/components/sidebar-skeleton.tsx b/src/components/sidebar-skeleton.tsx index 2ad3472..97a1d51 100644 --- a/src/components/sidebar-skeleton.tsx +++ b/src/components/sidebar-skeleton.tsx @@ -3,29 +3,29 @@ import { Skeleton } from "@/components/ui/skeleton"; export default function SidebarSkeleton() { return (
-
+
- +
-
+
- +
-
+
- +
-
+
- +
-
+
- +
diff --git a/src/components/sidebar.tsx b/src/components/sidebar.tsx index d397455..22d01ea 100644 --- a/src/components/sidebar.tsx +++ b/src/components/sidebar.tsx @@ -1,14 +1,14 @@ "use client"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { MoreHorizontal, SquarePen, Trash2 } from "lucide-react"; +import { SquarePen, Trash2 } from "lucide-react"; import { basePath, cn } from "@/lib/utils"; import { Button, buttonVariants } from "@/components/ui/button"; import { Message } from "ai/react"; import Image from "next/image"; import { useEffect, useState } from "react"; import SidebarSkeleton from "./sidebar-skeleton"; -import UserSettings from "./user-settings"; +import Settings from "./settings"; import { Dialog, DialogContent, @@ -17,11 +17,9 @@ import { DialogTitle, DialogTrigger, } from "./ui/dialog"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuTrigger, -} from "./ui/dropdown-menu"; + +import { DialogClose } from "@radix-ui/react-dialog"; +import { ChatOptions } from "./chat/chat-options"; interface SidebarProps { isCollapsed: boolean; @@ -29,6 +27,12 @@ interface SidebarProps { onClick?: () => void; isMobile: boolean; chatId: string; + chatOptions: ChatOptions; + setChatOptions: React.Dispatch>; +} + +interface Chats { + [key: string]: { chatId: string; messages: Message[] }[]; } export function Sidebar({ @@ -36,10 +40,10 @@ export function Sidebar({ isCollapsed, isMobile, chatId, + chatOptions, + setChatOptions, }: SidebarProps) { - const [localChats, setLocalChats] = useState< - { chatId: string; messages: Message[] }[] - >([]); + const [localChats, setLocalChats] = useState({}); const router = useRouter(); const [selectedChatId, setSselectedChatId] = useState(null); const [isLoading, setIsLoading] = useState(true); @@ -59,10 +63,7 @@ export function Sidebar({ }; }, [chatId]); - const getLocalstorageChats = (): { - chatId: string; - messages: Message[]; - }[] => { + const getLocalstorageChats = (): Chats => { const chats = Object.keys(localStorage).filter((key) => key.startsWith("chat_") ); @@ -86,8 +87,48 @@ export function Sidebar({ return bDate.getTime() - aDate.getTime(); }); + const groupChatsByDate = ( + chats: { chatId: string; messages: Message[] }[] + ) => { + const today = new Date(); + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); + + const groupedChats: Chats = {}; + + chats.forEach((chat) => { + const createdAt = new Date(chat.messages[0].createdAt ?? ""); + const diffInDays = Math.floor( + (today.getTime() - createdAt.getTime()) / (1000 * 3600 * 24) + ); + + let group: string; + if (diffInDays === 0) { + group = "Today"; + } else if (diffInDays === 1) { + group = "Yesterday"; + } else if (diffInDays <= 7) { + group = "Previous 7 Days"; + } else if (diffInDays <= 30) { + group = "Previous 30 Days"; + } else { + group = "Older"; + } + + if (!groupedChats[group]) { + groupedChats[group] = []; + } + groupedChats[group].push(chat); + }); + + return groupedChats; + }; + setIsLoading(false); - return chatObjects; + const groupedChats = groupChatsByDate(chatObjects); + + return groupedChats; + // return chatObjects; }; const handleDeleteChat = (chatId: string) => { @@ -98,9 +139,10 @@ export function Sidebar({ return (
-
+
+
-
-

Your chats

- {localChats.length > 0 && ( -
- {localChats.map(({ chatId, messages }, index) => ( - -
-
- - {messages.length > 0 ? messages[0].content : ""} - -
-
- - - - - - - - - - - - Delete chat? - - Are you sure you want to delete this chat? This - action cannot be undone. - -
- - -
-
-
-
-
-
- - ))} -
- )} - {isLoading && } -
+
+ {Object.keys(localChats).length > 0 && ( +
+ {Object.keys(localChats).map((group, index) => ( +
+

+ {group} +

+
    + {localChats[group].map(({ chatId, messages }, chatIndex) => ( +
  1. +
    + + + {messages.length > 0 ? messages[0].content : ""} + + +
    +
    + + + + + + + Delete chat? + + Are you sure you want to delete this chat? This + action cannot be undone. + +
    + + +
    +
    +
    +
    +
    +
  2. + ))} +
+
+ ))} +
+ )} + {isLoading && }
-
- +
+
); diff --git a/src/components/system-prompt-form.tsx b/src/components/system-prompt-form.tsx index 3ffdf65..1d6eab6 100644 --- a/src/components/system-prompt-form.tsx +++ b/src/components/system-prompt-form.tsx @@ -15,6 +15,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { toast } from "sonner"; import TextareaAutosize from "react-textarea-autosize"; import { SystemPromptProps } from "./system-prompt"; +import { DialogClose } from "@radix-ui/react-dialog"; const formSchema = z.object({ name: z.string().min(1, { message: "Please set a system prompt", @@ -66,7 +67,7 @@ export default function SystemPromptForm({
)} /> -
+ -
+ ); diff --git a/src/components/system-prompt.tsx b/src/components/system-prompt.tsx index fa0738d..b1130f2 100644 --- a/src/components/system-prompt.tsx +++ b/src/components/system-prompt.tsx @@ -3,6 +3,7 @@ import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "./ui/dialog"; import { MixIcon } from "@radix-ui/react-icons"; import SystemPromptForm from "./system-prompt-form"; import { ChatOptions } from "./chat/chat-options"; +import { Button } from "./ui/button"; export interface SystemPromptProps { chatOptions: ChatOptions; setChatOptions: React.Dispatch>; @@ -11,13 +12,18 @@ export default function SystemPrompt({ chatOptions, setChatOptions, }: SystemPromptProps) { + const [open, setOpen] = React.useState(false); return ( -
+
+
Save system prompt diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx index 51e507b..a02efc9 100644 --- a/src/components/ui/avatar.tsx +++ b/src/components/ui/avatar.tsx @@ -32,19 +32,4 @@ const AvatarImage = React.forwardRef< )) AvatarImage.displayName = AvatarPrimitive.Image.displayName -const AvatarFallback = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName - -export { Avatar, AvatarImage, AvatarFallback } +export { Avatar, AvatarImage } diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 9c38b4b..4d1896d 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -5,26 +5,26 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", + "inline-flex items-center justify-center whitespace-nowrap rounded-xs text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { default: - "bg-primary text-primary-foreground shadow hover:bg-primary/90", + "bg-primary text-primary-foreground hover:bg-primary/90", destructive: - "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: - "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", secondary: - "dark:bg-card/60 bg-accent/20 text-secondary-foreground shadow-sm hover:bg-secondary/60 hover:dark:bg-card/40", + "dark:bg-card/60 bg-accent/20 text-secondary-foreground hover:bg-secondary/60 hover:dark:bg-card/40", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", - secondaryLink: "bg-accent/90 dark:bg-secondary/80 text-secondary-foreground shadow-sm dark:hover:bg-secondary hover:bg-accent", + secondaryLink: "bg-accent/90 dark:bg-secondary/80 text-secondary-foreground dark:hover:bg-secondary hover:bg-accent", }, size: { default: "h-9 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-8", + sm: "h-8 rounded-xs px-3 text-xs", + lg: "h-10 rounded-xs px-8", icon: "h-14 w-14", iconSm: "h-8 w-8" }, diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 77e9fb7..ceb6a7f 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -9,7 +9,7 @@ const Card = React.forwardRef<
, - React.ComponentPropsWithoutRef & { - inset?: boolean - } ->(({ className, inset, children, ...props }, ref) => ( - - {children} - - -)) -DropdownMenuSubTrigger.displayName = - DropdownMenuPrimitive.SubTrigger.displayName - -const DropdownMenuSubContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DropdownMenuSubContent.displayName = - DropdownMenuPrimitive.SubContent.displayName - -const DropdownMenuContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, sideOffset = 4, ...props }, ref) => ( - - - -)) -DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName - -const DropdownMenuItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean - } ->(({ className, inset, ...props }, ref) => ( - -)) -DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName - -const DropdownMenuCheckboxItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, checked, ...props }, ref) => ( - - - - - - - {children} - -)) -DropdownMenuCheckboxItem.displayName = - DropdownMenuPrimitive.CheckboxItem.displayName - -const DropdownMenuRadioItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - - - - {children} - -)) -DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName - -const DropdownMenuLabel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - inset?: boolean - } ->(({ className, inset, ...props }, ref) => ( - -)) -DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName - -const DropdownMenuSeparator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName - -const DropdownMenuShortcut = ({ - className, - ...props -}: React.HTMLAttributes) => { - return ( - - ) -} -DropdownMenuShortcut.displayName = "DropdownMenuShortcut" - -export { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuCheckboxItem, - DropdownMenuRadioItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuGroup, - DropdownMenuPortal, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuRadioGroup, -} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index a92b8e0..7193cf1 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -11,7 +11,7 @@ const Input = React.forwardRef( , - React.ComponentPropsWithoutRef ->(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( - - - -)) -PopoverContent.displayName = PopoverPrimitive.Content.displayName - -export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx index ac2a8f2..557c0cd 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -24,7 +24,7 @@ const SelectTrigger = React.forwardRef< span]:line-clamp-1", + "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-xs border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", className )} {...props} @@ -80,7 +80,7 @@ const SelectContent = React.forwardRef< ) { return (
) diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx index f31588b..586efa6 100644 --- a/src/components/ui/textarea.tsx +++ b/src/components/ui/textarea.tsx @@ -10,7 +10,7 @@ const Textarea = React.forwardRef( return (