How to Setup Your AI Frontend
UI Setup
Interacting with Your AI on the Frontend.
ShadCN Components Installation
npx shadcn@latest add input-group avatarAI User Interface Setup
"use client"
import { useEffect, useRef, useState } from 'react'
import { Button } from '../ui/button'
import { Send } from 'lucide-react'
import ChatContainer from './chat-container'
function AIAgent({ name, avatar }: { name?: string, avatar?: string }) {
const [isOpen, setIsOpen] = useState<boolean>(false)
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (isOpen) {
containerRef?.current?.classList.remove("hidden")
setTimeout(() => {
containerRef?.current?.classList.remove("opacity-0", "scale-0")
containerRef?.current?.classList.add("opacity-100", "scale-100")
}, 100)
} else {
containerRef?.current?.classList.remove("opacity-100", "scale-100")
containerRef?.current?.classList.add("opacity-0", "scale-0")
setTimeout(() => {
containerRef?.current?.classList.add("hidden")
}, 300)
}
})
return (
<div className='fixed sm:bottom-16 bottom-5 flex sm:justify-self-end justify-self-end sm:mr-10 z-20'>
<div className='flex flex-col gap-2 sm:items-end items-end'>
<div className="transition-all duration-300 sm:origin-bottom-right origin-bottom ease-in-out opacity-0 scale-0 hidden"
ref={containerRef} >
<ChatContainer isOpen={isOpen} setIsOpen={setIsOpen} name={name} avatar={avatar}/>
</div>
<Button variant="default"
className='rounded-full p-6 w-fit sm:mr-0 mr-5'
onClick={() => setIsOpen(!isOpen)}>
<Send className='size-5 sm:animate-bounce' />
<span className='font-bold sm:flex hidden'>Chat with {name}</span>
</Button>
</div>
</div>
)
}
export default AIAgent"use client"
import { Send, X } from 'lucide-react'
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupTextarea } from '../ui/input-group'
import React, { useEffect, useRef, useState } from 'react'
import { cn } from '@/lib/utils';
import { Avatar, AvatarImage } from '../ui/avatar';
import { UseAiStore } from '@/app/state/use-store-ai';
interface AiState {
isOpen?: boolean,
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>,
name?: string,
avatar?: string
}
function ChatContainer({ isOpen, setIsOpen, avatar, name }: AiState) {
const { generateResponse, isGenerating, messages } = UseAiStore();
const messageRef = useRef<HTMLDivElement>(null);
const [prompt, setPrompt] = useState<string>("");
const handleSend = () => {
if (!prompt.trim()) return;
setPrompt("")
generateResponse(prompt)
}
const handleKeyEnter = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter') {
e.preventDefault()
}
if (prompt.trim() && e.key === 'Enter' && !e.shiftKey) {
setPrompt("")
e.preventDefault()
generateResponse(prompt)
}
};
useEffect(() => {
if (messageRef.current) {
messageRef.current.scrollIntoView({ behavior: "smooth" })
}
});
return (
<div className='sm:relative bg-background border
rounded-md sm:min-h-130 sm:min-w-100 max-w-100 h-screen flex flex-col w-screen max-h-130'>
<div className='flex flex-col h-full'>
<div className='flex items-center justify-between px-4 py-2 border-b-2'>
<div className='flex flex-row items-center gap-2'>
<Avatar className='h-12 w-12'>
<AvatarImage className='object-cover' src={avatar} />
</Avatar>
<div className='flex flex-col gap-1'>
<span className='font-bold'>{name}</span>
<div className='flex flex-row items-center gap-1'>
<div className='w-2 h-2 bg-green-500 rounded-full'></div>
<span className='text-xs'>Active Now</span>
</div>
</div>
</div>
<button
onClick={() => setIsOpen(!isOpen)}><X /></button>
</div>
<div className='flex-1 overflow-y-auto space-y-4 p-2 scrollable-div'>
<div className='flex flex-col items-start gap-2'>
<div className='bg-black dark:bg-white rounded-lg p-4 w-fit max-w-60'>
<span className='text-white dark:text-black text-sm'>Hello! I’m {name}, your friendly AI chatbot assistant. I’m here to answer questions, provide guidance, and chat with you anytime. Ask me anything, and let’s explore and learn together!</span>
</div>
</div>
{messages.map((msg, index) => (
<div key={index}
className={cn('flex flex-col gap-2', msg.role === "user"
? "items-end"
: "items-start")}>
<div className={cn("p-4 rounded-md max-w-60",
msg.role === "user"
? "bg-blue-500 text-white"
: "bg-black dark:bg-white text-white dark:text-black")}>
<span>
{msg.content}
</span>
</div>
</div>
))}
<div className='flex flex-col items-start'>
<div className={cn(!isGenerating && 'hidden')}>
<div className='bg-black dark:bg-white rounded-lg p-4 w-fit max-w-60'>
<div className='bg-black dark:bg-white flex flex-row gap-1'>
<div className='w-1 h-1 bg-white dark:bg-black animate-bounce rounded-full'></div>
<div className='w-1 h-1 bg-white dark:bg-black animate-bounce delay-150 rounded-full'></div>
<div className='w-1 h-1 bg-white dark:bg-black animate-bounce delay-300 rounded-full'></div>
</div>
</div>
</div>
</div>
<div className={cn(isOpen && "hidden")} ref={messageRef}></div>
</div>
<InputGroup className='rounded-t-none'>
<InputGroupTextarea
id="block-end-textarea"
placeholder="Aa"
onChange={(e) => setPrompt(e.target.value)}
value={prompt}
onKeyDown={handleKeyEnter}
maxLength={300}
/>
<InputGroupAddon align="block-end">
<InputGroupText>{prompt.length}/300</InputGroupText>
<InputGroupButton variant="default" size="sm"
className="ml-auto"
onClick={handleSend}
disabled={isGenerating || !prompt.trim()}>
<Send />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div >
</div >
)
}
export default ChatContainerimport AIAgent from "@/components/ai/ai-agent";
export default function Home() {
return (
<div>
<AIAgent
name="Krave"
avatar="https://github.com/shadcn.png" />
</div>
);
}