Compare commits
3 commits
80056b3728
...
59d2358845
| Author | SHA1 | Date | |
|---|---|---|---|
| 59d2358845 | |||
| 08f45451bb | |||
| 6a5041be43 |
18 changed files with 1350 additions and 3 deletions
10
app/chat/page.tsx
Normal file
10
app/chat/page.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Chat } from "@/components/chat";
|
||||
import { Container } from "@/components/container";
|
||||
|
||||
export default function ChatPage() {
|
||||
return (
|
||||
<Container>
|
||||
<Chat />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
128
app/globals.css
128
app/globals.css
|
|
@ -1,2 +1,130 @@
|
|||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
@theme inline {
|
||||
--font-heading: var(--font-sans);
|
||||
--font-sans: var(--font-sans);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--background);
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.514 0.222 16.935);
|
||||
--primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.81 0.117 11.638);
|
||||
--chart-2: oklch(0.645 0.246 16.439);
|
||||
--chart-3: oklch(0.586 0.253 17.585);
|
||||
--chart-4: oklch(0.514 0.222 16.935);
|
||||
--chart-5: oklch(0.455 0.188 13.697);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.586 0.253 17.585);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.455 0.188 13.697);
|
||||
--primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.81 0.117 11.638);
|
||||
--chart-2: oklch(0.645 0.246 16.439);
|
||||
--chart-3: oklch(0.586 0.253 17.585);
|
||||
--chart-4: oklch(0.514 0.222 16.935);
|
||||
--chart-5: oklch(0.455 0.188 13.697);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.645 0.246 16.439);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
html {
|
||||
@apply font-sans;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@
|
|||
"rules": {
|
||||
"recommended": true,
|
||||
"suspicious": {
|
||||
"noUnknownAtRules": "off"
|
||||
"noUnknownAtRules": "off",
|
||||
"noArrayIndexKey": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noStaticOnlyClass": "off"
|
||||
|
|
|
|||
25
components.json
Normal file
25
components.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "radix-nova",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"rtl": false,
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"menuColor": "default",
|
||||
"menuAccent": "subtle",
|
||||
"registries": {}
|
||||
}
|
||||
12
components/chat/chat-error.tsx
Normal file
12
components/chat/chat-error.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { AlertTriangleIcon } from "lucide-react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
|
||||
|
||||
export function ChatError({ error }: { error: Error }) {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangleIcon />
|
||||
<AlertTitle>{error.name}</AlertTitle>
|
||||
<AlertDescription>{error.message}</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
34
components/chat/chat-form.tsx
Normal file
34
components/chat/chat-form.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { ArrowUpIcon } from "lucide-react";
|
||||
import { type SubmitEvent, useState } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
|
||||
interface ChatFormProps {
|
||||
onMessage: (message: { text: string }) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function ChatForm({ onMessage, disabled }: ChatFormProps) {
|
||||
const [text, setText] = useState("");
|
||||
|
||||
function handleSubmit(e: SubmitEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
if (!text.trim()) return;
|
||||
setText("");
|
||||
onMessage({ text });
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="flex gap-2">
|
||||
<Textarea
|
||||
name="text"
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
className="min-h-18"
|
||||
/>
|
||||
<Button type="submit" disabled={disabled} size="icon-lg">
|
||||
<ArrowUpIcon />
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
32
components/chat/chat-messages.tsx
Normal file
32
components/chat/chat-messages.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import type { UIMessage } from "ai";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ChatMessagesProps {
|
||||
messages: UIMessage[];
|
||||
}
|
||||
|
||||
export function ChatMessages({ messages }: ChatMessagesProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{messages.map((message, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
"bg-primary text-primary-foreground px-3 py-2 rounded-lg whitespace-pre-wrap wrap-break-word break-all",
|
||||
message.role === "user"
|
||||
? "self-end bg-primary text-primary-foreground"
|
||||
: "self-start bg-accent text-accent-foreground",
|
||||
)}
|
||||
>
|
||||
{message.parts.map((part, index) => {
|
||||
if (part.type === "text") {
|
||||
return <span key={index}>{part.text}</span>;
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
26
components/chat/index.tsx
Normal file
26
components/chat/index.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
"use client";
|
||||
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { DefaultChatTransport } from "ai";
|
||||
import { ChatError } from "./chat-error";
|
||||
import { ChatForm } from "./chat-form";
|
||||
import { ChatMessages } from "./chat-messages";
|
||||
|
||||
export function Chat() {
|
||||
const { messages, sendMessage, status, error } = useChat({
|
||||
transport: new DefaultChatTransport({
|
||||
api: "http://localhost:3000/api/chat",
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl flex flex-col gap-4">
|
||||
{error && <ChatError error={error} />}
|
||||
<ChatMessages messages={messages} />
|
||||
<ChatForm
|
||||
onMessage={sendMessage}
|
||||
disabled={status === "submitted" || status === "streaming"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
76
components/ui/alert.tsx
Normal file
76
components/ui/alert.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const alertVariants = cva(
|
||||
"group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive:
|
||||
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"font-medium group-has-[>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 (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-action"
|
||||
className={cn("absolute top-2 right-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription, AlertAction };
|
||||
67
components/ui/button.tsx
Normal file
67
components/ui/button.tsx
Normal file
|
|
@ -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<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants };
|
||||
18
components/ui/textarea.tsx
Normal file
18
components/ui/textarea.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Textarea };
|
||||
6
lib/utils.ts
Normal file
6
lib/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
12
package.json
12
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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
15
server/modules/chat/index.ts
Normal file
15
server/modules/chat/index.ts
Normal file
|
|
@ -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,
|
||||
},
|
||||
);
|
||||
21
server/modules/chat/model.ts
Normal file
21
server/modules/chat/model.ts
Normal file
|
|
@ -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]>;
|
||||
};
|
||||
13
server/modules/chat/service.ts
Normal file
13
server/modules/chat/service.ts
Normal file
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue