setcobi(false)}>
+ {/* Dialog for URL and Model Name */}
+ {showDialog && (
+
+
{
+ if (e.key === 'Enter') {
+ handleDialogSubmit();
+ }
+ }}>
+
LM Studio/Ollama Configuration
+
Please provide the URL and model name to proceed.
+
+
+ setTempUrl(e.target.value)}
+ placeholder="Enter URL"
+ className="w-full"
+ autoFocus
+ />
+
+
+
+ setTempModelName(e.target.value)}
+ placeholder="Enter Model Name"
+ className="w-full"
+ />
+
+
+
+
+
+
+
+ )}
+
+ {/* Header */}
+ {/*
+
{chat.title || "New Chat"}
+ */}
+
+ {/* Message Area */}
+ {/*
w-9 */}
+ {/* Make chat history grow and handle overflow */}
+
+
+ {/* mx-auto flex w-full max-w-3xl flex-col space-y-12 px-4 pb-10 pt-safe-offset-10 */}
+ {chat.messages.length === 0 ? (
+
+
Send a message to start the conversation
+
+ ) : (
+ chat.messages.map((message) => (
+
handleCopyMessage(message.content)}
+ onBranch={() => handleBranchFromMessage(message.id)}
+ setmts={setmts}
+ setdsm={setdsm}
+ />
+ ))
+ )}
+
+
+
+ {/*
*/}
+ {/* */}
+
+ {/* Error Display */}
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Input Area */}
+
+
+ {/*
*/}
+ {/* Context Usage Bar */}
+ {/*
+
+ Context Usage
+ {contextUsage}%
+
+
+
*/}
+
+ {/* Text Input & Send Button */}
+
+
+
+
+ {ollamastate==0?(
):null}
+ {ollamastate===3 && (
+
+ setSelectedFilePath(e.target.value)}
+ placeholder="Enter file path or choose file"
+ className="flex-grow"
+ />
+
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/src/components/batu/components/chatui.tsx b/src/components/batu/components/chatui.tsx
new file mode 100644
index 0000000..83908d8
--- /dev/null
+++ b/src/components/batu/components/chatui.tsx
@@ -0,0 +1,443 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import ChatInterface from "../components/chat-interface"
+import ChatHistory from "../components/chat-history"
+import ApiKeyInput from "../components/api-key-input"
+import LMStudioURL from "../components/lmstudio-url"
+import LMStudioModelName from "../components/localmodelname"
+import FileGPTUrl from "../components/filegpt-url"
+import type { Chat, BranchPoint } from "../lib/types"
+import { Button } from "../components/ui/button"
+import { PlusIcon, MenuIcon, XIcon, Download, Bot } from "lucide-react"
+import ModelSelectionDialog from "../components/model-selection-dialog"
+import ExportDialog from "../components/export-dialog"
+import { Toaster } from "../components/ui/toaster"
+import { cn } from "../lib/utils"
+import DarkButton from './dark-button'
+interface FileItem {
+ name: string;
+ path: string;
+ is_dir: boolean;
+ size: number;
+ rawfs: number;
+ lmdate: number;
+ timestamp: number;
+ foldercon: number;
+ ftype: string;
+ parent: string;
+ }
+interface gptargs{
+ message?:FileItem,
+ fgptendpoint?:string,
+ setasollama:boolean
+ // localorremote:boolean
+}
+export default function ChatUI({message,fgptendpoint="localhost",setasollama=false}:gptargs) {
+ const [apiKey, setApiKey] = useState
("")
+ const [lmurl, setlmurl] = useState("")
+ const [model_name, set_model_name] = useState("")
+ const [filegpturl, setFilegpturl] = useState("")
+ const [selectedModel, setSelectedModel] = useState("")
+ const [selectedModelInfo, setSelectedModelInfo] = useState(null)
+ const [chats, setChats] = useState([])
+ const [currentChatId, setCurrentChatId] = useState("")
+ const [sidebarVisible, setSidebarVisible] = useState(true)
+ const [ollamastate, setollamastate] = useState(0)
+ const [isModelDialogOpen, setIsModelDialogOpen] = useState(false)
+ const [isExportDialogOpen, setIsExportDialogOpen] = useState(false)
+ const [allModels, setAllModels] = useState([])
+
+ useEffect(()=>{
+ setCollapsed(true)
+ },[currentChatId])
+ // Load API key and chats from localStorage on initial render
+ useEffect(() => {
+ const storedlmurl = localStorage.getItem("lmstudio_url")
+ if (ollamastate!==0 && storedlmurl) {
+ setlmurl(storedlmurl)
+ }
+
+ const stored_lm_model_name = localStorage.getItem("lmstudio_model_name")
+ if (ollamastate!==0 && storedlmurl && stored_lm_model_name) {
+ set_model_name(stored_lm_model_name)
+ setSelectedModel(model_name)
+ }
+
+ const storedFilegpturl = localStorage.getItem("filegpt_url")
+ if (ollamastate === 3 && storedFilegpturl) {
+ setFilegpturl(storedFilegpturl)
+ }
+ }, [ollamastate]);
+ console.log(lmurl)
+ console.log(model_name)
+ useEffect(() => {
+ const storedApiKey = localStorage.getItem("openrouter_api_key")
+ if (storedApiKey) {
+ setApiKey(storedApiKey)
+ }
+
+
+
+ const storedChats = localStorage.getItem("chat_history")
+ if (storedChats) {
+ try {
+ const parsedChats = JSON.parse(storedChats)
+ setChats(parsedChats)
+
+ // Set current chat to the most recent one if it exists
+ if (parsedChats.length > 0) {
+ setCurrentChatId(parsedChats[0].id)
+ } else {
+ createNewChat()
+ }
+ } catch (error) {
+ console.error("Failed to parse stored chats:", error)
+ createNewChat()
+ }
+ } else {
+ createNewChat()
+ }
+ }, [])
+
+ // Save chats to localStorage whenever they change
+ useEffect(() => {
+ if (chats.length > 0) {
+ localStorage.setItem("chat_history", JSON.stringify(chats))
+ }
+ }, [chats])
+
+ // Fetch models when API key is set
+ useEffect(() => {
+ if (!apiKey) return
+
+ const fetchModels = async () => {
+ try {
+ const response = await fetch("https://openrouter.ai/api/v1/models", {
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ },
+ })
+
+ if (!response.ok) {
+ throw new Error("Failed to fetch models")
+ }
+
+ const data = await response.json()
+ setAllModels(data.data)
+
+ // // Filter for free models (where pricing is 0)
+ // const freeModels = data.data.filter((model: any) => {
+ // return Number.parseFloat(model.pricing?.prompt) <= 0 && Number.parseFloat(model.pricing?.completion) <= 0
+ // })
+
+ // Set the first model as selected if none is selected
+ // if (freeModels.length > 0 && !selectedModel) {
+ // setSelectedModel(freeModels[0].id)
+ // setSelectedModelInfo(freeModels[0])
+ // }
+ } catch (err) {
+ console.error("Error fetching models:", err)
+ }
+ }
+
+ fetchModels()
+ }, [apiKey])
+
+ const createNewChat = () => {
+ const newChatId = Date.now().toString()
+ const newChat: Chat = {
+ id: newChatId,
+ title: "New Chat",
+ messages: [],
+ createdAt: new Date().toISOString(),
+ lastModelUsed: selectedModel,
+ branchedFrom: null,
+ }
+
+ setChats((prevChats) => [newChat, ...prevChats])
+ setCurrentChatId(newChatId)
+ }
+
+ const updateChat = (updatedChat: Chat) => {
+ setChats((prevChats) => prevChats.map((chat) => (chat.id === updatedChat.id ? updatedChat : chat)))
+ }
+ const renameChat = (id: string, newTitle: string) => {
+ setChats(chats.map((chat) => (chat.id === id ? { ...chat, title: newTitle } : chat)))
+ }
+ const deleteChat = (chatId: string) => {
+ setChats((prevChats) => prevChats.filter((chat) => chat.id !== chatId))
+
+ if (currentChatId === chatId) {
+ if (chats.length > 1) {
+ // Set current chat to the next available one
+ const nextChat = chats.find((chat) => chat.id !== chatId)
+ if (nextChat) {
+ setCurrentChatId(nextChat.id)
+ } else {
+ createNewChat()
+ }
+ } else {
+ createNewChat()
+ }
+ }
+ }
+
+ const handleBranchConversation = (branchPoint: BranchPoint) => {
+ const newChatId = Date.now().toString()
+ const originalChat = chats.find((chat) => chat.id === branchPoint.originalChatId)
+
+ if (!originalChat) return
+
+ // Create a title based on the last user message in the branch
+ const lastUserMessage = [...branchPoint.messages].reverse().find((msg) => msg.role === "user")
+ const branchTitle = lastUserMessage ? `Branch: ${lastUserMessage.content.slice(0, 20)}...` : "New Branch"
+
+ const newChat: Chat = {
+ id: newChatId,
+ title: branchTitle,
+ messages: branchPoint.messages,
+ createdAt: new Date().toISOString(),
+ lastModelUsed: selectedModel,
+ branchedFrom: {
+ chatId: branchPoint.originalChatId,
+ messageId: branchPoint.branchedFromMessageId,
+ timestamp: branchPoint.timestamp,
+ },
+ }
+
+ setChats((prevChats) => [newChat, ...prevChats])
+ setCurrentChatId(newChatId)
+ }
+
+ const handleSelectModel = (modelId: string) => {
+ setSelectedModel(modelId)
+ const modelInfo = allModels.find((model: any) => model.id === modelId)
+ setSelectedModelInfo(modelInfo || null)
+ setIsModelDialogOpen(false)
+ }
+const [collapsed, setCollapsed] = useState(true);
+
+ const toggleMenu = () => {
+ setCollapsed(prev => !prev);
+ };
+
+ const currentChat = chats.find((chat) => chat.id === currentChatId)
+ // const [viewportHeight, setViewportHeight] = useState((typeof window === 'undefined')? "h-full" :window.innerHeight);
+
+ // useEffect(() => {
+ // if (typeof window === 'undefined') return;
+ // const handleResize = () => {
+ // setViewportHeight(window.visualViewport?.height || window.innerHeight);
+ // };
+
+ // window.visualViewport?.addEventListener('resize', handleResize);
+ // window.addEventListener('resize', handleResize);
+
+ // // Initial call
+ // handleResize();
+
+ // return () => {
+ // window.visualViewport?.removeEventListener('resize', handleResize);
+ // window.removeEventListener('resize', handleResize);
+ // };
+ // }, []);
+
+ const debounce = (func: (...args: any[]) => void, wait: number): (...args: any[]) => void => {
+ let timeout: NodeJS.Timeout | undefined;
+ return function executedFunction(...args: any[]): void {
+ const later = (): void => {
+ clearTimeout(timeout);
+ func(...args);
+ };
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ };
+ };
+
+ useEffect(() => {
+ const setViewportHeight = () => {
+ document.documentElement.style.setProperty('--100vh', `${window.innerHeight}px`);
+ };
+ setViewportHeight();
+ const debouncedSetViewportHeight = debounce(setViewportHeight, 100);
+ window.addEventListener('resize', debouncedSetViewportHeight);
+ return () => {
+ window.removeEventListener('resize', debouncedSetViewportHeight);
+ };
+ }, []);
+
+ useEffect(() => {
+ if (typeof window !== 'undefined' && !window.isSecureContext) {
+ // In a real Next.js app, you might use next/router here
+ // For example: router.replace(window.location.href.replace('http:', 'https:'));
+ if (window.location.protocol !== 'https:') {
+ console.warn("Attempting to redirect to HTTPS (simulated for component context)");
+ // window.location.protocol = "https:"; // This would cause a full page reload
+ }
+ }
+ }, []);
+
+ return (
+
+ {(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {ollamastate==0?(
):null}
+ {ollamastate!==0 ? (
+ <>
+
+
+
+ {ollamastate !== 3 && (
+
+
+
+ )}
+ {ollamastate === 3 && (
+
+
+
+ )}
+ >
+ ) : <>>}
+
+
+
+
+
+
+
+
+
+ )}
+ {/*
*/}
+
+
+ {/* Main content */}
+
{setCollapsed(true)}} >
+ {/*
+ {!sidebarVisible?():null}
+
*/}
+
+ {currentChat && (
+
+ )}
+
+
+ {/* Model Selection Dialog */}
+
setIsModelDialogOpen(false)}
+ models={allModels}
+ selectedModel={selectedModel}
+ onSelectModel={handleSelectModel}
+ apiKey={apiKey}
+ />
+
+ {/* Export Dialog */}
+ setIsExportDialogOpen(false)} chat={currentChat} />
+
+
+
+ )
+}
+
+// Helper function to get a consistent color based on model name
+function getModelColor(modelId: string): string {
+ const colors = [
+ "purple-500",
+ "pink-500",
+ "rose-500",
+ "red-500",
+ "orange-500",
+ "amber-500",
+ "yellow-500",
+ "lime-500",
+ "green-500",
+ "emerald-500",
+ "teal-500",
+ "cyan-500",
+ "sky-500",
+ "blue-500",
+ "indigo-500",
+ "violet-500",
+ ]
+
+ // Simple hash function to get consistent color
+ let hash = 0
+ for (let i = 0; i < modelId.length; i++) {
+ hash = modelId.charCodeAt(i) + ((hash << 5) - hash)
+ }
+
+ const index = Math.abs(hash) % colors.length
+ return colors[index]
+}
+
+// Helper function to get a display name from model ID
+function getModelDisplayName(modelId: string): string {
+ // Extract the model name from the provider/model format
+ const parts = modelId.split("/")
+ return parts.length > 1 ? parts[1] : modelId
+}
diff --git a/src/components/batu/components/codeblock.tsx b/src/components/batu/components/codeblock.tsx
new file mode 100644
index 0000000..9ed9615
--- /dev/null
+++ b/src/components/batu/components/codeblock.tsx
@@ -0,0 +1,82 @@
+'use client';
+
+import { cn } from '../lib/utils';
+import React, { useEffect, useState } from 'react';
+import { codeToHtml } from 'shiki';
+import { useTheme } from 'next-themes';
+
+export type CodeBlockProps = {
+ children?: React.ReactNode;
+ className?: string;
+} & React.HTMLProps;
+
+function CodeBlock({ children, className, ...props }: CodeBlockProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export type CodeBlockCodeProps = {
+ code: string;
+ language?: string;
+ theme?: string;
+ className?: string;
+} & React.HTMLProps;
+
+function CodeBlockCode({
+ code,
+ language = 'tsx',
+ theme: propTheme,
+ className,
+ ...props
+}: CodeBlockCodeProps) {
+ const { resolvedTheme } = useTheme();
+ const [highlightedHtml, setHighlightedHtml] = useState(null);
+
+ // Use github-dark when in dark mode, github-light when in light mode
+ const theme =
+ propTheme || (resolvedTheme === 'dark' ? 'github-dark' : 'github-light');
+
+ useEffect(() => {
+ async function highlight() {
+ const html = await codeToHtml(code, { lang: language, theme });
+ setHighlightedHtml(html);
+ }
+ highlight();
+ }, [code, language, theme]);
+
+ const classNames = cn('', className);
+
+ // SSR fallback: render plain code if not hydrated yet
+ return highlightedHtml ? (
+
+ ) : (
+
+ );
+}
+
+export type CodeBlockGroupProps = React.HTMLAttributes;
+
+function CodeBlockGroup({
+ children,
+ className,
+ ...props
+}: CodeBlockGroupProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export { CodeBlockGroup, CodeBlockCode, CodeBlock };
\ No newline at end of file
diff --git a/src/components/batu/components/dark-button.tsx b/src/components/batu/components/dark-button.tsx
new file mode 100644
index 0000000..0d5521c
--- /dev/null
+++ b/src/components/batu/components/dark-button.tsx
@@ -0,0 +1,42 @@
+'use client';
+import { useState,useEffect, useContext } from 'react';
+import React from "react";
+import { useTheme } from 'next-themes';
+import { Moon, Sun } from 'lucide-react';
+
+export default function DarkButton() {
+ const { setTheme, theme } = useTheme();
+ useEffect(() => {
+ // dark?setTheme('light'):setTheme('dark');
+ const darkIcon = document.getElementById("theme-toggle-dark-icon")!;
+ const lightIcon = document.getElementById("theme-toggle-light-icon")!;
+ if (theme === 'dark') {
+ darkIcon.style.display = "block";
+ lightIcon.style.display = "none";
+ } else {
+ darkIcon.style.display = "none";
+ lightIcon.style.display = "block";
+ }
+ }, [theme]);
+ return (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/src/components/batu/components/export-dialog.tsx b/src/components/batu/components/export-dialog.tsx
new file mode 100644
index 0000000..2ab7299
--- /dev/null
+++ b/src/components/batu/components/export-dialog.tsx
@@ -0,0 +1,152 @@
+"use client"
+
+import { useState } from "react"
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../components/ui/dialog"
+import { Button } from "../components/ui/button"
+import { RadioGroup, RadioGroupItem } from "../components/ui/radio-group"
+import { Label } from "../components/ui/label"
+import { Download } from "lucide-react"
+import type { Chat } from "../lib/types"
+
+interface ExportDialogProps {
+ isOpen: boolean
+ onClose: () => void
+ chat: Chat | undefined
+}
+
+type ExportFormat = "pdf" | "txt" | "json"
+
+export default function ExportDialog({ isOpen, onClose, chat }: ExportDialogProps) {
+ const [exportFormat, setExportFormat] = useState("txt")
+
+ const handleExport = () => {
+ if (!chat) return
+
+ switch (exportFormat) {
+ case "txt":
+ exportAsTxt(chat)
+ break
+ case "json":
+ exportAsJson(chat)
+ break
+ case "pdf":
+ exportAsPdf(chat)
+ break
+ }
+
+ onClose()
+ }
+
+ const exportAsTxt = (chat: Chat) => {
+ let content = `# ${chat.title || "Chat Export"}\n`
+ content += `# Date: ${new Date(chat.createdAt).toLocaleString()}\n\n`
+
+ chat.messages.forEach((message) => {
+ content += `${message.role.toUpperCase()} [${new Date(message.timestamp).toLocaleString()}]:\n`
+ content += `${message.content}\n\n`
+ })
+
+ downloadFile(content, `chat-export-${Date.now()}.txt`, "text/plain")
+ }
+
+ const exportAsJson = (chat: Chat) => {
+ const content = JSON.stringify(chat, null, 2)
+ downloadFile(content, `chat-export-${Date.now()}.json`, "application/json")
+ }
+
+ const exportAsPdf = (chat: Chat) => {
+ // This is a simplified version that creates a basic PDF using browser print
+ // For a production app, you'd want to use a library like jsPDF or pdfmake
+ const content = document.createElement("div")
+ content.innerHTML = `
+ ${chat.title || "Chat Export"}
+ Date: ${new Date(chat.createdAt).toLocaleString()}
+
+ ${chat.messages
+ .map(
+ (message) => `
+
+
${message.role.toUpperCase()} [${new Date(message.timestamp).toLocaleString()}]:
+
${message.content}
+
+ `,
+ )
+ .join("")}
+ `
+
+ const printWindow = window.open("", "_blank")
+ if (printWindow) {
+ printWindow.document.write(`
+
+
+ ${chat.title || "Chat Export"}
+
+
+
+ ${content.innerHTML}
+
+
+
+ `)
+ printWindow.document.close()
+ }
+ }
+
+ const downloadFile = (content: string, fileName: string, contentType: string) => {
+ const blob = new Blob([content], { type: contentType })
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement("a")
+ a.href = url
+ a.download = fileName
+ document.body.appendChild(a)
+ a.click()
+ document.body.removeChild(a)
+ URL.revokeObjectURL(url)
+ }
+
+ return (
+
+ )
+}
diff --git a/src/components/batu/components/filegpt-url.tsx b/src/components/batu/components/filegpt-url.tsx
new file mode 100644
index 0000000..c4e4dc6
--- /dev/null
+++ b/src/components/batu/components/filegpt-url.tsx
@@ -0,0 +1,46 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import { Input } from "./ui/input"
+import { Label } from "./ui/label"
+import { Button } from "./ui/button"
+import { SaveIcon } from "lucide-react"
+
+interface FileGPTUrlInputProps {
+ filegpturl: string
+ setFilegpturl: (key: string) => void
+}
+
+export default function FileGPTUrl({ filegpturl, setFilegpturl }: FileGPTUrlInputProps) {
+ const [inputValue, setInputValue] = useState(filegpturl)
+
+ useEffect(() => {
+ setInputValue(filegpturl)
+ }, [filegpturl])
+
+ const handleSave = () => {
+ setFilegpturl(inputValue)
+ localStorage.setItem("filegpt_url", inputValue)
+ }
+
+ return (
+
+
+
+
+ setInputValue(e.target.value)}
+ placeholder="FileGPT Endpoint (e.g., http://localhost:8694)"
+ className="pr-10"
+ />
+
+
+
+
+ )
+}
diff --git a/src/components/batu/components/lmstudio-url.tsx b/src/components/batu/components/lmstudio-url.tsx
new file mode 100644
index 0000000..ad42752
--- /dev/null
+++ b/src/components/batu/components/lmstudio-url.tsx
@@ -0,0 +1,46 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import { Input } from "../components/ui/input"
+import { Label } from "../components/ui/label"
+import { Button } from "../components/ui/button"
+import { EyeIcon, EyeOffIcon, SaveIcon } from "lucide-react"
+
+interface LMUrlInputProps {
+ lmurl: string
+ setlmurl: (key: string) => void
+}
+
+export default function LMStudioURL({ lmurl, setlmurl }: LMUrlInputProps) {
+ // const [showKey, setShowKey] = useState(false)
+ const [inputValue, setInputValue] = useState(lmurl)
+ useEffect(()=>{
+ setInputValue(lmurl)
+ },[lmurl])
+
+ const handleSave = () => {
+ setlmurl(inputValue)
+ localStorage.setItem("lmstudio_url", inputValue)
+ }
+
+ return (
+
+
+
+
+ setInputValue(e.target.value)}
+ placeholder="LM Studio IP"
+ className="pr-10"
+ />
+
+
+
+
+ )
+}
diff --git a/src/components/batu/components/localmodelname.tsx b/src/components/batu/components/localmodelname.tsx
new file mode 100644
index 0000000..7d34fee
--- /dev/null
+++ b/src/components/batu/components/localmodelname.tsx
@@ -0,0 +1,45 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import { Input } from "../components/ui/input"
+import { Label } from "../components/ui/label"
+import { Button } from "../components/ui/button"
+import { EyeIcon, EyeOffIcon, SaveIcon } from "lucide-react"
+
+interface LMmodelnameProps {
+ model_name: string
+ set_model_name: (key: string) => void
+}
+
+export default function LMStudioModelName({ model_name, set_model_name }: LMmodelnameProps) {
+ // const [showKey, setShowKey] = useState(false)
+ const [inputValue, setInputValue] = useState(model_name)
+ useEffect(()=>{
+ setInputValue(model_name)
+ },[model_name])
+ const handleSave = () => {
+ set_model_name(inputValue)
+ localStorage.setItem("lmstudio_model_name", inputValue)
+ }
+
+ return (
+
+
+
+
+ setInputValue(e.target.value)}
+ placeholder="LM Studio Model name"
+ className="pr-10"
+ />
+
+
+
+
+ )
+}
diff --git a/src/components/batu/components/markdown-renderer.tsx b/src/components/batu/components/markdown-renderer.tsx
new file mode 100644
index 0000000..9217de6
--- /dev/null
+++ b/src/components/batu/components/markdown-renderer.tsx
@@ -0,0 +1,33 @@
+'use client';
+
+import React from 'react';
+import { ScrollArea } from '../components/ui/scroll-area';
+import { Markdown } from './markdown';
+import { cn } from '../lib/utils';
+
+interface MarkdownRendererProps {
+ content: string;
+ className?: string;
+}
+
+/**
+ * Renderer for Markdown content with scrollable container
+ */
+export function MarkdownRenderer({
+ content,
+ className
+}: MarkdownRendererProps) {
+ return (
+
+
+
+
+ {content}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/batu/components/markdown.tsx b/src/components/batu/components/markdown.tsx
new file mode 100644
index 0000000..ba84a69
--- /dev/null
+++ b/src/components/batu/components/markdown.tsx
@@ -0,0 +1,209 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { cn } from '../lib/utils';
+import { marked } from 'marked';
+import { memo, useId, useMemo } from 'react';
+import ReactMarkdown, { Components } from 'react-markdown';
+import remarkGfm from 'remark-gfm';
+import { CodeBlock, CodeBlockCode } from './codeblock';
+import React from 'react';
+
+export type MarkdownProps = {
+ children: string;
+ id?: string;
+ className?: string;
+ components?: Partial;
+};
+
+function parseMarkdownIntoBlocks(markdown: string): string[] {
+ const tokens = marked.lexer(markdown);
+ return tokens.map((token: any) => token.raw);
+}
+
+function extractLanguage(className?: string): string {
+ if (!className) return 'plaintext';
+ const match = className.match(/language-(\w+)/);
+ return match ? match[1] : 'plaintext';
+}
+
+const INITIAL_COMPONENTS: Partial = {
+ code: function CodeComponent({ className, children, ...props }: any) {
+ const isInline =
+ !props.node?.position?.start.line ||
+ props.node?.position?.start.line === props.node?.position?.end.line;
+
+ if (isInline) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ const language = extractLanguage(className);
+
+ return (
+
+
+
+ );
+ },
+ pre: function PreComponent({ children }: any) {
+ return <>{children}>;
+ },
+ ul: function UnorderedList({ children, ...props }: any) {
+ return (
+
+ );
+ },
+ ol: function OrderedList({ children, ...props }: any) {
+ return (
+
+ {children}
+
+ );
+ },
+ li: function ListItem({ children, ...props }: any) {
+ return (
+
+ {children}
+
+ );
+ },
+ h1: function H1({ children, ...props }: any) {
+ return (
+
+ {children}
+
+ );
+ },
+ h2: function H2({ children, ...props }: any) {
+ return (
+
+ {children}
+
+ );
+ },
+ h3: function H3({ children, ...props }: any) {
+ return (
+
+ {children}
+
+ );
+ },
+ blockquote: function Blockquote({ children, ...props }: any) {
+ return (
+
+ {children}
+
+ );
+ },
+ a: function Anchor({ children, href, ...props }: any) {
+ return (
+
+ {children}
+
+ );
+ },
+ table: function Table({ children, ...props }: any) {
+ return (
+
+ );
+ },
+ th: function TableHeader({ children, ...props }: any) {
+ return (
+
+ {children}
+ |
+ );
+ },
+ td: function TableCell({ children, ...props }: any) {
+ return (
+
+ {children}
+ |
+ );
+ },
+};
+
+const MemoizedMarkdownBlock = memo(
+ function MarkdownBlock({
+ content,
+ components = INITIAL_COMPONENTS,
+ }: {
+ content: string;
+ components?: Partial;
+ }) {
+ return (
+
+ {content}
+
+ );
+ },
+ function propsAreEqual(prevProps: any, nextProps: any) {
+ return prevProps.content === nextProps.content;
+ },
+);
+
+MemoizedMarkdownBlock.displayName = 'MemoizedMarkdownBlock';
+
+function MarkdownComponent({
+ children,
+ id,
+ className,
+ components = INITIAL_COMPONENTS,
+}: MarkdownProps) {
+ const generatedId = useId();
+ const blockId = id ?? generatedId;
+ const blocks = useMemo(() => parseMarkdownIntoBlocks(children), [children]);
+
+ return (
+
+ {blocks.map((block, index) => (
+
+ ))}
+
+ );
+}
+
+const Markdown = memo(MarkdownComponent);
+Markdown.displayName = 'Markdown';
+
+export { Markdown };
\ No newline at end of file
diff --git a/src/components/batu/components/message-item.tsx b/src/components/batu/components/message-item.tsx
new file mode 100644
index 0000000..95ce4a8
--- /dev/null
+++ b/src/components/batu/components/message-item.tsx
@@ -0,0 +1,162 @@
+"use client"
+
+import type { Message } from "../lib/types"
+import { UserIcon, BotIcon, CopyIcon, GitBranchIcon, RefreshCw } from "lucide-react"
+import { cn } from "../lib/utils"
+import { format } from "date-fns"
+// import ReactMarkdown from "react-markdown"
+import {Markdown} from "./markdown"
+// import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
+// import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"
+import { useEffect, useState } from "react"
+import { Button } from "../components/ui/button"
+
+interface MessageItemProps {
+ message: Message
+ isStreaming?: boolean
+ onCopy: () => void
+ onBranch: () => void
+ setdsm: any
+ setmts: any
+}
+
+export default function MessageItem({ message, isStreaming = false, onCopy, onBranch,setdsm,setmts }: MessageItemProps) {
+ const isUser = message.role === "user"
+ const [showCursor, setShowCursor] = useState(true)
+ const [isHovered, setIsHovered] = useState(false)
+
+ // Blinking cursor effect for streaming messages
+ useEffect(() => {
+ if (!isStreaming) return
+
+ const interval = setInterval(() => {
+ setShowCursor((prev) => !prev)
+ }, 500)
+
+ return () => clearInterval(interval)
+ }, [isStreaming])
+
+ // Custom renderer for code blocks
+ // const components = {
+ // code({ node, inline, className, children, ...props }: any) {
+ // const match = /language-(\w+)/.exec(className || "")
+ // return !inline && match ? (
+ //
+ // {String(children).replace(/\n$/, "")}
+ //
+ // ) : (
+ //
+ // {children}
+ //
+ // )
+ // },
+ // }
+ const Resend=()=>{
+ setdsm(true)
+ setmts(message.content)
+ }
+
+ return (
+ setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}>
+
+
+ {/*
+ {isUser ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
*/}
+
+
+
+
+ {/* {isUser ? "You" : "AI Assistant"} */}
+ {message.model && !isUser && (
+ ({getModelDisplayName(message.model)})
+ )}
+
+ {/*
+ {format(new Date(message.timestamp), "MMM d, h:mm a")}
+ */}
+
+
+
+ {message.content}
+ {isStreaming && showCursor && ▌}
+
+
+
+
+
+ {!isStreaming && (
+
+
+
+
+
+ )}
+
+
+ )
+}
+
+// Helper function to get a consistent color based on model name
+function getModelColor(modelId: string): string {
+ const colors = [
+ "purple-500",
+ "pink-500",
+ "rose-500",
+ "red-500",
+ "orange-500",
+ "amber-500",
+ "yellow-500",
+ "lime-500",
+ "green-500",
+ "emerald-500",
+ "teal-500",
+ "cyan-500",
+ "sky-500",
+ "blue-500",
+ "indigo-500",
+ "violet-500",
+ ]
+
+ // Simple hash function to get consistent color
+ let hash = 0
+ for (let i = 0; i < modelId.length; i++) {
+ hash = modelId.charCodeAt(i) + ((hash << 5) - hash)
+ }
+
+ const index = Math.abs(hash) % colors.length
+ return colors[index]
+}
+
+// Helper function to get a display name from model ID
+function getModelDisplayName(modelId: string): string {
+ // Extract the model name from the provider/model format
+ const parts = modelId.split("/")
+ return parts.length > 1 ? parts[1] : modelId
+}
diff --git a/src/components/batu/components/model-selection-dialog.tsx b/src/components/batu/components/model-selection-dialog.tsx
new file mode 100644
index 0000000..2fe0485
--- /dev/null
+++ b/src/components/batu/components/model-selection-dialog.tsx
@@ -0,0 +1,238 @@
+"use client"
+
+import { useState, useEffect } from "react"
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../components/ui/dialog"
+import { BotIcon, Loader2 } from "lucide-react"
+import { cn } from "../lib/utils"
+import { ScrollArea } from "../components/ui/scroll-area"
+import { Input } from "../components/ui/input"
+import { Badge } from "../components/ui/badge"
+import Marquee from "react-fast-marquee";
+interface ModelSelectionDialogProps {
+ isOpen: boolean
+ onClose: () => void
+ models: any[]
+ selectedModel: string
+ onSelectModel: (modelId: string) => void
+ apiKey: string
+}
+
+export default function ModelSelectionDialog({
+ isOpen,
+ onClose,
+ models,
+ selectedModel,
+ onSelectModel,
+ apiKey,
+}: ModelSelectionDialogProps) {
+ const [isLoading, setIsLoading] = useState(false)
+ const [filteredModels, setFilteredModels] = useState([])
+ const [searchQuery, setSearchQuery] = useState("")
+
+ // useEffect(() => {
+ // if (!apiKey || models.length > 0) return
+
+ // const fetchModels = async () => {
+ // setIsLoading(true)
+ // try {
+ // const response = await fetch("https://openrouter.ai/api/v1/models", {
+ // headers: {
+ // Authorization: `Bearer ${apiKey}`,
+ // },
+ // })
+
+ // if (!response.ok) {
+ // throw new Error("Failed to fetch models")
+ // }
+
+ // const data = await response.json()
+ // // Models are fetched in the parent component
+ // } catch (err) {
+ // console.error("Error fetching models:", err)
+ // } finally {
+ // setIsLoading(false)
+ // }
+ // }
+
+ // fetchModels()
+ // }, [apiKey, models.length])
+
+ // Filter models based on search query
+ useEffect(() => {
+ if (!models) {
+ setFilteredModels([])
+ return
+ }
+
+ if (!searchQuery) {
+ setFilteredModels(models)
+ return
+ }
+
+ const filtered = models.filter((model) => {
+ const modelName = model.id.toLowerCase()
+ const provider = model.id.split("/")[0].toLowerCase()
+ const modelId = model.id.split("/").pop().toLowerCase()
+ return (
+ modelName.includes(searchQuery.toLowerCase()) ||
+ provider.includes(searchQuery.toLowerCase()) ||
+ modelId.includes(searchQuery.toLowerCase())
+ )
+ })
+
+ setFilteredModels(filtered)
+ }, [models, searchQuery])
+
+ const HoverMarqueeItem = ({ text }: { text: string }) => {
+ const [isHovered, setIsHovered] = useState(false);
+
+ return (
+ setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ className="text-center w-full"
+ >
+
+
+ );
+ };
+
+ // Get a random color for the model icon
+ const getModelColor = (modelId: string): string => {
+ const colors = [
+ "bg-purple-500",
+ "bg-pink-500",
+ "bg-rose-500",
+ "bg-red-500",
+ "bg-orange-500",
+ "bg-amber-500",
+ "bg-yellow-500",
+ "bg-lime-500",
+ "bg-green-500",
+ "bg-emerald-500",
+ "bg-teal-500",
+ "bg-cyan-500",
+ "bg-sky-500",
+ "bg-blue-500",
+ "bg-indigo-500",
+ "bg-violet-500",
+ ]
+
+ // Simple hash function to get consistent color
+ let hash = 0
+ for (let i = 0; i < modelId.length; i++) {
+ hash = modelId.charCodeAt(i) + ((hash << 5) - hash)
+ }
+
+ const index = Math.abs(hash) % colors.length
+ return colors[index]
+ }
+ const [modelcount, setmodelcount] = useState(10)
+
+ // Format pricing to be more readable
+ // const formatPrice = (price: string) => {
+ // if (!price) return "N/A"
+ // const numPrice = Number.parseFloat(price)
+ // if (numPrice === 0) return "Free"
+ // return `$${numPrice.toFixed(7)}`
+ // }
+ // const [isHovered, setIsHovered] = useState(false);
+ return (
+
+ )
+}
diff --git a/src/components/batu/components/model-selector.tsx b/src/components/batu/components/model-selector.tsx
new file mode 100644
index 0000000..a7989a9
--- /dev/null
+++ b/src/components/batu/components/model-selector.tsx
@@ -0,0 +1,98 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../components/ui/select"
+import { Label } from "../components/ui/label"
+import { Loader2 } from "lucide-react"
+// import type { Model } from "../lib/types"
+
+interface ModelSelectorProps {
+ apiKey: string
+ selectedModel: string
+ setSelectedModel: (model: string) => void
+}
+
+export default function ModelSelector({ apiKey, selectedModel, setSelectedModel }: ModelSelectorProps) {
+ const [models, setModels] = useState([])
+ const [isLoading, setIsLoading] = useState(false)
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ if (!apiKey) return
+
+ const fetchModels = async () => {
+ setIsLoading(true)
+ setError(null)
+
+ try {
+ const response = await fetch("https://openrouter.ai/api/v1/models", {
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ },
+ })
+
+ if (!response.ok) {
+ throw new Error("Failed to fetch models")
+ }
+
+ const data = await response.json()
+
+ // Filter for free models (where pricing is 0)
+ const freeModels = data.data.filter((model: any) => {
+ return Number.parseFloat(model.pricing?.prompt) <= 0 && Number.parseFloat(model.pricing?.completion) <= 0
+ })
+
+ setModels(
+ freeModels.map((model: any) => ({
+ id: model.id,
+ name: model.name || model.id.split("/").pop(),
+ provider: model.id.split("/")[0],
+ })),
+ )
+
+ // Set the first model as selected if none is selected
+ if (freeModels.length > 0 && !selectedModel) {
+ setSelectedModel(freeModels[0].id)
+ }
+ } catch (err) {
+ console.error("Error fetching models:", err)
+ setError(err instanceof Error ? err.message : "Failed to fetch models")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ fetchModels()
+ }, [apiKey, selectedModel, setSelectedModel])
+
+ return (
+
+
+
+
+ )
+}
diff --git a/src/components/batu/components/theme-provider.tsx b/src/components/batu/components/theme-provider.tsx
new file mode 100644
index 0000000..55c2f6e
--- /dev/null
+++ b/src/components/batu/components/theme-provider.tsx
@@ -0,0 +1,11 @@
+'use client'
+
+import * as React from 'react'
+import {
+ ThemeProvider as NextThemesProvider,
+ type ThemeProviderProps,
+} from 'next-themes'
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return {children}
+}
diff --git a/src/components/batu/components/ui/accordion.tsx b/src/components/batu/components/ui/accordion.tsx
new file mode 100644
index 0000000..1fc96f6
--- /dev/null
+++ b/src/components/batu/components/ui/accordion.tsx
@@ -0,0 +1,58 @@
+"use client"
+
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDown } from "lucide-react"
+
+import { cn } from "../../lib/utils"
+
+const Accordion = AccordionPrimitive.Root
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AccordionItem.displayName = "AccordionItem"
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+))
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+
+AccordionContent.displayName = AccordionPrimitive.Content.displayName
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/src/components/batu/components/ui/alert-dialog.tsx b/src/components/batu/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..58c23d1
--- /dev/null
+++ b/src/components/batu/components/ui/alert-dialog.tsx
@@ -0,0 +1,141 @@
+"use client"
+
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "../../lib/utils"
+import { buttonVariants } from "../../components/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/src/components/batu/components/ui/alert.tsx b/src/components/batu/components/ui/alert.tsx
new file mode 100644
index 0000000..e9286a3
--- /dev/null
+++ b/src/components/batu/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "../../lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/src/components/batu/components/ui/aspect-ratio.tsx b/src/components/batu/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..d6a5226
--- /dev/null
+++ b/src/components/batu/components/ui/aspect-ratio.tsx
@@ -0,0 +1,7 @@
+"use client"
+
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+
+const AspectRatio = AspectRatioPrimitive.Root
+
+export { AspectRatio }
diff --git a/src/components/batu/components/ui/avatar.tsx b/src/components/batu/components/ui/avatar.tsx
new file mode 100644
index 0000000..9122baa
--- /dev/null
+++ b/src/components/batu/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "../../lib/utils"
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/src/components/batu/components/ui/badge.tsx b/src/components/batu/components/ui/badge.tsx
new file mode 100644
index 0000000..c2466e3
--- /dev/null
+++ b/src/components/batu/components/ui/badge.tsx
@@ -0,0 +1,29 @@
+import type * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "../../lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+)
+
+export interface BadgeProps extends React.HTMLAttributes, VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return
+}
+
+export { Badge, badgeVariants }
diff --git a/src/components/batu/components/ui/breadcrumb.tsx b/src/components/batu/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..c1720a5
--- /dev/null
+++ b/src/components/batu/components/ui/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "../../lib/utils"
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode
+ }
+>(({ ...props }, ref) => )
+Breadcrumb.displayName = "Breadcrumb"
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<"ol">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbList.displayName = "BreadcrumbList"
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<"li">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbItem.displayName = "BreadcrumbItem"
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+})
+BreadcrumbLink.displayName = "BreadcrumbLink"
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<"span">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbPage.displayName = "BreadcrumbPage"
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) => (
+ svg]:w-3.5 [&>svg]:h-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+)
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More
+
+)
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/src/components/batu/components/ui/button.tsx b/src/components/batu/components/ui/button.tsx
new file mode 100644
index 0000000..2c1f020
--- /dev/null
+++ b/src/components/batu/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "../../lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/src/components/batu/components/ui/calendar.tsx b/src/components/batu/components/ui/calendar.tsx
new file mode 100644
index 0000000..b68c521
--- /dev/null
+++ b/src/components/batu/components/ui/calendar.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "../../lib/utils"
+import { buttonVariants } from "../../components/ui/button"
+
+export type CalendarProps = React.ComponentProps
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ ,
+ IconRight: ({ ...props }) => ,
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = "Calendar"
+
+export { Calendar }
diff --git a/src/components/batu/components/ui/card.tsx b/src/components/batu/components/ui/card.tsx
new file mode 100644
index 0000000..dd8ed5f
--- /dev/null
+++ b/src/components/batu/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "../../lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/src/components/batu/components/ui/carousel.tsx b/src/components/batu/components/ui/carousel.tsx
new file mode 100644
index 0000000..d0c38fd
--- /dev/null
+++ b/src/components/batu/components/ui/carousel.tsx
@@ -0,0 +1,262 @@
+"use client"
+
+import * as React from "react"
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react"
+import { ArrowLeft, ArrowRight } from "lucide-react"
+
+import { cn } from "../../lib/utils"
+import { Button } from "../../components/ui/button"
+
+type CarouselApi = UseEmblaCarouselType[1]
+type UseCarouselParameters = Parameters
+type CarouselOptions = UseCarouselParameters[0]
+type CarouselPlugin = UseCarouselParameters[1]
+
+type CarouselProps = {
+ opts?: CarouselOptions
+ plugins?: CarouselPlugin
+ orientation?: "horizontal" | "vertical"
+ setApi?: (api: CarouselApi) => void
+}
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0]
+ api: ReturnType[1]
+ scrollPrev: () => void
+ scrollNext: () => void
+ canScrollPrev: boolean
+ canScrollNext: boolean
+} & CarouselProps
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ")
+ }
+
+ return context
+}
+
+const Carousel = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & CarouselProps
+>(
+ (
+ {
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins
+ )
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return
+ }
+
+ setCanScrollPrev(api.canScrollPrev())
+ setCanScrollNext(api.canScrollNext())
+ }, [])
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev()
+ }, [api])
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext()
+ }, [api])
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault()
+ scrollPrev()
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault()
+ scrollNext()
+ }
+ },
+ [scrollPrev, scrollNext]
+ )
+
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return
+ }
+
+ setApi(api)
+ }, [api, setApi])
+
+ React.useEffect(() => {
+ if (!api) {
+ return
+ }
+
+ onSelect(api)
+ api.on("reInit", onSelect)
+ api.on("select", onSelect)
+
+ return () => {
+ api?.off("select", onSelect)
+ }
+ }, [api, onSelect])
+
+ return (
+
+
+ {children}
+
+
+ )
+ }
+)
+Carousel.displayName = "Carousel"
+
+const CarouselContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselContent.displayName = "CarouselContent"
+
+const CarouselItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselItem.displayName = "CarouselItem"
+
+const CarouselPrevious = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselPrevious.displayName = "CarouselPrevious"
+
+const CarouselNext = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselNext.displayName = "CarouselNext"
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+}
diff --git a/src/components/batu/components/ui/chart.tsx b/src/components/batu/components/ui/chart.tsx
new file mode 100644
index 0000000..d999ca0
--- /dev/null
+++ b/src/components/batu/components/ui/chart.tsx
@@ -0,0 +1,365 @@
+"use client"
+
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
+
+import { cn } from "../../lib/utils"
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
+
+ return context
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"]
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+})
+ChartContainer.displayName = "Chart"
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([_, config]) => config.theme || config.color
+ )
+
+ if (!colorConfig.length) {
+ return null
+ }
+
+ return (
+