Components / AI Chat Interfaces
AI Chat Window
A full-featured AI chat window with message history, streaming response animation, and conversation management.
Components / AI Chat Interfaces
A full-featured AI chat window with message history, streaming response animation, and conversation management.
Install the core libraries required for this component.
npm install reactAdd the necessary Shadcn UI primitives.
npx shadcn-ui@latest add card scroll-area avatar button textarea select separatorimport { useState, useEffect, useRef } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
export default function AIChatWindow01() {
const [messages, setMessages] = useState<
{ id: number; author: string; text: string; avatar: string }[]
>([
{
id: 1,
author: "ChatGPT",
text: "Welcome! I'm ChatGPT. Ask me anything.",
avatar: "https://i.pravatar.cc/40?img=12",
},
]);
const [inputMessage, setInputMessage] = useState("");
const [selectedModel, setSelectedModel] = useState("chatgpt");
const [searchEnabled, setSearchEnabled] = useState(false);
const [isStreaming, setIsStreaming] = useState(false);
const scrollRef = useRef<HTMLDivElement>(null);
// Auto-scroll to bottom on new message
useEffect(() => {
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight });
}, [messages, isStreaming]);
const handleSend = () => {
if (!inputMessage.trim()) return;
const newMessage = {
id: messages.length + 1,
author: "User",
text: inputMessage,
avatar: "https://i.pravatar.cc/40?img=5",
};
setMessages((prev) => [...prev, newMessage]);
setInputMessage("");
// Simulate AI streaming response
setIsStreaming(true);
setTimeout(() => {
const aiResponse = {
id: messages.length + 2,
author: "ChatGPT",
text: `Response from ${selectedModel} ${searchEnabled ? "(with search context)" : ""
}`,
avatar: "https://i.pravatar.cc/40?img=12",
};
setMessages((prev) => [...prev, aiResponse]);
setIsStreaming(false);
}, 1200);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
const handleClearConversation = () => {
setMessages([]);
};
return (
<Card className="border-0 flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-2 ">
<h3 className="font-semibold">AI Chat Window</h3>
<div className="flex gap-2">
<Button onClick={handleClearConversation}>
New Chat
</Button>
<Select onValueChange={setSelectedModel} defaultValue={selectedModel}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="Select model" />
</SelectTrigger>
<SelectContent>
<SelectItem value="chatgpt">ChatGPT</SelectItem>
<SelectItem value="gpt4">GPT-4</SelectItem>
<SelectItem value="custom-llm">Custom LLM</SelectItem>
</SelectContent>
</Select>
<Button
variant={searchEnabled ? "default" : "outline"}
onClick={() => setSearchEnabled((prev) => !prev)}
>
{searchEnabled ? "Search On" : "Search"}
</Button>
</div>
</div>
{/* Message history */}
<CardContent className="p-2 flex-1 overflow-hidden">
<ScrollArea
ref={scrollRef}
className="h-full gap-3 mb-4 pr-2"
>
{messages.map((msg) => (
<div
key={msg.id}
className={`flex items-start gap-3 ${msg.author === "ChatGPT" ? "flex-row" : "flex-row-reverse"
}`}
>
<Avatar>
<AvatarImage src={msg.avatar} />
<AvatarFallback>{msg.author[0]}</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<div className="p-2 border rounded-lg border-border max-w-xs">
{msg.text}
</div>
</div>
</div>
))}
{isStreaming && (
<div className="flex items-start gap-3">
<Avatar>
<AvatarImage src="https://i.pravatar.cc/40?img=12" />
<AvatarFallback>A</AvatarFallback>
</Avatar>
<div className="p-2 border rounded-lg border-border max-w-xs italic text-muted-foreground">
AI is typing...
</div>
</div>
)}
</ScrollArea>
</CardContent>
{/* Input */}
<div className="flex flex-col gap-2 p-2">
<div className="flex items-end gap-2">
<Textarea
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type your message..."
className="resize-none grow overflow-hidden p-2"
rows={1}
/>
<Button onClick={handleSend}>Send</Button>
</div>
</div>
</Card>
);
} import { useState, useEffect, useRef } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
export default function AIChatWindow01() {
const [messages, setMessages] = useState<
{ id: number; author: string; text: string; avatar: string }[]
>([
{
id: 1,
author: "ChatGPT",
text: "Welcome! I'm ChatGPT. Ask me anything.",
avatar: "https://i.pravatar.cc/40?img=12",
},
]);
const [inputMessage, setInputMessage] = useState("");
const [selectedModel, setSelectedModel] = useState("chatgpt");
const [searchEnabled, setSearchEnabled] = useState(false);
const [isStreaming, setIsStreaming] = useState(false);
const scrollRef = useRef<HTMLDivElement>(null);
// Auto-scroll to bottom on new message
useEffect(() => {
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight });
}, [messages, isStreaming]);
const handleSend = () => {
if (!inputMessage.trim()) return;
const newMessage = {
id: messages.length + 1,
author: "User",
text: inputMessage,
avatar: "https://i.pravatar.cc/40?img=5",
};
setMessages((prev) => [...prev, newMessage]);
setInputMessage("");
// Simulate AI streaming response
setIsStreaming(true);
setTimeout(() => {
const aiResponse = {
id: messages.length + 2,
author: "ChatGPT",
text: `Response from ${selectedModel} ${searchEnabled ? "(with search context)" : ""
}`,
avatar: "https://i.pravatar.cc/40?img=12",
};
setMessages((prev) => [...prev, aiResponse]);
setIsStreaming(false);
}, 1200);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
const handleClearConversation = () => {
setMessages([]);
};
return (
<Card className="border-0 flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-2 ">
<h3 className="font-semibold">AI Chat Window</h3>
<div className="flex gap-2">
<Button onClick={handleClearConversation}>
New Chat
</Button>
<Select onValueChange={setSelectedModel} defaultValue={selectedModel}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="Select model" />
</SelectTrigger>
<SelectContent>
<SelectItem value="chatgpt">ChatGPT</SelectItem>
<SelectItem value="gpt4">GPT-4</SelectItem>
<SelectItem value="custom-llm">Custom LLM</SelectItem>
</SelectContent>
</Select>
<Button
variant={searchEnabled ? "default" : "outline"}
onClick={() => setSearchEnabled((prev) => !prev)}
>
{searchEnabled ? "Search On" : "Search"}
</Button>
</div>
</div>
{/* Message history */}
<CardContent className="p-2 flex-1 overflow-hidden">
<ScrollArea
ref={scrollRef}
className="h-full gap-3 mb-4 pr-2"
>
{messages.map((msg) => (
<div
key={msg.id}
className={`flex items-start gap-3 ${msg.author === "ChatGPT" ? "flex-row" : "flex-row-reverse"
}`}
>
<Avatar>
<AvatarImage src={msg.avatar} />
<AvatarFallback>{msg.author[0]}</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<div className="p-2 border rounded-lg border-border max-w-xs">
{msg.text}
</div>
</div>
</div>
))}
{isStreaming && (
<div className="flex items-start gap-3">
<Avatar>
<AvatarImage src="https://i.pravatar.cc/40?img=12" />
<AvatarFallback>A</AvatarFallback>
</Avatar>
<div className="p-2 border rounded-lg border-border max-w-xs italic text-muted-foreground">
AI is typing...
</div>
</div>
)}
</ScrollArea>
</CardContent>
{/* Input */}
<div className="flex flex-col gap-2 p-2">
<div className="flex items-end gap-2">
<Textarea
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type your message..."
className="resize-none grow overflow-hidden p-2"
rows={1}
/>
<Button onClick={handleSend}>Send</Button>
</div>
</div>
</Card>
);
}
Primitives required in your
@/components/ui
directory.
| Component | Path |
|---|---|
| card | @/components/ui/card |
| scroll area | @/components/ui/scroll-area |
| avatar | @/components/ui/avatar |
| button | @/components/ui/button |
| textarea | @/components/ui/textarea |
| select | @/components/ui/select |
| separator | @/components/ui/separator |
Internal functions used to handle component logic.
| Function | Parameters |
|---|---|
| handleSend() | None |
| handleKeyDown() | e: React.KeyboardEvent<HTMLTextAreaElement> |
| handleClearConversation() | None |
| AIChatWindow01() | None |
React state variables managed within this component.
| Variable | Initial Value |
|---|---|
| inputMessage | "" |
| selectedModel | "chatgpt" |
| searchEnabled | false |
| isStreaming | false |
All variable declarations found in this component.
| Name | Kind | Value |
|---|---|---|
| scrollRef | | useRef<HTMLDivElement>(null) |
| newMessage | | { |
| aiResponse | | { |
Every JSX/HTML tag used in this component, with usage count and source.
| Tag | Count | Type | Source |
|---|---|---|---|
| <Avatar> | 2× | | @/components/ui/avatar |
| <AvatarFallback> | 2× | | @/components/ui/avatar |
| <AvatarImage> | 2× | | @/components/ui/avatar |
| <Card> | 1× | | @/components/ui/card |
| <CardContent> | 1× | | @/components/ui/card |
| <HTMLDivElement> | 1× | | Local / Inline |
| <HTMLTextAreaElement> | 1× | | Local / Inline |
| <ScrollArea> | 1× | | @/components/ui/scroll-area |
| <SelectContent> | 1× | | @/components/ui/select |
| <SelectItem> | 3× | | @/components/ui/select |
| <SelectTrigger> | 1× | | @/components/ui/select |
| <SelectValue> | 1× | | @/components/ui/select |
| <Button> | 3× | | Native HTML |
| <div> | 9× | | Native HTML |
| <h3> | 1× | | Native HTML |
| <Select> | 1× | | Native HTML |
| <Textarea> | 1× | | Native HTML |