mirror of
https://github.com/m4rcel-lol/custom-discord-ai-chatbot-site.git
synced 2025-12-06 19:03:58 +05:30
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:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
55
App.tsx
Normal 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;
|
||||||
25
README.md
25
README.md
@@ -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
41
components/Badges.tsx
Normal 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
256
components/ChatArea.tsx
Normal 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
94
components/DMList.tsx
Normal 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;
|
||||||
108
components/FullProfileModal.tsx
Normal file
108
components/FullProfileModal.tsx
Normal 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
112
components/MiniProfile.tsx
Normal 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
38
components/ServerRail.tsx
Normal 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;
|
||||||
97
components/UserProfileSidebar.tsx
Normal file
97
components/UserProfileSidebar.tsx
Normal 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
127
index.html
Normal 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
15
index.tsx
Normal 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
5
metadata.json
Normal 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
24
package.json
Normal 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
42
services/geminiService.ts
Normal 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
29
tsconfig.json
Normal 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
23
types.ts
Normal 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
23
vite.config.ts
Normal 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, '.'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user