| "use client"; |
|
|
| import { useState, useRef, useEffect } from "react"; |
| import { |
| Card, |
| CardContent, |
| CardDescription, |
| CardFooter, |
| CardHeader, |
| CardTitle |
| } from "@hanzo/ui/primitives/card"; |
| import { Button } from "@hanzo/ui/primitives/button"; |
| import { Input } from "@hanzo/ui/primitives/input"; |
| import { ScrollArea } from "@hanzo/ui/primitives/scroll-area"; |
| import { Avatar, AvatarFallback, AvatarImage } from "@hanzo/ui/primitives/avatar"; |
| import { Badge } from "@hanzo/ui/primitives/badge"; |
| import { Textarea } from "@hanzo/ui/primitives/textarea"; |
| import { |
| Send, |
| Bot, |
| User, |
| Copy, |
| Download, |
| RefreshCw, |
| Loader2, |
| Sparkles, |
| Settings |
| } from "lucide-react"; |
| import { cn } from "@hanzo/ui/util"; |
|
|
| interface Message { |
| id: string; |
| role: "user" | "assistant"; |
| content: string; |
| timestamp: Date; |
| isStreaming?: boolean; |
| } |
|
|
| export default function AIChatInterface() { |
| const [messages, setMessages] = useState<Message[]>([ |
| { |
| id: "1", |
| role: "assistant", |
| content: "Hello! I'm your AI assistant powered by @hanzo/ui. How can I help you today?", |
| timestamp: new Date(), |
| } |
| ]); |
| const [input, setInput] = useState(""); |
| const [isLoading, setIsLoading] = useState(false); |
| const scrollRef = useRef<HTMLDivElement>(null); |
|
|
| useEffect(() => { |
| scrollRef.current?.scrollIntoView({ behavior: "smooth" }); |
| }, [messages]); |
|
|
| const handleSend = async () => { |
| if (!input.trim() || isLoading) return; |
|
|
| const userMessage: Message = { |
| id: Date.now().toString(), |
| role: "user", |
| content: input, |
| timestamp: new Date() |
| }; |
|
|
| setMessages(prev => [...prev, userMessage]); |
| setInput(""); |
| setIsLoading(true); |
|
|
| |
| const assistantMessage: Message = { |
| id: (Date.now() + 1).toString(), |
| role: "assistant", |
| content: "", |
| timestamp: new Date(), |
| isStreaming: true |
| }; |
|
|
| setMessages(prev => [...prev, assistantMessage]); |
|
|
| |
| const response = `I understand you're asking about "${input}". Let me help you with that. |
| |
| This response demonstrates the streaming capability of our chat interface built with @hanzo/ui components. The interface features: |
| |
| • Real-time message streaming |
| • Markdown support for rich text formatting |
| • Code syntax highlighting |
| • Responsive design that works on all devices |
| • Dark/light theme support |
| |
| The UI is built entirely with @hanzo/ui primitives like Card, Button, ScrollArea, and Avatar components, ensuring consistency with the Hanzo design system.`; |
|
|
| let currentText = ""; |
| const words = response.split(" "); |
|
|
| for (let i = 0; i < words.length; i++) { |
| currentText += (i > 0 ? " " : "") + words[i]; |
| await new Promise(resolve => setTimeout(resolve, 30)); |
|
|
| setMessages(prev => prev.map(msg => |
| msg.id === assistantMessage.id |
| ? { ...msg, content: currentText } |
| : msg |
| )); |
| } |
|
|
| setMessages(prev => prev.map(msg => |
| msg.id === assistantMessage.id |
| ? { ...msg, isStreaming: false } |
| : msg |
| )); |
|
|
| setIsLoading(false); |
| }; |
|
|
| return ( |
| <div className="min-h-screen bg-background p-4"> |
| <div className="max-w-4xl mx-auto"> |
| <Card className="h-[80vh] flex flex-col"> |
| <CardHeader className="border-b"> |
| <div className="flex items-center justify-between"> |
| <div className="flex items-center gap-2"> |
| <div className="w-10 h-10 rounded-lg bg-gradient-to-br from-violet-500 to-purple-700 flex items-center justify-center"> |
| <Sparkles className="w-6 h-6 text-white" /> |
| </div> |
| <div> |
| <CardTitle>AI Chat Interface</CardTitle> |
| <CardDescription>Powered by @hanzo/ui components</CardDescription> |
| </div> |
| </div> |
| <div className="flex items-center gap-2"> |
| <Badge variant="outline">GPT-4</Badge> |
| <Button variant="ghost" size="icon"> |
| <Settings className="w-4 h-4" /> |
| </Button> |
| </div> |
| </div> |
| </CardHeader> |
| |
| <ScrollArea className="flex-1 p-4"> |
| <div className="space-y-4"> |
| {messages.map((message) => ( |
| <div |
| key={message.id} |
| className={cn( |
| "flex gap-3", |
| message.role === "user" ? "justify-end" : "justify-start" |
| )} |
| > |
| {message.role === "assistant" && ( |
| <Avatar> |
| <AvatarFallback className="bg-violet-100"> |
| <Bot className="w-5 h-5 text-violet-600" /> |
| </AvatarFallback> |
| </Avatar> |
| )} |
| |
| <Card className={cn( |
| "max-w-[70%]", |
| message.role === "user" |
| ? "bg-gradient-to-r from-violet-500 to-purple-600 text-white" |
| : "bg-muted" |
| )}> |
| <CardContent className="p-3"> |
| <p className="whitespace-pre-wrap"> |
| {message.content} |
| {message.isStreaming && ( |
| <span className="inline-block w-2 h-4 ml-1 bg-violet-500 animate-pulse" /> |
| )} |
| </p> |
| <div className="flex items-center gap-2 mt-2"> |
| <span className="text-xs opacity-60"> |
| {message.timestamp.toLocaleTimeString()} |
| </span> |
| {message.role === "assistant" && !message.isStreaming && ( |
| <div className="flex gap-1"> |
| <Button variant="ghost" size="icon" className="h-6 w-6"> |
| <Copy className="w-3 h-3" /> |
| </Button> |
| <Button variant="ghost" size="icon" className="h-6 w-6"> |
| <RefreshCw className="w-3 h-3" /> |
| </Button> |
| </div> |
| )} |
| </div> |
| </CardContent> |
| </Card> |
| |
| {message.role === "user" && ( |
| <Avatar> |
| <AvatarFallback> |
| <User className="w-5 h-5" /> |
| </AvatarFallback> |
| </Avatar> |
| )} |
| </div> |
| ))} |
| <div ref={scrollRef} /> |
| </div> |
| </ScrollArea> |
| |
| <CardFooter className="border-t p-4"> |
| <div className="flex gap-2 w-full"> |
| <Textarea |
| placeholder="Type your message..." |
| value={input} |
| onChange={(e) => setInput(e.target.value)} |
| onKeyDown={(e) => { |
| if (e.key === "Enter" && !e.shiftKey) { |
| e.preventDefault(); |
| handleSend(); |
| } |
| }} |
| className="min-h-[50px] max-h-[150px]" |
| /> |
| <Button |
| onClick={handleSend} |
| disabled={isLoading || !input.trim()} |
| className="px-4" |
| > |
| {isLoading ? ( |
| <Loader2 className="w-4 h-4 animate-spin" /> |
| ) : ( |
| <Send className="w-4 h-4" /> |
| )} |
| </Button> |
| </div> |
| </CardFooter> |
| </Card> |
| </div> |
| </div> |
| ); |
| } |