svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+function AlertDescription({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+export { Alert, AlertTitle, AlertDescription, AlertAction };
diff --git a/components/ui/button.tsx b/components/ui/button.tsx
new file mode 100644
index 0000000..188ca1f
--- /dev/null
+++ b/components/ui/button.tsx
@@ -0,0 +1,67 @@
+import { cva, type VariantProps } from "class-variance-authority";
+import { Slot } from "radix-ui";
+import type * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+const buttonVariants = cva(
+ "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
+ outline:
+ "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
+ ghost:
+ "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
+ destructive:
+ "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default:
+ "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
+ icon: "size-8",
+ "icon-xs":
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
+ "icon-sm":
+ "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
+ "icon-lg": "size-9",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+function Button({
+ className,
+ variant = "default",
+ size = "default",
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps
& {
+ asChild?: boolean;
+ }) {
+ const Comp = asChild ? Slot.Root : "button";
+
+ return (
+
+ );
+}
+
+export { Button, buttonVariants };
diff --git a/components/ui/textarea.tsx b/components/ui/textarea.tsx
new file mode 100644
index 0000000..9da9eab
--- /dev/null
+++ b/components/ui/textarea.tsx
@@ -0,0 +1,18 @@
+import type * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
+ return (
+
+ );
+}
+
+export { Textarea };
diff --git a/lib/utils.ts b/lib/utils.ts
new file mode 100644
index 0000000..365058c
--- /dev/null
+++ b/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/package.json b/package.json
index 43d562d..88b0092 100644
--- a/package.json
+++ b/package.json
@@ -10,11 +10,21 @@
"format": "biome format --write"
},
"dependencies": {
+ "@ai-sdk/openai": "^3.0.63",
+ "@ai-sdk/react": "^3.0.178",
"@elysia/eden": "^1.4.10",
+ "ai": "^6.0.176",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
"elysia": "^1.4.28",
+ "lucide-react": "^1.14.0",
"next": "16.2.6",
+ "radix-ui": "^1.4.3",
"react": "19.2.4",
- "react-dom": "19.2.4"
+ "react-dom": "19.2.4",
+ "shadcn": "^4.7.0",
+ "tailwind-merge": "^3.5.0",
+ "tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@biomejs/biome": "2.2.0",
diff --git a/server/index.ts b/server/index.ts
index e45f4f5..e3894f0 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -1,4 +1,5 @@
import Elysia from "elysia";
+import { chat } from "./modules/chat";
import { health } from "./modules/health";
-export const app = new Elysia({ prefix: "/api" }).use(health);
+export const app = new Elysia({ prefix: "/api" }).use(health).use(chat);
diff --git a/server/modules/chat/index.ts b/server/modules/chat/index.ts
new file mode 100644
index 0000000..944422b
--- /dev/null
+++ b/server/modules/chat/index.ts
@@ -0,0 +1,15 @@
+import { Elysia } from "elysia";
+
+import { ChatModel } from "./model";
+import { Chat } from "./service";
+
+export const chat = new Elysia({ prefix: "/chat" }).post(
+ "/",
+ async ({ body }) => {
+ const stream = await Chat.streamText(body);
+ return stream.toUIMessageStreamResponse();
+ },
+ {
+ body: ChatModel.chat,
+ },
+);
diff --git a/server/modules/chat/model.ts b/server/modules/chat/model.ts
new file mode 100644
index 0000000..0c06ea5
--- /dev/null
+++ b/server/modules/chat/model.ts
@@ -0,0 +1,21 @@
+import { t, type UnwrapSchema } from "elysia";
+
+export const ChatModel = {
+ chat: t.Object({
+ messages: t.Array(
+ t.Object({
+ id: t.String(),
+ role: t.Union([
+ t.Literal("user"),
+ t.Literal("assistant"),
+ t.Literal("system"),
+ ]),
+ parts: t.Array(t.Any()),
+ }),
+ ),
+ }),
+} as const;
+
+export type ChatModel = {
+ [k in keyof typeof ChatModel]: UnwrapSchema<(typeof ChatModel)[k]>;
+};
diff --git a/server/modules/chat/service.ts b/server/modules/chat/service.ts
new file mode 100644
index 0000000..acb8826
--- /dev/null
+++ b/server/modules/chat/service.ts
@@ -0,0 +1,13 @@
+import { openai } from "@ai-sdk/openai";
+import { convertToModelMessages, streamText } from "ai";
+import type { ChatModel } from "./model";
+
+export abstract class Chat {
+ static async streamText(chat: ChatModel["chat"]) {
+ return streamText({
+ model: openai("gpt-5.5"),
+ system: "You are Yae Miko from Genshin Impact",
+ messages: await convertToModelMessages(chat.messages),
+ });
+ }
+}