Run biome check and add ChatError component

This commit is contained in:
James Blair 2026-05-08 20:33:24 +12:00
parent 6a5041be43
commit 08f45451bb
Signed by: james
SSH key fingerprint: SHA256:hsDR3ENI2ZMgh+CNb+0PrFsj29DI007R+BMKUVGi6zQ
13 changed files with 228 additions and 136 deletions

View file

@ -1,6 +1,5 @@
import { Chat } from "@/components/chat";
import { Container } from "@/components/container";
import { Prose } from "@/components/prose";
export default function ChatPage() {
return (

View file

@ -19,7 +19,8 @@
"rules": {
"recommended": true,
"suspicious": {
"noUnknownAtRules": "off"
"noUnknownAtRules": "off",
"noArrayIndexKey": "off"
},
"complexity": {
"noStaticOnlyClass": "off"

View file

@ -0,0 +1,14 @@
import { AlertTriangleIcon } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
export function ChatError({ error }: { error?: Error }) {
if (!error) return null;
return (
<Alert variant="destructive">
<AlertTriangleIcon />
<AlertTitle>{error.name}</AlertTitle>
<AlertDescription>{error.message}</AlertDescription>
</Alert>
);
}

View file

@ -1,7 +1,7 @@
import { useState, type SubmitEvent } from "react";
import { ArrowUpIcon } from "lucide-react";
import { type SubmitEvent, useState } from "react";
import { Button } from "../ui/button";
import { Textarea } from "../ui/textarea";
import { ArrowUpIcon, SendIcon } from "lucide-react";
interface ChatFormProps {
onMessage: (message: { text: string }) => void;

View file

@ -1,5 +1,5 @@
import type { UIMessage } from "ai";
import { cn } from "@/lib/utils";
import { UIMessage } from "ai";
interface ChatMessagesProps {
messages: UIMessage[];

View file

@ -1,9 +1,10 @@
"use client";
import { ChatMessages } from "./chat-messages";
import { ChatForm } from "./chat-form";
import { DefaultChatTransport } from "ai";
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({
@ -13,7 +14,8 @@ export function Chat() {
});
return (
<div className="max-w-2xl flex flex-col gap-2">
<div className="max-w-2xl flex flex-col gap-4">
<ChatError error={error} />
<ChatMessages messages={messages} />
<ChatForm
onMessage={sendMessage}

76
components/ui/alert.tsx Normal file
View 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 };

View file

@ -1,8 +1,8 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cva, type VariantProps } from "class-variance-authority";
import { Slot } from "radix-ui";
import type * as React from "react";
import { cn } from "@/lib/utils"
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",
@ -38,8 +38,8 @@ const buttonVariants = cva(
variant: "default",
size: "default",
},
}
)
},
);
function Button({
className,
@ -49,9 +49,9 @@ function Button({
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
asChild?: boolean;
}) {
const Comp = asChild ? Slot.Root : "button"
const Comp = asChild ? Slot.Root : "button";
return (
<Comp
@ -61,7 +61,7 @@ function Button({
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
);
}
export { Button, buttonVariants }
export { Button, buttonVariants };

View file

@ -1,6 +1,6 @@
import * as React from "react"
import type * as React from "react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
@ -8,11 +8,11 @@ function Textarea({ className, ...props }: React.ComponentProps<"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
className,
)}
{...props}
/>
)
);
}
export { Textarea }
export { Textarea };

View file

@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}

View file

@ -1,5 +1,5 @@
import Elysia from "elysia";
import { health } from "./modules/health";
import { chat } from "./modules/chat";
import { health } from "./modules/health";
export const app = new Elysia({ prefix: "/api" }).use(health).use(chat);

View file

@ -1,6 +1,6 @@
import { openai } from "@ai-sdk/openai";
import { convertToModelMessages, streamText } from "ai";
import type { ChatModel } from "./model";
import { openai } from "@ai-sdk/openai";
export abstract class Chat {
static async streamText(chat: ChatModel["chat"]) {