feat: Initialize Discord-style AI chat project

Sets up the project structure, dependencies, and basic configuration for a Discord-inspired AI chat application. Includes placeholder components and types, Vite configuration for local development, and basic styling for a dark theme and custom scrollbars. Integrates Gemini API key handling and a service for sending messages.
This commit is contained in:
m5rcel { Marcel }
2025-12-04 21:13:35 +01:00
parent 2e3ea2d71d
commit cb1f66dbff
18 changed files with 1130 additions and 8 deletions

24
.gitignore vendored Normal file
View File

@@ -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?

55
App.tsx Normal file
View File

@@ -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<any>(null);
useEffect(() => {
// Simulate initial asset loading
const timer = setTimeout(() => {
setIsLoading(false);
}, 1500);
return () => clearTimeout(timer);
}, []);
if (isLoading) {
return (
<div className="h-screen w-screen bg-[#313338] flex flex-col items-center justify-center">
<div className="relative w-24 h-24 mb-4 animate-bounce">
{/* Simple Logo Placeholder */}
<div className="w-20 h-20 bg-[#5865F2] rounded-2xl flex items-center justify-center mx-auto shadow-xl">
<svg width="48" height="48" viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg">
<path d="M19.25 4.5H4.75C3.50736 4.5 2.5 5.50736 2.5 6.75V17.25C2.5 18.4926 3.50736 19.5 4.75 19.5H19.25C20.4926 19.5 21.5 18.4926 21.5 17.25V6.75C21.5 5.50736 20.4926 4.5 19.25 4.5Z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M8.5 10L12 14L15.5 10" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
</div>
<div className="text-[#dbdee1] font-bold text-lg animate-pulse">Starting...</div>
<div className="text-[#949ba4] text-sm mt-2">Connecting to Discord-Style Interface</div>
</div>
);
}
return (
<div className="flex h-screen w-screen overflow-hidden font-sans bg-[#313338]">
<ChatArea
activeChannelName={activeChannelName}
onOpenFullProfile={setActiveFullProfile}
/>
<UserProfileSidebar onOpenFullProfile={setActiveFullProfile} />
{activeFullProfile && (
<FullProfileModal
user={activeFullProfile}
onClose={() => setActiveFullProfile(null)}
/>
)}
</div>
);
};
export default App;

View File

@@ -1,11 +1,20 @@
<div align="center"> <div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" /> <img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
<h1>Built with AI Studio</h2>
<p>The fastest path from prompt to production with Gemini.</p>
<a href="https://aistudio.google.com/apps">Start building</a>
</div> </div>
# 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`

41
components/Badges.tsx Normal file
View File

@@ -0,0 +1,41 @@
import React from 'react';
// Updated to Green "Verified AI" style
export const VerifiedBotBadge = () => (
<span className="flex items-center gap-[2px] bg-[#23a559] text-white text-[10px] px-[4px] rounded-[4px] h-[15px] select-none align-baseline ml-1.5 font-medium leading-none">
<svg aria-label="Verified AI" aria-hidden="false" role="img" width="10" height="10" viewBox="0 0 16 15.2" fill="none" xmlns="http://www.w3.org/2000/svg" className="mt-[1px]">
<path d="M7.4,11.17,4.17,8.22,5.55,6.81l1.85,1.69L12.95,3l1.38,1.43L7.4,11.17Z" fill="currentColor"/>
</svg>
AI
</span>
);
export const StaffBadge = () => (
<div className="group/badge relative cursor-pointer hover:bg-[#3f4147] rounded p-0.5" title="Discord Staff">
<img
src="https://raw.githubusercontent.com/m4rcel-lol/assets/27e24d3b15c32ba2d39e54d544b66ba2575e2e3d/865852-discord-clyde.png"
alt="Discord Staff"
className="w-[22px] h-[22px] object-contain"
/>
</div>
);
export const CertifiedBadge = () => (
<div className="group/badge relative cursor-pointer hover:bg-[#3f4147] rounded p-0.5" title="Nitro Subscriber Since February 21st 2024">
<img
src="https://raw.githubusercontent.com/m4rcel-lol/assets/283298c22f1a688c8277e1f6777144731bfdf1e0/67822-opal-nitro-tier.png"
alt="Nitro Subscriber"
className="w-[22px] h-[22px] object-contain"
/>
</div>
);
export const BugHunterBadge = () => (
<div className="group/badge relative cursor-pointer hover:bg-[#3f4147] rounded p-0.5" title="This Chatbot has been made by Google">
<img
src="https://raw.githubusercontent.com/m4rcel-lol/assets/0d09cd59eed6bf752d4dfdcfb21dada876860012/47207-google.png"
alt="Made by Google"
className="w-[22px] h-[22px] object-contain"
/>
</div>
);

256
components/ChatArea.tsx Normal file
View File

@@ -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<ChatAreaProps> = ({ activeChannelName, onOpenFullProfile }) => {
const [messages, setMessages] = useState<Message[]>([
{
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<any>(null);
const [popoutPos, setPopoutPos] = useState({ x: 0, y: 0 });
const messagesEndRef = useRef<HTMLDivElement>(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 (
<div className="flex-1 flex flex-col bg-[#313338] h-full relative min-w-0 animate-fade-in">
{/* Header */}
<div className="h-12 border-b border-[#26272D] flex items-center justify-between px-4 shadow-sm z-10 select-none">
<div className="flex items-center gap-2 overflow-hidden">
<div className="text-[#80848e] text-2xl font-light">@</div>
<h3 className="font-bold text-[#f2f3f5] truncate text-base">{activeChannelName}</h3>
<div className="flex items-center gap-1.5 ml-2">
<div className="w-2.5 h-2.5 bg-[#23a559] rounded-full"></div>
</div>
<VerifiedBotBadge />
</div>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto custom-scrollbar p-4 flex flex-col">
{/* Welcome Placeholder */}
<div className="mt-4 mb-8 px-4 animate-slide-up">
<div className="w-20 h-20 rounded-full flex items-center justify-center mb-4 shadow-lg overflow-hidden bg-[#232428]">
<img src={GEMINI_AVATAR} alt="Gemini" className="w-full h-full object-cover" />
</div>
<h2 className="text-3xl font-bold text-white mb-2">
<span className="font-bold">{activeChannelName}</span>
</h2>
<div className="text-[#b5bac1] text-sm flex items-center gap-2">
<span>Verified System Bot</span>
<span className="w-1 h-1 bg-[#b5bac1] rounded-full"></span>
<span>This is a temporary chat conversation with </span> <span>{activeChannelName}</span> <span>Your messages will be lost after refreshing the site.</span>
</div>
</div>
{messages.map((msg, index) => {
const isUser = msg.role === 'user';
return (
<div
key={msg.id}
className="group flex justify-start w-full hover:bg-[#2e3035] -mx-4 px-8 py-0.5 mt-[1.0625rem] first:mt-0 animate-fade-in"
style={{ width: 'calc(100% + 2rem)' }}
>
<div className="flex gap-4 w-full">
{/* Avatar */}
<div
className="flex-shrink-0 mt-0.5 cursor-pointer hover:drop-shadow-md transition-all active:translate-y-[1px]"
onClick={(e) => handleUserClick(e, msg)}
>
<img
src={msg.avatarUrl}
alt={msg.senderName}
className="w-10 h-10 rounded-full hover:opacity-90 transition-opacity"
/>
</div>
{/* Content */}
<div className="flex flex-col items-start w-full min-w-0">
<div className="flex items-center gap-2 mb-1">
<span
className="text-[#f2f3f5] font-medium hover:underline cursor-pointer text-[1rem]"
onClick={(e) => handleUserClick(e, msg)}
>
{msg.senderName}
</span>
{!isUser && (
<div className="flex items-center gap-1 select-none">
<VerifiedBotBadge />
<div className="flex items-center ml-1 space-x-0.5">
<StaffBadge />
<CertifiedBadge />
<BugHunterBadge />
</div>
</div>
)}
<span className="text-xs text-[#949ba4] font-medium ml-1 mt-[1px]">
{formatTime(msg.timestamp)}
</span>
</div>
<div className="text-[#dbdee1] leading-[1.375rem] w-full markdown-content text-[0.95rem]">
<ReactMarkdown>
{msg.content}
</ReactMarkdown>
</div>
</div>
</div>
</div>
);
})}
{isLoading && (
<div className="flex items-start gap-4 mt-[1.0625rem] px-4 animate-fade-in">
<div className="w-10 h-10 rounded-full bg-[#404249] animate-pulse"></div>
<div className="space-y-2 pt-1 w-full max-w-lg">
<div className="h-4 bg-[#404249] rounded w-24 animate-pulse"></div>
<div className="h-4 bg-[#404249] rounded w-48 animate-pulse"></div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input Area */}
<div className="px-4 pb-6 pt-2 bg-[#313338]">
<div className="relative bg-[#383a40] rounded-lg flex items-center px-4 py-3 transition-colors focus-within:ring-1 focus-within:ring-[#00A8FC]">
<input
type="text"
className="bg-transparent flex-1 text-[#dbdee1] outline-none placeholder-[#949ba4] h-[24px] font-normal"
placeholder={`Message @${activeChannelName}`}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
disabled={isLoading}
autoFocus
/>
</div>
{isLoading && (
<div className="absolute bottom-1 left-4 text-[10px] font-bold text-[#dbdee1] animate-pulse">
Gemini AI is typing...
</div>
)}
</div>
{popoutUser && (
<MiniProfile
user={popoutUser}
position={popoutPos}
onClose={() => setPopoutUser(null)}
onOpenFullProfile={onOpenFullProfile}
/>
)}
</div>
);
};
export default ChatArea;

94
components/DMList.tsx Normal file
View File

@@ -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<DMListProps> = ({ activeChannelId, onSelectChannel }) => {
return (
<div className="w-60 bg-[#2b2d31] flex flex-col h-full hidden md:flex">
{/* Search / Find Button */}
<div className="h-12 shadow-sm flex items-center px-2.5 shadow-md z-10">
<button className="w-full text-left text-sm bg-[#1e1f22] text-[#949ba4] px-2 py-1.5 rounded-[4px] truncate">
Find or start a conversation
</button>
</div>
{/* List */}
<div className="flex-1 overflow-y-auto p-2 space-y-0.5 custom-scrollbar">
<div className="flex items-center justify-between px-2 pt-2 pb-1 text-[#949ba4] hover:text-[#dbdee1] cursor-pointer">
<span className="text-xs font-bold uppercase tracking-wide">Direct Messages</span>
<Plus size={14} />
</div>
{MOCK_CHANNELS.map((channel) => (
<div
key={channel.id}
onClick={() => 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]'
}`}
>
<div className="relative">
<img
src={channel.avatarUrl}
alt={channel.name}
className="w-8 h-8 rounded-full"
/>
{channel.status && (
<div className={`absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 border-[3px] border-[#2b2d31] rounded-full flex items-center justify-center
${channel.status === 'online' ? 'bg-[#23a559]' : ''}
${channel.status === 'idle' ? 'bg-[#f0b232]' : ''}
${channel.status === 'dnd' ? 'bg-[#f23f43]' : ''}
${channel.status === 'offline' ? 'bg-[#80848e]' : ''}
`}>
{channel.status === 'idle' && <div className="w-2 h-2 bg-[#2b2d31] rounded-full absolute -top-0.5 -left-0.5" />}
{channel.status === 'dnd' && <div className="w-1.5 h-0.5 bg-[#2b2d31] rounded-full" />}
</div>
)}
</div>
<div className="flex-1 truncate">
<span className={`font-medium ${activeChannelId === channel.id ? 'text-white' : 'text-[#dbdee1]'}`}>
{channel.name}
</span>
{channel.id === '1' && <span className="ml-1 text-[10px] bg-[#5865F2] text-white px-1 rounded-[3px] py-[1px]">BOT</span>}
</div>
<X size={14} className="opacity-0 group-hover:opacity-100 text-[#949ba4] hover:text-white" />
</div>
))}
</div>
{/* User Area */}
<div className="bg-[#232428] px-2 py-1.5 flex items-center justify-between">
<div className="flex items-center gap-2 group hover:bg-[#3f4147] p-1 rounded-md cursor-pointer -ml-1">
<div className="relative">
<img src="https://picsum.photos/seed/me/50/50" alt="Me" className="w-8 h-8 rounded-full" />
<div className="absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 bg-[#23a559] border-[3px] border-[#232428] rounded-full"></div>
</div>
<div className="text-xs">
<div className="font-semibold text-white">Current User</div>
<div className="text-[#dbdee1] opacity-60">#1234</div>
</div>
</div>
{/* Buttons */}
<div className="flex items-center">
{/* Minimal controls simulation */}
</div>
</div>
</div>
);
};
export default DMList;

View File

@@ -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<FullProfileModalProps> = ({ user, onClose }) => {
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 animate-fade-in" onClick={onClose}>
<div
className="w-[600px] rounded-2xl overflow-hidden shadow-2xl animate-scale-in relative"
style={{ background: user.theme || '#111214' }}
onClick={(e) => e.stopPropagation()} // Prevent closing when clicking inside
>
{/* Banner */}
<div
className="h-[210px] w-full relative bg-cover bg-center"
style={{
backgroundColor: user.color,
backgroundImage: user.bannerUrl ? `url(${user.bannerUrl})` : undefined
}}
>
{/* Close Button */}
<div className="absolute top-4 right-4 z-10">
<div
onClick={onClose}
className="w-8 h-8 bg-black/30 hover:bg-black/50 rounded-full flex items-center justify-center cursor-pointer text-white transition-colors backdrop-blur-sm"
>
<X size={18} />
</div>
</div>
</div>
<div className="relative px-6 pb-6">
{/* Avatar */}
<div className="absolute -top-[70px] left-6">
<div
className="w-[136px] h-[136px] rounded-full p-[8px] relative"
style={{ background: user.theme || '#111214' }}
>
<div className="w-full h-full bg-[#111214] rounded-full overflow-hidden relative">
<img src={user.avatarUrl} alt={user.name} className="w-full h-full rounded-full object-cover" />
</div>
<div className="absolute bottom-2 right-2 w-8 h-8 bg-[#23a559] border-[6px] border-[#111214] rounded-full z-20"></div>
</div>
</div>
{/* Top Badges */}
{user.isBot && (
<div className="flex justify-end pt-4 pb-2">
<div className="bg-[#232428]/50 rounded-lg p-1.5 flex gap-1.5 border border-[#1e1f22]/50 backdrop-blur-sm">
<StaffBadge />
<CertifiedBadge />
<BugHunterBadge />
</div>
</div>
)}
{!user.isBot && <div className="h-10"></div>}
{/* Info Card */}
<div className={`mt-8 rounded-2xl p-4 ${user.theme ? 'bg-[#232428]/60 backdrop-blur-md' : 'bg-[#232428]'}`}>
{/* Name Header */}
<div className="flex items-center gap-2 mb-1">
<span className="text-2xl font-bold text-white">{user.name}</span>
{user.isBot && <VerifiedBotBadge />}
</div>
<div className="text-base text-[#dbdee1] mb-5">{user.username}</div>
{/* Divider */}
<div className="h-[1px] bg-[#3f4147] w-full mb-4" />
{/* About Me */}
<div className="mb-6">
<div className="uppercase text-[12px] font-bold text-[#b5bac1] mb-2">About Me</div>
<div className="text-sm text-[#dbdee1] leading-relaxed">
{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."}
</div>
</div>
{/* Member Since */}
<div className="flex gap-12">
<div>
<div className="uppercase text-[12px] font-bold text-[#b5bac1] mb-1">Member Since</div>
<div className="text-sm text-[#dbdee1]">Feb 21, 2024</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default FullProfileModal;

112
components/MiniProfile.tsx Normal file
View File

@@ -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<MiniProfileProps> = ({ user, position, onClose, onOpenFullProfile }) => {
const ref = useRef<HTMLDivElement>(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 (
<div
ref={ref}
style={style}
className="fixed z-50 w-[300px] rounded-xl shadow-[0_8px_16px_rgba(0,0,0,0.24)] overflow-hidden animate-scale-in"
>
{/* Banner */}
<div
className="h-[60px] w-full relative bg-cover bg-center"
style={{
backgroundColor: user.color,
backgroundImage: user.bannerUrl ? `url(${user.bannerUrl})` : undefined
}}
/>
<div className="relative px-4 pb-4">
{/* Avatar */}
<div className="absolute -top-[30px] left-4 group cursor-pointer" onClick={handleAvatarClick}>
<div
className="w-[80px] h-[80px] rounded-full p-[6px] relative"
style={{ background: user.theme || '#232428' }}
>
<div className="w-full h-full bg-[#232428] rounded-full overflow-hidden relative">
<img src={user.avatarUrl} alt={user.name} className="w-full h-full rounded-full transition-opacity group-hover:opacity-80 object-cover" />
</div>
<div className="absolute top-0 left-0 w-full h-full bg-black/40 rounded-full opacity-0 group-hover:opacity-100 flex items-center justify-center text-[10px] text-white font-bold transition-opacity z-10">
VIEW PROFILE
</div>
<div className="absolute bottom-1 right-1 w-5 h-5 bg-[#23a559] border-[4px] border-[#232428] rounded-full z-20"></div>
</div>
</div>
{/* Badges */}
{user.isBot && (
<div className="flex justify-end pt-3 pb-1">
<div className="bg-[#111214]/50 rounded-lg p-1 flex gap-1 border border-[#1e1f22]/50 backdrop-blur-sm">
<StaffBadge />
<CertifiedBadge />
<BugHunterBadge />
</div>
</div>
)}
{!user.isBot && <div className="h-6"></div>}
{/* Info */}
<div className={`mt-8 rounded-lg p-3 pb-4 ${user.theme ? 'bg-[#111214]/60 border border-[#1e1f22]/50 backdrop-blur-md' : 'bg-[#111214]'}`}>
<div className="flex items-center flex-wrap gap-1">
<span className="text-xl font-bold text-white hover:underline cursor-pointer" onClick={handleAvatarClick}>{user.name}</span>
{user.isBot && <VerifiedBotBadge />}
</div>
<div className="text-sm text-[#dbdee1] mb-3">{user.username}</div>
<div className="h-[1px] bg-[#2e2f34] w-full mb-3" />
<div className="uppercase text-[11px] font-bold text-[#b5bac1] mb-1">About Me</div>
<div className="text-sm text-[#dbdee1]">
{user.isBot
? "I am a helpful AI assistant residing in your Discord DMs."
: "Just a human chatting with an AI."}
</div>
<div className="uppercase text-[11px] font-bold text-[#b5bac1] mt-3 mb-1">Member Since</div>
<div className="text-sm text-[#dbdee1]">Feb 21, 2024</div>
</div>
</div>
</div>
);
};
export default MiniProfile;

38
components/ServerRail.tsx Normal file
View File

@@ -0,0 +1,38 @@
import React from 'react';
import { Compass, Plus, Download, Home } from 'lucide-react';
const ServerRail: React.FC = () => {
return (
<div className="hidden sm:flex flex-col items-center w-[72px] bg-[#1e1f22] py-3 space-y-2 overflow-y-auto no-scrollbar">
{/* Home Icon */}
<div className="group relative flex items-center justify-center w-12 h-12 bg-[#5865F2] text-white rounded-[16px] cursor-pointer transition-all duration-200 hover:rounded-[16px] hover:bg-[#5865F2]">
<Home size={28} />
<div className="absolute left-0 w-1 h-8 bg-white rounded-r-full -ml-4" />
</div>
<div className="w-8 h-[2px] bg-[#35363C] rounded-lg mx-auto" />
{/* Mock Servers */}
{[...Array(3)].map((_, i) => (
<div key={i} className="group relative flex items-center justify-center w-12 h-12 bg-[#313338] hover:bg-[#5865F2] text-gray-100 hover:text-white rounded-[24px] hover:rounded-[16px] transition-all duration-200 cursor-pointer">
<img
src={`https://picsum.photos/seed/server${i}/50/50`}
alt="Server"
className="w-full h-full object-cover rounded-[inherit]"
/>
<div className="absolute left-0 w-1 h-2 bg-white rounded-r-full -ml-4 opacity-0 group-hover:opacity-100 group-hover:h-5 transition-all duration-200" />
</div>
))}
<div className="group relative flex items-center justify-center w-12 h-12 bg-[#313338] hover:bg-[#23a559] text-[#23a559] hover:text-white rounded-[24px] hover:rounded-[16px] transition-all duration-200 cursor-pointer">
<Plus size={24} />
</div>
<div className="group relative flex items-center justify-center w-12 h-12 bg-[#313338] hover:bg-[#23a559] text-[#23a559] hover:text-white rounded-[24px] hover:rounded-[16px] transition-all duration-200 cursor-pointer">
<Compass size={24} />
</div>
</div>
);
};
export default ServerRail;

View File

@@ -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<UserProfileSidebarProps> = ({ 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 (
<div
className="hidden lg:flex flex-col w-[340px] h-full border-l border-[#2e2f34] overflow-y-auto custom-scrollbar animate-fade-in"
style={{ background: geminiUser.theme }}
>
{/* Banner */}
<div
className="h-[120px] w-full relative bg-cover bg-center"
style={{
backgroundColor: geminiUser.color,
backgroundImage: `url(${geminiUser.bannerUrl})`
}}
>
{/* Pencil icon on hover could go here */}
</div>
{/* Profile Header */}
<div className="px-4 pb-3 mb-2 relative">
{/* Avatar */}
<div className="absolute -top-[44px] left-4" onClick={() => onOpenFullProfile(geminiUser)}>
<div className="w-[88px] h-[88px] rounded-full p-[6px] relative group cursor-pointer" style={{ background: geminiUser.theme }}>
<div className="w-full h-full bg-[#232428] rounded-full relative overflow-hidden">
<img
src={geminiUser.avatarUrl}
alt="Profile"
className="w-full h-full rounded-full transition-opacity group-hover:opacity-80 object-cover"
/>
</div>
<div className="absolute top-0 left-0 w-full h-full bg-black/40 rounded-full opacity-0 group-hover:opacity-100 flex items-center justify-center text-[10px] text-white font-bold transition-opacity z-10">
VIEW PROFILE
</div>
<div className="absolute bottom-1 right-1 w-6 h-6 bg-[#23a559] border-[5px] border-[#232428] rounded-full z-20"></div>
</div>
</div>
{/* Badges Container */}
<div className="flex justify-end pt-3 gap-1">
<div className="bg-[#111214]/50 rounded-lg p-1 flex gap-1 border border-[#1e1f22]/50 backdrop-blur-sm">
<StaffBadge />
<CertifiedBadge />
<BugHunterBadge />
</div>
</div>
{/* User Info */}
<div className="mt-2 bg-[#111214]/60 p-3 rounded-lg border border-[#1e1f22]/50 backdrop-blur-md">
<div className="flex items-center gap-1.5">
<span className="text-xl font-bold text-white cursor-pointer hover:underline" onClick={() => onOpenFullProfile(geminiUser)}>Gemini AI</span>
<VerifiedBotBadge />
</div>
<div className="text-sm text-[#dbdee1] mt-0.5">gemini_ai</div>
<div className="mt-3 border-t border-[#dbdee1]/20 pt-3">
<div className="uppercase text-[11px] font-bold text-[#dbdee1]/70 mb-1.5">About Me</div>
<div className="text-sm text-[#f2f3f5] leading-relaxed">
I am a large language model, trained by Google. I can help you with writing, learning, planning, and more.
</div>
</div>
<div className="mt-3">
<div className="uppercase text-[11px] font-bold text-[#dbdee1]/70 mb-1.5">Member Since</div>
<div className="text-sm text-[#f2f3f5]">Feb 21, 2024</div>
</div>
</div>
{/* Note */}
<div className="mt-4 pt-4 border-t border-[#dbdee1]/20">
<div className="uppercase text-[11px] font-bold text-[#dbdee1]/70 mb-2">Note</div>
<textarea
className="w-full bg-transparent text-xs text-[#dbdee1] placeholder-[#dbdee1]/50 resize-none focus:outline-none h-8"
placeholder="Click to add a note"
/>
</div>
</div>
</div>
);
};
export default UserProfileSidebar;

127
index.html Normal file
View File

@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Discord-Style AI Chat</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
background-color: #313338;
color: #dbdee1;
}
/* Custom Scrollbar to match Discord feel */
::-webkit-scrollbar {
width: 8px;
height: 8px;
background-color: #2b2d31;
}
::-webkit-scrollbar-thumb {
background-color: #1a1b1e;
border-radius: 4px;
}
::-webkit-scrollbar-track {
background-color: #2b2d31;
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scaleIn {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in {
animation: fadeIn 0.2s ease-out forwards;
}
.animate-scale-in {
animation: scaleIn 0.15s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
.animate-slide-up {
animation: slideUp 0.3s ease-out forwards;
}
/* Markdown Styles */
.markdown-content strong {
font-weight: 700;
color: #f2f3f5;
}
.markdown-content em {
font-style: italic;
}
.markdown-content u {
text-decoration: underline;
}
.markdown-content a {
color: #00A8FC;
text-decoration: none;
}
.markdown-content a:hover {
text-decoration: underline;
}
.markdown-content code {
background-color: #2b2d31;
padding: 0.2em 0.4em;
border-radius: 3px;
font-family: monospace;
font-size: 0.85em;
}
.markdown-content pre {
background-color: #2b2d31;
padding: 1em;
border-radius: 4px;
margin-top: 0.5em;
margin-bottom: 0.5em;
overflow-x: auto;
border: 1px solid #1e1f22;
}
.markdown-content pre code {
background-color: transparent;
padding: 0;
border-radius: 0;
color: #dbdee1;
font-size: 0.9em;
}
.markdown-content blockquote {
border-left: 4px solid #4e5058;
padding-left: 1em;
margin: 0.5em 0;
color: #949ba4;
}
.markdown-content ul {
list-style-type: disc;
padding-left: 1.5em;
margin: 0.5em 0;
}
.markdown-content ol {
list-style-type: decimal;
padding-left: 1.5em;
margin: 0.5em 0;
}
</style>
<script type="importmap">
{
"imports": {
"react": "https://aistudiocdn.com/react@^19.2.1",
"react-dom/": "https://aistudiocdn.com/react-dom@^19.2.1/",
"react/": "https://aistudiocdn.com/react@^19.2.1/",
"@google/genai": "https://aistudiocdn.com/@google/genai@^1.31.0",
"lucide-react": "https://aistudiocdn.com/lucide-react@^0.555.0",
"react-markdown": "https://aistudiocdn.com/react-markdown@^10.1.0"
}
}
</script>
</head>
<body>
<div id="root"></div>
</body>
</html>

15
index.tsx Normal file
View File

@@ -0,0 +1,15 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error("Could not find root element to mount to");
}
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

5
metadata.json Normal file
View File

@@ -0,0 +1,5 @@
{
"name": "Discord-Style AI Chat",
"description": "A modern, dark-themed chat interface inspired by Discord's DM UI, featuring a responsive layout and Gemini-powered AI responses.",
"requestFramePermissions": []
}

24
package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "discord-style-ai-chat",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.1",
"react-dom": "^19.2.1",
"@google/genai": "^1.31.0",
"lucide-react": "^0.555.0",
"react-markdown": "^10.1.0"
},
"devDependencies": {
"@types/node": "^22.14.0",
"@vitejs/plugin-react": "^5.0.0",
"typescript": "~5.8.2",
"vite": "^6.2.0"
}
}

42
services/geminiService.ts Normal file
View File

@@ -0,0 +1,42 @@
import { GoogleGenAI } from "@google/genai";
import { Message } from '../types';
let chatSession: any = null;
const getClient = () => {
const apiKey = process.env.API_KEY;
if (!apiKey) {
console.error("API Key not found");
return null;
}
return new GoogleGenAI({ apiKey });
};
export const sendMessageToGemini = async (
newMessage: string,
previousMessages: Message[]
): Promise<string> => {
const ai = getClient();
if (!ai) return "Error: API Key is missing. Please check your configuration.";
try {
// We maintain a simple local chat session if possible, or create new one
// Ideally, we'd persist the `chatSession` object, but for this stateless example
// we can re-hydrate somewhat or just use the persistent variable module-level.
if (!chatSession) {
chatSession = ai.chats.create({
model: 'gemini-2.5-flash',
config: {
systemInstruction: "You are a casual, friendly AI chat companion in a Discord-like interface. Keep your responses concise, conversational, and use markdown where appropriate (like bolding or code blocks). Do not be overly formal.",
},
});
}
const response = await chatSession.sendMessage({ message: newMessage });
return response.text;
} catch (error) {
console.error("Gemini API Error:", error);
return "I'm having trouble connecting to the mainframe right now. Try again later!";
}
};

29
tsconfig.json Normal file
View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"types": [
"node"
],
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}

23
types.ts Normal file
View File

@@ -0,0 +1,23 @@
export interface Message {
id: string;
role: 'user' | 'model';
content: string;
timestamp: Date;
senderName: string;
avatarUrl: string;
}
export interface User {
id: string;
name: string;
status: 'online' | 'idle' | 'dnd' | 'offline';
avatarUrl: string;
}
export interface Channel {
id: string;
name: string;
type: 'dm' | 'group';
status?: 'online' | 'idle' | 'dnd' | 'offline';
avatarUrl?: string;
}

23
vite.config.ts Normal file
View File

@@ -0,0 +1,23 @@
import path from 'path';
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, '.', '');
return {
server: {
port: 3000,
host: '0.0.0.0',
},
plugins: [react()],
define: {
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
},
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
}
}
};
});