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 && ( +
+ + Generated or uploaded image + + {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 ( + + + + Cookie Consent + + We use cookies to enhance your browsing experience and store chat messages locally. By clicking "Accept", you agree to our use of cookies. + + + + + + + + {showPrivacyPolicy && ( + + + + Privacy Policy + + +

At Karma-Pollinations, we respect your privacy and are committed to protecting your personal data. This privacy policy will inform you about how we collect, use, and protect your information when you use our Karma-Pollinations chatbox. + + 1. Data Collection: We collect and store your chat messages locally on your device using browser localStorage. This allows us to provide a seamless experience and maintain your conversation history. + + 2. Cookie Usage: We use cookies to remember your preferences and consent choices. These cookies are essential for the proper functioning of our service. + + 3. Data Usage: The information we collect is used solely to improve your experience with our chatbox. We do not sell or share your personal data with third parties. + + 4. Data Protection: We implement appropriate technical and organizational measures to ensure the security of your personal data. + + 5. User Rights: You have the right to access, rectify, or erase your personal data. You can clear your chat history at any time using the reset function. + + 6. Changes to Policy: We may update this privacy policy from time to time. We will notify you of any changes by posting the new policy on this page. + + By using our Karma-Pollinations chatbox, you agree to the collection and use of information in accordance with this policy. If you have any questions about this privacy policy, please contact us. + + + + + +

+ )} +
+ ) +} + 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 ( +
+
+ Karma-Pollinations Logo +
+

karma.pollinations.ai

+
+ {/* */} + + +
+
+ ) +} + 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 ( +
+
+ setMessage(e.target.value)} + placeholder="Type your message here..." + className="flex-grow" + /> + + + + + +
+
+
+ + + + + + + +

Attached images will influence text and model interpretation

+
+
+
+ {file && {file.name}} +
+
+