diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..ae386e2 --- /dev/null +++ b/App.tsx @@ -0,0 +1,55 @@ +import React, { useState, useEffect } from 'react'; +import ChatArea from './components/ChatArea'; +import UserProfileSidebar from './components/UserProfileSidebar'; +import FullProfileModal from './components/FullProfileModal'; + +const App: React.FC = () => { + const [isLoading, setIsLoading] = useState(true); + const [activeChannelName] = useState('Gemini AI'); + const [activeFullProfile, setActiveFullProfile] = useState(null); + + useEffect(() => { + // Simulate initial asset loading + const timer = setTimeout(() => { + setIsLoading(false); + }, 1500); + return () => clearTimeout(timer); + }, []); + + if (isLoading) { + return ( +
+
+ {/* Simple Logo Placeholder */} +
+ + + + +
+
+
Starting...
+
Connecting to Discord-Style Interface
+
+ ); + } + + return ( +
+ + + + {activeFullProfile && ( + setActiveFullProfile(null)} + /> + )} +
+ ); +}; + +export default App; \ No newline at end of file diff --git a/README.md b/README.md index 2241000..1ac3f43 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,20 @@
- GHBanner - -

Built with AI Studio

- -

The fastest path from prompt to production with Gemini.

- - Start building -
+ +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +View your app in AI Studio: https://ai.studio/apps/drive/1--JasS0nNpTl-PmY3TkDkX8eL8Omwi5n + +## Run Locally + +**Prerequisites:** Node.js + + +1. Install dependencies: + `npm install` +2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +3. Run the app: + `npm run dev` diff --git a/components/Badges.tsx b/components/Badges.tsx new file mode 100644 index 0000000..bb5b216 --- /dev/null +++ b/components/Badges.tsx @@ -0,0 +1,41 @@ +import React from 'react'; + +// Updated to Green "Verified AI" style +export const VerifiedBotBadge = () => ( + + + + + AI + +); + +export const StaffBadge = () => ( +
+ Discord Staff +
+); + +export const CertifiedBadge = () => ( +
+ Nitro Subscriber +
+); + +export const BugHunterBadge = () => ( +
+ Made by Google +
+); \ No newline at end of file diff --git a/components/ChatArea.tsx b/components/ChatArea.tsx new file mode 100644 index 0000000..7e709fa --- /dev/null +++ b/components/ChatArea.tsx @@ -0,0 +1,256 @@ +import React, { useState, useEffect, useRef } from 'react'; +import ReactMarkdown from 'react-markdown'; +import { Message } from '../types'; +import { sendMessageToGemini } from '../services/geminiService'; +import { VerifiedBotBadge, StaffBadge, CertifiedBadge, BugHunterBadge } from './Badges'; +import MiniProfile from './MiniProfile'; + +interface ChatAreaProps { + activeChannelName: string; + onOpenFullProfile: (user: any) => void; +} + +const GEMINI_AVATAR = 'https://raw.githubusercontent.com/m4rcel-lol/assets/f08fbf7f9e51418c372697e2df89ddb28c59efe3/rectangle-gemini-google-icon-symbol-logo-free-png.webp'; +const USER_AVATAR = 'https://picsum.photos/seed/me/50/50'; +const GEMINI_BANNER = 'https://raw.githubusercontent.com/m4rcel-lol/assets/13ebd5bfa7abe5ee591107b9a7b411f3e3ae2d13/Gemini-API-IoT-banner_1.original.png'; + +const ChatArea: React.FC = ({ activeChannelName, onOpenFullProfile }) => { + const [messages, setMessages] = useState([ + { + id: 'welcome', + role: 'model', + content: `Hello! I'm your AI assistant. I support **bold**, *italics*, \`code\`, and code blocks: +\`\`\`javascript +console.log("Hello Discord!"); +\`\`\` +Ask me anything!`, + timestamp: new Date(), + senderName: 'Gemini AI', + avatarUrl: GEMINI_AVATAR + } + ]); + const [inputValue, setInputValue] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + // Popout State + const [popoutUser, setPopoutUser] = useState(null); + const [popoutPos, setPopoutPos] = useState({ x: 0, y: 0 }); + + const messagesEndRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + const handleSendMessage = async () => { + if (!inputValue.trim() || isLoading) return; + + const userMsg: Message = { + id: Date.now().toString(), + role: 'user', + content: inputValue, + timestamp: new Date(), + senderName: 'You', + avatarUrl: USER_AVATAR + }; + + setMessages(prev => [...prev, userMsg]); + setInputValue(''); + setIsLoading(true); + + try { + const responseText = await sendMessageToGemini(inputValue, messages); + + const botMsg: Message = { + id: (Date.now() + 1).toString(), + role: 'model', + content: responseText, + timestamp: new Date(), + senderName: 'Gemini AI', + avatarUrl: GEMINI_AVATAR + }; + + setMessages(prev => [...prev, botMsg]); + } catch (error) { + console.error(error); + } finally { + setIsLoading(false); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }; + + const formatTime = (date: Date) => { + return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); + }; + + const handleUserClick = (e: React.MouseEvent, msg: Message) => { + e.stopPropagation(); + const rect = e.currentTarget.getBoundingClientRect(); + setPopoutPos({ x: rect.right, y: rect.top }); + + setPopoutUser({ + name: msg.senderName, + avatarUrl: msg.avatarUrl, + isBot: msg.role === 'model', + username: msg.role === 'model' ? 'gemini_ai' : 'current_user', + color: msg.role === 'model' ? '#5865F2' : '#f0b232', // Banner/Accent Color + theme: msg.role === 'model' + ? 'linear-gradient(to bottom right, #4c1d95, #1e40af)' // Darker Purple-Blue Gradient for better text visibility + : '#232428', // Default Dark Gray for User + bannerUrl: msg.role === 'model' ? GEMINI_BANNER : undefined + }); + }; + + return ( +
+ {/* Header */} +
+
+
@
+

{activeChannelName}

+ +
+
+
+ + +
+
+ + {/* Messages */} +
+ {/* Welcome Placeholder */} +
+
+ Gemini +
+

+ {activeChannelName} +

+
+ Verified System Bot + + This is a temporary chat conversation with {activeChannelName} Your messages will be lost after refreshing the site. +
+
+ + {messages.map((msg, index) => { + const isUser = msg.role === 'user'; + + return ( +
+
+ + {/* Avatar */} +
handleUserClick(e, msg)} + > + {msg.senderName} +
+ + {/* Content */} +
+
+ handleUserClick(e, msg)} + > + {msg.senderName} + + + {!isUser && ( +
+ +
+ + + +
+
+ )} + + + {formatTime(msg.timestamp)} + +
+ +
+ + {msg.content} + +
+
+ +
+
+ ); + })} + + {isLoading && ( +
+
+
+
+
+
+
+ )} + +
+
+ + {/* Input Area */} +
+
+ + setInputValue(e.target.value)} + onKeyDown={handleKeyDown} + disabled={isLoading} + autoFocus + /> +
+ + {isLoading && ( +
+ Gemini AI is typing... +
+ )} +
+ + {popoutUser && ( + setPopoutUser(null)} + onOpenFullProfile={onOpenFullProfile} + /> + )} +
+ ); +}; + +export default ChatArea; \ No newline at end of file diff --git a/components/DMList.tsx b/components/DMList.tsx new file mode 100644 index 0000000..43edd1f --- /dev/null +++ b/components/DMList.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { User, Plus, X } from 'lucide-react'; +import { Channel } from '../types'; + +interface DMListProps { + activeChannelId: string; + onSelectChannel: (id: string) => void; +} + +const MOCK_CHANNELS: Channel[] = [ + { id: '1', name: 'Gemini AI', type: 'dm', status: 'online', avatarUrl: 'https://picsum.photos/seed/gemini/50/50' }, + { id: '2', name: 'Wumpus', type: 'dm', status: 'idle', avatarUrl: 'https://picsum.photos/seed/wumpus/50/50' }, + { id: '3', name: 'Clyde', type: 'dm', status: 'dnd', avatarUrl: 'https://picsum.photos/seed/clyde/50/50' }, + { id: '4', name: 'Nelly', type: 'dm', status: 'offline', avatarUrl: 'https://picsum.photos/seed/nelly/50/50' }, +]; + +const DMList: React.FC = ({ activeChannelId, onSelectChannel }) => { + return ( +
+ {/* Search / Find Button */} +
+ +
+ + {/* List */} +
+
+ Direct Messages + +
+ + {MOCK_CHANNELS.map((channel) => ( +
onSelectChannel(channel.id)} + className={`group flex items-center gap-3 px-2 py-2 rounded-[4px] cursor-pointer transition-colors ${ + activeChannelId === channel.id + ? 'bg-[#404249] text-white' + : 'text-[#949ba4] hover:bg-[#35373c] hover:text-[#dbdee1]' + }`} + > +
+ {channel.name} + {channel.status && ( +
+ {channel.status === 'idle' &&
} + {channel.status === 'dnd' &&
} +
+ )} +
+
+ + {channel.name} + + {channel.id === '1' && BOT} +
+ +
+ ))} +
+ + {/* User Area */} +
+
+
+ Me +
+
+
+
Current User
+
#1234
+
+
+ {/* Buttons */} +
+ {/* Minimal controls simulation */} +
+
+
+ ); +}; + +export default DMList; diff --git a/components/FullProfileModal.tsx b/components/FullProfileModal.tsx new file mode 100644 index 0000000..a6c8703 --- /dev/null +++ b/components/FullProfileModal.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import { StaffBadge, CertifiedBadge, BugHunterBadge, VerifiedBotBadge } from './Badges'; +import { X } from 'lucide-react'; + +interface FullProfileModalProps { + user: { + name: string; + avatarUrl: string; + isBot: boolean; + username: string; + color: string; + theme?: string; + bannerUrl?: string; + }; + onClose: () => void; +} + +const FullProfileModal: React.FC = ({ user, onClose }) => { + return ( +
+
e.stopPropagation()} // Prevent closing when clicking inside + > + {/* Banner */} +
+ {/* Close Button */} +
+
+ +
+
+
+ +
+ {/* Avatar */} +
+
+
+ {user.name} +
+
+
+
+ + {/* Top Badges */} + {user.isBot && ( +
+
+ + + +
+
+ )} + {!user.isBot &&
} + + {/* Info Card */} +
+ {/* Name Header */} +
+ {user.name} + {user.isBot && } +
+
{user.username}
+ + {/* Divider */} +
+ + {/* About Me */} +
+
About Me
+
+ {user.isBot + ? "I am a highly advanced Large Language Model, trained by Google. I can help you with writing, learning, planning, and more. I am designed to be helpful, harmless, and honest." + : "Just a human chatting with an AI. Enjoys long walks on the digital beach and dark mode UIs."} +
+
+ + {/* Member Since */} +
+
+
Member Since
+
Feb 21, 2024
+
+
+
+ +
+
+
+ ); +}; + +export default FullProfileModal; \ No newline at end of file diff --git a/components/MiniProfile.tsx b/components/MiniProfile.tsx new file mode 100644 index 0000000..3e47950 --- /dev/null +++ b/components/MiniProfile.tsx @@ -0,0 +1,112 @@ +import React, { useEffect, useRef } from 'react'; +import { StaffBadge, CertifiedBadge, BugHunterBadge, VerifiedBotBadge } from './Badges'; + +interface MiniProfileProps { + user: { + name: string; + avatarUrl: string; + isBot: boolean; + username: string; + color: string; + theme?: string; + bannerUrl?: string; + }; + position: { x: number, y: number }; + onClose: () => void; + onOpenFullProfile: (user: any) => void; +} + +const MiniProfile: React.FC = ({ user, position, onClose, onOpenFullProfile }) => { + const ref = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + onClose(); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [onClose]); + + const style: React.CSSProperties = { + top: Math.min(position.y, window.innerHeight - 400), // Prevent going off bottom + left: position.x + 20, // Offset to right + background: user.theme || '#232428' + }; + + const handleAvatarClick = () => { + onClose(); // Close mini profile + onOpenFullProfile(user); // Open full profile + }; + + return ( +
+ {/* Banner */} +
+ +
+ {/* Avatar */} +
+
+
+ {user.name} +
+
+ VIEW PROFILE +
+
+
+
+ + {/* Badges */} + {user.isBot && ( +
+
+ + + +
+
+ )} + {!user.isBot &&
} + + {/* Info */} +
+
+ {user.name} + {user.isBot && } +
+
{user.username}
+ +
+ +
About Me
+
+ {user.isBot + ? "I am a helpful AI assistant residing in your Discord DMs." + : "Just a human chatting with an AI."} +
+ +
Member Since
+
Feb 21, 2024
+
+
+
+ ); +}; + +export default MiniProfile; \ No newline at end of file diff --git a/components/ServerRail.tsx b/components/ServerRail.tsx new file mode 100644 index 0000000..48d1bd6 --- /dev/null +++ b/components/ServerRail.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Compass, Plus, Download, Home } from 'lucide-react'; + +const ServerRail: React.FC = () => { + return ( +
+ {/* Home Icon */} +
+ +
+
+ +
+ + {/* Mock Servers */} + {[...Array(3)].map((_, i) => ( +
+ Server +
+
+ ))} + +
+ +
+ +
+ +
+
+ ); +}; + +export default ServerRail; diff --git a/components/UserProfileSidebar.tsx b/components/UserProfileSidebar.tsx new file mode 100644 index 0000000..95a9063 --- /dev/null +++ b/components/UserProfileSidebar.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { StaffBadge, CertifiedBadge, BugHunterBadge, VerifiedBotBadge } from './Badges'; + +interface UserProfileSidebarProps { + onOpenFullProfile: (user: any) => void; +} + +const UserProfileSidebar: React.FC = ({ onOpenFullProfile }) => { + const geminiUser = { + name: 'Gemini AI', + avatarUrl: 'https://raw.githubusercontent.com/m4rcel-lol/assets/f08fbf7f9e51418c372697e2df89ddb28c59efe3/rectangle-gemini-google-icon-symbol-logo-free-png.webp', + isBot: true, + username: 'gemini_ai', + color: '#5865F2', + theme: 'linear-gradient(to bottom right, #4c1d95, #1e40af)', // Darker Purple-Blue Gradient for readability + bannerUrl: 'https://raw.githubusercontent.com/m4rcel-lol/assets/13ebd5bfa7abe5ee591107b9a7b411f3e3ae2d13/Gemini-API-IoT-banner_1.original.png' + }; + + return ( +
+ {/* Banner */} +
+ {/* Pencil icon on hover could go here */} +
+ + {/* Profile Header */} +
+ {/* Avatar */} +
onOpenFullProfile(geminiUser)}> +
+
+ Profile +
+
+ VIEW PROFILE +
+
+
+
+ + {/* Badges Container */} +
+
+ + + +
+
+ + {/* User Info */} +
+
+ onOpenFullProfile(geminiUser)}>Gemini AI + +
+
gemini_ai
+ +
+
About Me
+
+ I am a large language model, trained by Google. I can help you with writing, learning, planning, and more. +
+
+ +
+
Member Since
+
Feb 21, 2024
+
+
+ + {/* Note */} +
+
Note
+