Krave
How to Setup Your AI Frontend

UI Setup

Interacting with Your AI on the Frontend.

ShadCN Components Installation

Terminal
npx shadcn@latest add input-group avatar

AI User Interface Setup

src/components/ai/ai-agent.tsx
"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
src/components/ai/chat-container.tsx
"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 ChatContainer
src/app/page.tsx
import AIAgent from "@/components/ai/ai-agent";

export default function Home() {
  return (
    <div>
      <AIAgent
        name="Krave"
        avatar="https://github.com/shadcn.png" />
    </div>
  );
}

On this page