diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..5ef6a520780202a1d6addd833d800ccb1ecac0bb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,41 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files (can opt-in for committing if needed)
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..d42011aa07a361efe39328cf6da0663b4f46528b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,99 @@
+# Pollinations Chatbox - Karma.yt Experiment
+
+An experimental chat interface powered by Karma.yt, utilizing the advanced AI and image generation capabilities of Pollinations. Explore various AI models for interactive text and image generation, with local storage for seamless conversation continuity. This project is an innovative integration of Pollinations technologies to create a unique and flexible chat experience.
+
+## 📧 Contact
+
+If you have any questions or suggestions, feel free to reach out to us via email at: **hello@pollinations.ai**
+
+## 🌍 Social Media
+
+- **Discord**: [Pollinations Discord](https://discord.gg/k9F7SyTgqn)
+- **WhatsApp**: [WhatsApp Group](https://chat.whatsapp.com/JxQEn2FKDny0DdwkDuzoQR)
+
+## 🛠️ Requirements
+
+Before running the project locally, make sure you have the following:
+
+- **Node.js**: Version 16.x or above
+- **Next.js**: Used for building and rendering the website.
+- **pnpm** (recommended), **npm**, or **bun** for package management.
+
+### Installation
+
+1. Clone the repository:
+
+ ```bash
+ git clone https://github.com/diogo-karma/pollinations-chatbox.git
+ cd pollinations-chatbox
+ ```
+
+2. Install dependencies:
+
+ If using **pnpm** (recommended):
+
+ ```bash
+ pnpm install
+ ```
+
+ Or using **npm**:
+
+ ```bash
+ npm install
+ ```
+
+ Or **bun**:
+
+ ```bash
+ bun install
+ ```
+
+3. Start the development server:
+
+ ```bash
+ pnpm dev
+ ```
+
+ Or using **npm**:
+
+ ```bash
+ npm run dev
+ ```
+
+ Or **bun**:
+
+ ```bash
+ bun dev
+ ```
+
+4. Open the app in your browser at [http://localhost:3000](http://localhost:3000).
+
+## 🛠️ Folder Structure
+
+The project follows the standard Next.js conventions with additional folders for organization:
+
+```
+/app # App logic and components, including pages
+ /api # API routes for backend interaction
+/components # Reusable UI components
+ /ui # Custom UI components built with ShadCN and Tailwind
+/hooks # Custom hooks for additional functionality
+/lib # Helper functions and external libraries
+/public # Public assets like images and fonts
+/styles # Global styles, including Tailwind configuration
+```
+
+## 🎨 Design and UX
+
+We use **Tailwind CSS** for styling the interface, ensuring a responsive design, while **ShadCN UI** is used for creating a modern and fluid user experience (UX).
+
+## 📚 Full Documentation
+
+For more details on how the Pollinations technology works, visit the official documentation at [Pollinations.ai](https://pollinations.ai).
+
+---
+
+Thank you for exploring the **Pollinations (chatbox) Experiment**! Stay tuned for updates and improvements as we continue the development of this application. If you'd like to contribute or discuss new ideas, join us on **Discord** or the **WhatsApp group**!
+
+[Pollinations Discord](https://discord.gg/k9F7SyTgqn)
+[WhatsApp Group](https://chat.whatsapp.com/JxQEn2FKDny0DdwkDuzoQR)
\ No newline at end of file
diff --git a/app/api/messages/route.ts b/app/api/messages/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8071cdbc9994f6efef78ac195fc0c85d850384f1
--- /dev/null
+++ b/app/api/messages/route.ts
@@ -0,0 +1,92 @@
+import { NextRequest, NextResponse } from "next/server";
+
+export async function POST(req: NextRequest) {
+ const formData = await req.formData();
+ console.dir(formData);
+ const message = formData.get("message") as string;
+ const type = formData.get("type") as string;
+ const model = JSON.parse(formData.get("model") as string);
+ const image = formData.get("image") as string;
+
+ let content = ``;
+ let imageUrl = null;
+ let visionDescription = null;
+ const seed = Math.floor(Math.random() * 1337);
+
+
+ if (image) {
+ // const visionPayload = {
+ // messages: [
+ // {
+ // role: "user",
+ // content: [
+ // { type: "text", text: message },
+ // {
+ // type: "image_url",
+ // image_url: {
+ // url: image,
+ // },
+ // },
+ // ],
+ // },
+ // ],
+ // };
+
+ // const visionResponse = await fetch(process.env.AZURE_API_URL, {
+ // method: "POST",
+ // headers: {
+ // "Content-Type": "application/json",
+ // "api-key": process.env.AZURE_API_KEY,
+ // },
+ // body: JSON.stringify(visionPayload),
+ // });
+
+ // console.log(
+ // process.env.AZURE_API_URL,
+ // process.env.AZURE_API_KEY,
+ // JSON.stringify(visionPayload)
+ // );
+ // const visionData = await visionResponse.json();
+ // console.log(visionData);
+ // visionDescription =
+ // visionData?.choices[0]?.message?.content || "No description available.";
+
+ // if (type === "text") {
+ // const influencedPrompt = `${message} influenced by ${visionDescription}`;
+ // const prompt = encodeURIComponent(influencedPrompt);
+ // content = await fetch(
+ // `https://text.pollinations.ai/${prompt}?seed=${seed}&model=${model.name}`
+ // )
+ // .then((res) => res.text())
+ // .catch((err) => `Error fetching text: ${err.message}`);
+ // } else if (type === "image") {
+ // const combinedPrompt = `${message} ${visionDescription}`;
+ // const prompt = encodeURIComponent(combinedPrompt);
+ // imageUrl = `https://image.pollinations.ai/prompt/${prompt}?width=1024&height=1024&seed=${seed}&model=${model.name}`;
+ // }
+ } else if (type === "text") {
+ const prompt = encodeURIComponent(message);
+ content = await fetch(
+ `https://text.pollinations.ai/${prompt}?seed=${seed}&model=${model.name}`
+ )
+ .then((res) => res.text())
+ .catch((err) => `Error fetching text: ${err.message}`);
+ } else if (type === "image") {
+ const prompt = encodeURIComponent(message);
+ imageUrl = `https://image.pollinations.ai/prompt/${prompt}?width=1024&height=1024&seed=${seed}&model=${model.name}&nologo=true`;
+ } else {
+ return NextResponse.json(
+ { error: "Invalid type. Use 'image' or 'text'." },
+ { status: 400 }
+ );
+ }
+
+ return NextResponse.json({
+ type,
+ model,
+ seed,
+ imageUrl,
+ content,
+ visionDescription,
+ });
+}
diff --git a/app/globals.css b/app/globals.css
new file mode 100644
index 0000000000000000000000000000000000000000..ac6844236cb8a3195672afa1ea2867e0853a9618
--- /dev/null
+++ b/app/globals.css
@@ -0,0 +1,94 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+body {
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+@layer utilities {
+ .text-balance {
+ text-wrap: balance;
+ }
+}
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 0 0% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 0 0% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 0 0% 3.9%;
+ --primary: 0 0% 9%;
+ --primary-foreground: 0 0% 98%;
+ --secondary: 0 0% 96.1%;
+ --secondary-foreground: 0 0% 9%;
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 89.8%;
+ --input: 0 0% 89.8%;
+ --ring: 0 0% 3.9%;
+ --chart-1: 12 76% 61%;
+ --chart-2: 173 58% 39%;
+ --chart-3: 197 37% 24%;
+ --chart-4: 43 74% 66%;
+ --chart-5: 27 87% 67%;
+ --radius: 0.5rem;
+ --sidebar-background: 0 0% 98%;
+ --sidebar-foreground: 240 5.3% 26.1%;
+ --sidebar-primary: 240 5.9% 10%;
+ --sidebar-primary-foreground: 0 0% 98%;
+ --sidebar-accent: 240 4.8% 95.9%;
+ --sidebar-accent-foreground: 240 5.9% 10%;
+ --sidebar-border: 220 13% 91%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
+ }
+ .dark {
+ --background: 0 0% 3.9%;
+ --foreground: 0 0% 98%;
+ --card: 0 0% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 0 0% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary: 0 0% 98%;
+ --primary-foreground: 0 0% 9%;
+ --secondary: 0 0% 14.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+ --accent: 0 0% 14.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+ --ring: 0 0% 83.1%;
+ --chart-1: 220 70% 50%;
+ --chart-2: 160 60% 45%;
+ --chart-3: 30 80% 55%;
+ --chart-4: 280 65% 60%;
+ --chart-5: 340 75% 55%;
+ --sidebar-background: 240 5.9% 10%;
+ --sidebar-foreground: 240 4.8% 95.9%;
+ --sidebar-primary: 224.3 76.3% 48%;
+ --sidebar-primary-foreground: 0 0% 100%;
+ --sidebar-accent: 240 3.7% 15.9%;
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
+ --sidebar-border: 240 3.7% 15.9%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5c0e280493cea486d2fb5327fa25d7bfae1541f6
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,14 @@
+import { ThemeProvider } from "@/components/theme-provider"
+import './globals.css'
+export default function RootLayout({ children }) {
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
+
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5e937eb14f0083cb8c54f42fa460fc57b03161aa
--- /dev/null
+++ b/app/page.tsx
@@ -0,0 +1,156 @@
+'use client'
+
+import { useState, useEffect, useRef } from 'react'
+import { useTheme } from 'next-themes'
+import Header from '@/components/Header'
+import ChatArea from '@/components/ChatArea'
+import InputArea from '@/components/InputArea'
+import Footer from '@/components/Footer'
+import CookieConsent from '@/components/CookieConsent'
+import { v4 as uuidv4 } from 'uuid'
+
+export default function KarmaPollinations() {
+ const [messages, setMessages] = useState([])
+ const [textModels, setTextModels] = useState([])
+ const [imageModels, setImageModels] = useState([])
+ const { setTheme } = useTheme()
+ const chatAreaRef = useRef(null)
+ const [showCookieConsent, setShowCookieConsent] = useState(true)
+
+ useEffect(() => {
+ setTheme('dark')
+ fetchModels()
+ loadMessagesFromLocalStorage()
+ const consent = localStorage.getItem('cookieConsent')
+ if (consent === 'accepted') {
+ setShowCookieConsent(false)
+ }
+ }, [setTheme])
+
+ useEffect(() => {
+ fetchModels()
+ }, [])
+
+ useEffect(() => {
+ scrollToBottom()
+ }, [messages])
+
+ const fetchModels = async () => {
+ try {
+ const textResponse = await fetch('https://text.pollinations.ai/models')
+ const textData = await textResponse.json()
+ setTextModels(textData.map(model => ({ ...model, id: `text-${model.name}` })))
+
+ const imageResponse = await fetch('https://image.pollinations.ai/models')
+ const imageData = await imageResponse.json()
+ setImageModels(imageData.map((model, index) => ({ id: `image-${model}-${index}`, name: model, description: model })))
+ } catch (error) {
+ console.error('Error fetching models:', error)
+ }
+ }
+
+ const handleSendMessage = async (message, type, model, file) => {
+ const newMessage = {
+ id: uuidv4(),
+ content: message,
+ sender: 'user',
+ timestamp: new Date().toISOString(),
+ type,
+ model,
+ }
+ const updatedMessages = [...messages, newMessage]
+ setMessages(updatedMessages)
+ saveMessagesToLocalStorage(updatedMessages)
+
+ try {
+ const formData = new FormData()
+ formData.append('message', message)
+ formData.append('type', type)
+ formData.append('model', JSON.stringify(model))
+ if (file) {
+ formData.append('image', file)
+ }
+
+ const response = await fetch('/api/messages', {
+ method: 'POST',
+ body: formData,
+ })
+
+ if (!response.ok) {
+ throw new Error('Failed to process message')
+ }
+
+ const data = await response.json()
+
+ const assistantMessage = {
+ id: uuidv4(),
+ content: data.content,
+ sender: 'assistant',
+ timestamp: new Date().toISOString(),
+ type: data.type,
+ model: data.model,
+ }
+
+ if (data.imageUrl) {
+ assistantMessage.imageUrl = data.imageUrl
+ assistantMessage.imageText = data.imageText
+ }
+
+ const finalMessages = [...updatedMessages, assistantMessage]
+ setMessages(finalMessages)
+ saveMessagesToLocalStorage(finalMessages)
+ } catch (error) {
+ console.error('Error processing message:', error)
+ }
+ }
+
+ const scrollToBottom = () => {
+ if (chatAreaRef.current) {
+ chatAreaRef.current.scrollTo({
+ top: chatAreaRef.current.scrollHeight,
+ behavior: 'smooth',
+ })
+ }
+ }
+
+ const handleReset = () => {
+ localStorage.removeItem('chatMessages')
+ setMessages([])
+ }
+
+ const loadMessagesFromLocalStorage = () => {
+ const storedMessages = localStorage.getItem('chatMessages')
+ if (storedMessages) {
+ setMessages(JSON.parse(storedMessages))
+ }
+ }
+
+ const saveMessagesToLocalStorage = (messages) => {
+ localStorage.setItem('chatMessages', JSON.stringify(messages))
+ }
+
+ const handleAcceptCookies = () => {
+ setShowCookieConsent(false)
+ localStorage.setItem('cookieConsent', 'accepted')
+ }
+
+ return (
+
+ {/* Fixed Header */}
+
+
+ {/* Main Content with Scrollable Center */}
+
+
+
+
+ {/* Fixed Footer */}
+
+
+ )
+}
diff --git a/components.json b/components.json
new file mode 100644
index 0000000000000000000000000000000000000000..d9ef0ae537daba0b9fb71e78522e5aa763c4de93
--- /dev/null
+++ b/components.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "app/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "iconLibrary": "lucide"
+}
\ No newline at end of file
diff --git a/components/ChatArea.tsx b/components/ChatArea.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b6e028f95c6c46538241c31089a0e75b229a50a4
--- /dev/null
+++ b/components/ChatArea.tsx
@@ -0,0 +1,121 @@
+'use client'
+
+import { useState, useEffect, useRef } from 'react'
+import { Copy, User } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import { motion } from 'framer-motion'
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
+import ReactMarkdown from 'react-markdown' // For rendering markdown content
+import remarkGfm from 'remark-gfm' // To support GitHub-flavored markdown (e.g., tables, strikethrough, etc.)
+
+export default function ChatArea({ messages, chatAreaRef }) {
+ const [copiedId, setCopiedId] = useState(null) // State to track the copied message
+ const bottomRef = useRef(null) // Ref for auto-scroll to bottom
+
+ // Function to copy message content to the clipboard
+ const handleCopy = (content, id) => {
+ navigator.clipboard.writeText(content)
+ setCopiedId(id)
+ setTimeout(() => setCopiedId(null), 2000) // Reset copied state after 2 seconds
+ }
+
+ // Automatically scroll to the bottom whenever new messages arrive
+ useEffect(() => {
+ bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
+ }, [messages])
+
+ return (
+
+ {messages.map((message) => (
+
+
+ {/* Avatar for user or bot */}
+
+ {message.sender === 'user' ? (
+
+ ) : (
+
+ )}
+ {message.sender === 'user' ? : 'K'}
+
+
+ {/* Message container */}
+
+
+ {message.sender === 'user' ? 'You' : 'Karma'}
+ {/* Copy Button */}
+
+ {/* Copy confirmation */}
+ {copiedId === message.id ? (
+ Copied ✅
+ ) : (
+ )}
+
+
+
+ {/* Image rendering with link */}
+ {message.imageUrl && (
+
+
+
+
+ {message.imageText && (
+
{message.imageText}
+ )}
+
+ )}
+
+ {/* Markdown rendering for message content */}
+ {message.content && (
+
+
+ {message.content}
+
+
+ )}
+
+ {/* Metadata (model and timestamp) */}
+
+
model: {message.model?.description || 'N/A'} [{new Date(message.timestamp).toLocaleString()}] - {message.seed}
+
+
+
+
+
+
+ ))}
+ {/* Ref for auto-scroll to bottom */}
+
+
+ )
+}
diff --git a/components/CookieConsent.tsx b/components/CookieConsent.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..caceaa559c2633c4775f8c164e1e61b864cd6b92
--- /dev/null
+++ b/components/CookieConsent.tsx
@@ -0,0 +1,56 @@
+'use client'
+
+import { useState } from 'react'
+import { Button } from '@/components/ui/button'
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
+
+export default function CookieConsent({ onAccept }) {
+ const [showPrivacyPolicy, setShowPrivacyPolicy] = useState(false)
+
+ return (
+
+ )
+}
+
diff --git a/components/Footer.tsx b/components/Footer.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9eda53e02d9713da8cf7883de230b3a57f9a2a0f
--- /dev/null
+++ b/components/Footer.tsx
@@ -0,0 +1,32 @@
+import { FaDiscord, FaWhatsapp, FaInstagram, FaTiktok, FaGithub } from 'react-icons/fa'
+
+export default function Footer() {
+ return (
+
+
+ )
+}
+
diff --git a/components/Header.tsx b/components/Header.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d42b5a4778ea6e9e277c39604831143202468da1
--- /dev/null
+++ b/components/Header.tsx
@@ -0,0 +1,29 @@
+'use client'
+
+import { Moon, Sun, RefreshCw } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import { useTheme } from 'next-themes'
+import { SharePopover } from './SharePopover'
+
+export default function Header({ onReset }) {
+ const { theme, setTheme } = useTheme()
+
+ return (
+
+ )
+}
+
diff --git a/components/InputArea.tsx b/components/InputArea.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a0f673e36d403581305ac9ac537cbd92d5ed758c
--- /dev/null
+++ b/components/InputArea.tsx
@@ -0,0 +1,154 @@
+'use client'
+
+import { useState, useEffect, ChangeEvent, FormEvent } from 'react'
+import { Send, Paperclip } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
+import { motion } from 'framer-motion'
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
+import Footer from './Footer'
+
+// Tipos para os modelos de texto e imagem
+interface Model {
+ id: string
+ description: string
+}
+
+interface FileData {
+ base64: string | ArrayBuffer | null
+ type: string
+ name: string
+}
+
+interface InputAreaProps {
+ onSendMessage: (message: string, type: 'text' | 'image', model: Model | undefined, fileBase64: string | null) => void
+ textModels: Model[]
+ imageModels: Model[]
+}
+
+export default function InputArea({ onSendMessage, textModels, imageModels }: InputAreaProps) {
+ const [message, setMessage] = useState('')
+ const [type, setType] = useState<'text' | 'image'>('text')
+ const [selectedModel, setSelectedModel] = useState(textModels[0]?.id || '')
+ const [file, setFile] = useState(null)
+
+ useEffect(() => {
+ if (textModels.length > 0 && type === 'text') {
+ setSelectedModel(textModels[0].id)
+ } else if (imageModels.length > 0 && type === 'image') {
+ setSelectedModel(imageModels[0].id)
+ }
+ }, [type, textModels, imageModels])
+
+ const handleSubmit = (e: FormEvent) => {
+ e.preventDefault()
+ if (message.trim() || file) {
+ const model = type === 'text'
+ ? textModels.find(m => m.id === selectedModel)
+ : imageModels.find(m => m.id === selectedModel)
+
+ onSendMessage(message, type, model, file ? file.base64 : null)
+ setMessage('')
+ setFile(null)
+ }
+ }
+
+ const handleFileChange = (e: ChangeEvent) => {
+ const selectedFile = e.target.files?.[0]
+ if (selectedFile) {
+ // Verifica o tamanho do arquivo (4MB)
+ if (selectedFile.size > 4 * 1024 * 1024) {
+ alert('File size exceeds 4MB limit.')
+ return
+ }
+
+ // Cria o Base64 do arquivo com mimetype
+ const reader = new FileReader()
+ reader.onloadend = () => {
+ setFile({
+ base64: reader.result,
+ type: selectedFile.type,
+ name: selectedFile.name,
+ })
+ }
+ reader.readAsDataURL(selectedFile)
+ }
+ }
+
+ const handleTypeChange = (newType: 'text' | 'image') => {
+ setType(newType)
+ setSelectedModel(newType === 'text' ? textModels[0]?.id : imageModels[0]?.id)
+ }
+
+ return (
+
+ )
+}
diff --git a/components/SharePopover.tsx b/components/SharePopover.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..97481410eda718cc907f8ce3470d00101cc40740
--- /dev/null
+++ b/components/SharePopover.tsx
@@ -0,0 +1,44 @@
+'use client'
+import { useState } from 'react'
+import { Share, Copy } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@/components/ui/popover'
+
+export function SharePopover() {
+ const [copied, setCopied] = useState(false)
+ const url = typeof window !== 'undefined' ? window.location.href : ''
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(url)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 2000)
+ }
+
+ return (
+
+
+
+
+
+
+
Share this conversation
+
+
+
+
+ {copied &&
Copied to clipboard!
}
+
+
+
+ )
+}
+
diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..490a6f8e3b7aa401ce34f07feb15857541906ece
--- /dev/null
+++ b/components/theme-provider.tsx
@@ -0,0 +1,10 @@
+'use client'
+
+import * as React from 'react'
+import { ThemeProvider as NextThemesProvider } from 'next-themes'
+import { type ThemeProviderProps } from 'next-themes'
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return {children}
+}
+
diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..24c788c2c44cb058812cdbb97d071439a87af5f0
--- /dev/null
+++ b/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/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..25e7b474464c9849cef836f5c863be0f38bdaac3
--- /dev/null
+++ b/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/components/ui/alert.tsx b/components/ui/alert.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..41fa7e0561a3fdb5f986c1213a35e563de740e96
--- /dev/null
+++ b/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/components/ui/aspect-ratio.tsx b/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d6a5226f5e9cc09a2bd1ccbce465524e41f04345
--- /dev/null
+++ b/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/components/ui/avatar.tsx b/components/ui/avatar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..51e507ba9d08bcdbb1fb630498f1cbdf2bf50093
--- /dev/null
+++ b/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/components/ui/badge.tsx b/components/ui/badge.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f000e3ef5176395b067dfc3f3e1256a80c450015
--- /dev/null
+++ b/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * 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/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..60e6c96f72f0350d08b47e4730cab8f3975dc853
--- /dev/null
+++ b/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/components/ui/button.tsx b/components/ui/button.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..36496a28727a3643b4212a14225d4f6cbd50bda5
--- /dev/null
+++ b/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/components/ui/calendar.tsx b/components/ui/calendar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..61d2b451ef5fd1d9faa1162371628a8132dd51df
--- /dev/null
+++ b/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/components/ui/card.tsx b/components/ui/card.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f62edea578d50058bef5e6bcc178b88d145564e9
--- /dev/null
+++ b/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/components/ui/carousel.tsx b/components/ui/carousel.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ec505d00d95ecdbbc1501d1bfa7020e51d6010a7
--- /dev/null
+++ b/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/components/ui/chart.tsx b/components/ui/chart.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8620baa3b32301ef0e071b13049f2a00caa6f0bc
--- /dev/null
+++ b/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 (
+