Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { streamText } from "ai";

export async function POST(req: Request) {
const { messages } = await req.json();

const response = await streamText({
model: "gpt-4o-mini",
messages,
});

return response.toTextStreamResponse();
}
7 changes: 7 additions & 0 deletions app/chat/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"use client";

import ChatLayout from "@/components/chat/ChatLayout";

export default function ChatPage() {
return <ChatLayout />;
}
88 changes: 44 additions & 44 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,71 +5,71 @@

:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--card-foreground: oklch(0.13 0.028 261.692);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.13 0.028 261.692);
--primary: oklch(0.21 0.034 264.665);
--primary-foreground: oklch(0.985 0.002 247.839);
--secondary: oklch(0.967 0.003 264.542);
--secondary-foreground: oklch(0.21 0.034 264.665);
--muted: oklch(0.967 0.003 264.542);
--muted-foreground: oklch(0.551 0.027 264.364);
--accent: oklch(0.967 0.003 264.542);
--accent-foreground: oklch(0.21 0.034 264.665);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--border: oklch(0.928 0.006 264.531);
--input: oklch(0.928 0.006 264.531);
--ring: oklch(0.707 0.022 261.325);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
--sidebar: oklch(0.985 0.002 247.839);
--sidebar-foreground: oklch(0.13 0.028 261.692);
--sidebar-primary: oklch(0.21 0.034 264.665);
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
--sidebar-accent: oklch(0.967 0.003 264.542);
--sidebar-accent-foreground: oklch(0.21 0.034 264.665);
--sidebar-border: oklch(0.928 0.006 264.531);
--sidebar-ring: oklch(0.707 0.022 261.325);
--background: oklch(1 0 0);
--foreground: oklch(0.13 0.028 261.692);
}

.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.92 0.004 286.32);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--background: oklch(0.13 0.028 261.692);
--foreground: oklch(0.985 0.002 247.839);
--card: oklch(0.21 0.034 264.665);
--card-foreground: oklch(0.985 0.002 247.839);
--popover: oklch(0.21 0.034 264.665);
--popover-foreground: oklch(0.985 0.002 247.839);
--primary: oklch(0.928 0.006 264.531);
--primary-foreground: oklch(0.21 0.034 264.665);
--secondary: oklch(0.278 0.033 256.848);
--secondary-foreground: oklch(0.985 0.002 247.839);
--muted: oklch(0.278 0.033 256.848);
--muted-foreground: oklch(0.707 0.022 261.325);
--accent: oklch(0.278 0.033 256.848);
--accent-foreground: oklch(0.985 0.002 247.839);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
--ring: oklch(0.551 0.027 264.364);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar: oklch(0.21 0.034 264.665);
--sidebar-foreground: oklch(0.985 0.002 247.839);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
--sidebar-accent: oklch(0.278 0.033 256.848);
--sidebar-accent-foreground: oklch(0.985 0.002 247.839);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.552 0.016 285.938);
--sidebar-ring: oklch(0.551 0.027 264.364);
}

@theme inline {
Expand Down
5 changes: 3 additions & 2 deletions components.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "zinc",
"baseColor": "gray",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
"registries": {}
}
18 changes: 18 additions & 0 deletions components/chat/ChatHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use client";

// import { Separator } from "@/components/ui/separator";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";

export default function ChatHeader() {
return (
<div className="w-full p-4 flex items-center gap-3 border-b">
<Avatar className="h-9 w-9">
<AvatarFallback>AI</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<h1 className="text-lg font-semibold tracking-tight">Chatbot</h1>
<p className="text-xs text-muted-foreground">Ask me anything</p>
</div>
</div>
);
}
33 changes: 33 additions & 0 deletions components/chat/ChatInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"use client";

import type { ChangeEvent, FormEvent } from "react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";

export default function ChatInput({
input,
handleInputChange,
handleSubmit,
}: {
input: string;
handleInputChange: (e: ChangeEvent<HTMLTextAreaElement>) => void;
handleSubmit: (e: FormEvent<HTMLFormElement>) => void;
}) {
return (
<form
onSubmit={handleSubmit}
className="w-full border-t p-4 flex items-end gap-2 bg-black"
>
<Textarea
value={input}
onChange={handleInputChange}
placeholder="Type your message…"
className="min-h-[52px] max-h-[120px] resize-none"
/>

<Button type="submit" className="h-[52px] px-6">
Send
</Button>
</form>
);
}
31 changes: 31 additions & 0 deletions components/chat/ChatLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client";

import ChatHeader from "./ChatHeader";
import ChatMessages from "./ChatMessages";
import ChatInput from "./ChatInput";
import { useChat } from "@ai-sdk/react"; // correct package

export default function ChatLayout() {
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: "/api/chat",
});

return (
<div className="w-full min-h-screen flex items-center justify-center bg-gray-1000">
<div className="w-full max-w-md h-[80vh] flex flex-col bg-black rounded-2xl shadow-2xl border border-gray-200 overflow-hidden">
{/* Header */}
<ChatHeader />

{/* Messages */}
<ChatMessages messages={messages} />

{/* Input */}
<ChatInput
input={input}
handleInputChange={handleInputChange}
handleSubmit={handleSubmit}
/>
</div>
</div>
);
}
33 changes: 33 additions & 0 deletions components/chat/ChatMessages.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"use client";

import { useEffect, useRef } from "react";
import type { ReactNode } from "react";
import { ScrollArea } from "@/components/ui/scroll-area";
import MessageBubble from "./MessageBubble";
import type { UIMessage } from "ai";

type Message = UIMessage & { content?: ReactNode | string };

export default function ChatMessages({ messages }: { messages: Message[] }) {
const bottomRef = useRef<HTMLDivElement>(null);

useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);

return (
<ScrollArea className="flex-1 overflow-y-auto p-4">
<div className="flex flex-col gap-4">
{messages.map((msg) => (
<MessageBubble
key={msg.id}
role={msg.role === "assistant" ? "assistant" : "user"}
content={msg.content}
/>
))}

<div ref={bottomRef} />
</div>
</ScrollArea>
);
}
46 changes: 46 additions & 0 deletions components/chat/MessageBubble.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use client";

import { cn } from "@/lib/utils";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { ReactNode } from "react";

interface MessageBubbleProps {
role: "user" | "assistant";
content?: ReactNode | string;
}

export default function MessageBubble({ role, content }: MessageBubbleProps) {
const isUser = role === "user";

return (
<div
className={cn(
"flex w-full items-start gap-2",
isUser && "justify-end"
)}
>
{!isUser && (
<Avatar className="h-8 w-8">
<AvatarFallback>AI</AvatarFallback>
</Avatar>
)}

<div
className={cn(
"max-w-[80%] rounded-2xl px-4 py-2 text-sm",
isUser
? "bg-primary text-primary-foreground"
: "bg-secondary text-secondary-foreground"
)}
>
{content}
</div>

{isUser && (
<Avatar className="h-8 w-8">
<AvatarFallback>U</AvatarFallback>
</Avatar>
)}
</div>
);
}
6 changes: 3 additions & 3 deletions lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
return twMerge(clsx(inputs))
}
Loading