import React, { useEffect, useMemo, useRef, useState } from "react"; import { Search, Hash, Plus, Send, Sun, Moon, Users, MessageSquare, Bot, Sparkles, X, Settings, ShieldCheck, Compass, Reply, ChevronRight, } from "lucide-react"; /** * AICORD — single-user, Discord-style AI sandbox * ------------------------------------------------ * - Everything (servers, channels, users, messages) is procedurally generated. * - No backend, no auth, no real people. Just you and bots. * - Features: * • Servers + channels, DM drawer * • Server discovery (join/leave/open) * • Profiles (click avatars/members) * • User settings (name, status, bio, theme) * • Search (servers/channels/users/messages, Cmd/Ctrl-K) * • Reactions + “reply” button (cosmetic) */ // ----------------------------- Utilities ---------------------------------- const clamp = (n, a, b) => Math.max(a, Math.min(b, n)); // Lightweight seeded RNG (xorshift32) function makeRNG(seed) { let x = seed >>> 0 || 123456789; return function rand() { x ^= x << 13; x ^= x >>> 17; x ^= x << 5; return (x >>> 0) / 4294967296; }; } function pick(rng, arr) { return arr[Math.floor(rng() * arr.length)]; } function chance(rng, p = 0.5) { return rng() < p; } // Silly name & text generators const FIRST = [ "Nova", "Rex", "Mira", "Orion", "Ada", "Zeph", "Quill", "Echo", "Zara", "Kai", "Lumi", "Rune", "Iris", "Sol", "Nyx", "Vega", "Juno", "Aria", "Atlas", "Rhea", "Bex", "Pax", "Noa", "Rune", "Sage", ]; const LAST = [ "Skylark", "Kepler", "Hawthorne", "Cobalt", "Faraday", "Quasar", "Sterling", "Starling", "Wilde", "Ash", "Kestrel", "Onyx", "Hearth", "Vale", "North", "Blake", "Rook", "Dune", "Voss", "Solis", "Cade", "Wren", ]; const SERVER_TOPICS = [ "Neural Brew Co.", "Dream-Synth Lab", "Pixel Parlor", "Prompt Garden", "Logic Loom", "Circuit Circus", "Vector Valley", "Glyph Guild", "Story Forge", "Audio Arcade", "Retro Robotics", "Quantum Café", "Byte Bazaar", "Orbital Orchard", "Shader Sanctuary", ]; const CHANNELS = [ "general", "prompts", "showcase", "bot-lab", "memes", "music", "off-topic", "ask-anything", "help", "work-in-progress", ]; const EMOTES = ["✨", "🤖", "🧠", "⚙️", "🎛️", "🎨", "🛰️", "🧩", "🦾", "🧪", "💡", "🧷", "📎"]; const TOPICS = [ "text-generation", "image diffusion", "RLHF", "agents", "tool-use", "embeddings", "vector DBs", "audio LLMs", "synthetic data", "prompt engineering", "safety", ]; const OPINIONS = [ "hot take: few-shot beats fine-tune here", "today I learned: temperature≠creativity", "long prompts are just configs with lore", "remember: evals before vibes", "I ship everything behind a feature flag", "prefer cosine to dot for retrieval", "stop putting secrets in prompts", "agents aren’t interns, they’re workflows", "small model + good data > huge model + junk", ]; function sentence(rng) { const who = `${pick(rng, FIRST)} ${pick(rng, LAST)}`; const emo = pick(rng, EMOTES); const t = pick(rng, TOPICS); const op = pick(rng, OPINIONS); const filler = [ `spun up a mini-agent for ${t}`, `benching variants; ${op}`, `shipping a demo ${emo}`, `trying to reproduce a paper on ${t}`, `broke prod (again) ${emo}`, `writing a guide on ${t}`, `pairing with a bot on codegen ${emo}`, ]; return `${who}: ${pick(rng, filler)}`; } function loremMsg(rng) { const n = 1 + Math.floor(rng() * 3); return Array.from({ length: n }, () => sentence(rng)).join("\n"); } function avatar(seedStr) { // Simple deterministic gradient avatar based on a hash let h = 0; for (let i = 0; i < seedStr.length; i++) h = Math.imul(31, h) + seedStr.charCodeAt(i) | 0; const hue = Math.abs(h) % 360; return `linear-gradient(135deg,hsl(${hue} 70% 55%),hsl(${(hue + 60) % 360} 70% 45%))`; } // ----------------------------- World Model --------------------------------- function makeUser(rng, id) { const name = `${pick(rng, FIRST)} ${pick(rng, LAST)}`; const handle = `@${name.toLowerCase().replace(/[^a-z]/g, "")}${Math.floor( rng() * 999 )}`; const bio = `Into ${pick(rng, TOPICS)}, ${pick( rng, TOPICS )}, and ${pick(rng, TOPICS)}. ${pick(rng, OPINIONS)}.`; return { id: `u${id}`, name, handle, bio }; } function makeServer(rng, id) { const title = pick(rng, SERVER_TOPICS); const channels = CHANNELS.map((c, i) => ({ id: `c${id}-${i}`, name: c, topic: pick(rng, TOPICS), })); const members = Array.from( { length: 12 + Math.floor(rng() * 30) }, (_, i) => makeUser(rng, `${id}-${i}`) ); return { id: `s${id}`, title, channels, members }; } function generateWorld(seed = 42) { const rng = makeRNG(seed); const servers = Array.from({ length: 10 }, (_, i) => makeServer(rng, i + 1)); const users = Array.from({ length: 40 }, (_, i) => makeUser(rng, i + 100)); const dms = users.slice(0, 18).map((u, i) => ({ id: `d${i}`, peer: u, messages: Array.from( { length: 8 + Math.floor(rng() * 8) }, () => ({ id: crypto.randomUUID(), from: u, text: loremMsg(rng), ts: Date.now() - Math.floor(rng() * 1e8), }) ), })); return { seed, servers, users, dms }; } // ----------------------------- Search -------------------------------------- function fuseLikeScore(q, text) { if (!q) return 0; const nq = q.trim().toLowerCase(); const nt = text.toLowerCase(); if (nt.includes(nq)) return 100 + nq.length; // token overlap const qTokens = new Set(nq.split(/\s+/)); const tTokens = new Set(nt.split(/\s+/)); let overlap = 0; qTokens.forEach((t) => { if (t && tTokens.has(t)) overlap++; }); return overlap * 10; } function searchWorld(world, q) { if (!q) return { servers: [], channels: [], users: [], messages: [] }; const servers = world.servers .map((s) => ({ item: s, score: Math.max( fuseLikeScore(q, s.title), s.channels.reduce( (m, ch) => Math.max(m, fuseLikeScore(q, ch.name + " " + ch.topic)), 0 ) ), })) .filter(({ score }) => score > 0) .sort((a, b) => b.score - a.score) .slice(0, 7); const channels = world.servers .flatMap((s) => s.channels.map((ch) => ({ item: { ...ch, serverId: s.id, serverTitle: s.title }, score: fuseLikeScore(q, `${ch.name} ${ch.topic} ${s.title}`), })) ) .filter((r) => r.score > 0) .sort((a, b) => b.score - a.score) .slice(0, 10); const users = [...world.users, ...world.servers.flatMap((s) => s.members)] .map((u) => ({ item: u, score: Math.max( fuseLikeScore(q, u.name), fuseLikeScore(q, u.handle), fuseLikeScore(q, u.bio) ), })) .filter((r) => r.score > 0) .sort((a, b) => b.score - a.score) .slice(0, 10); const rng = makeRNG(world.seed + 999); const messages = Array.from({ length: 60 }, () => ({ id: crypto.randomUUID(), from: pick(rng, world.users), text: loremMsg(rng), location: chance(rng, 0.6) ? { type: "channel", serverTitle: pick(rng, world.servers).title, channel: pick(rng, CHANNELS), } : { type: "dm", peer: pick(rng, world.users).name }, ts: Date.now() - Math.floor(rng() * 1e9), })) .map((m) => ({ item: m, score: fuseLikeScore( q, `${m.from.name} ${m.text} ${ m.location.type === "channel" ? m.location.serverTitle + " #" + m.location.channel : m.location.peer }` ), })) .filter((r) => r.score > 40) .sort((a, b) => b.score - a.score) .slice(0, 12); return { servers, channels, users, messages }; } // ----------------------------- UI Bits ------------------------------------- function Badge({ children }) { return ( {children} ); } function Modal({ open, onClose, children, className = "" }) { if (!open) return null; return (
e.stopPropagation()} >
{children}
); } function RailButton({ label, active, onClick }) { return ( ); } function MemberPill({ user, onOpenProfile }) { return ( ); } function ChannelRow({ channel, active, onClick }) { return ( ); } function MessageBubble({ m, me = false, onReact, onThread, onOpenProfile }) { return (
); } function ProfileCard({ user }) { if (!user) return null; return (
{user.name}
{user.handle}
{user.bio}
AI native Beta
); } function SettingsPanel({ meName, setMeName, dark, setDark, bio, setBio, status, setStatus, }) { return (
User Settings
Profile
setMeName(e.target.value)} className="w-full rounded-lg border border-neutral-700 bg-neutral-900 px-2 py-1 text-sm outline-none" /> setStatus(e.target.value)} placeholder="Online, Busy, Lurking…" className="w-full rounded-lg border border-neutral-700 bg-neutral-900 px-2 py-1 text-sm outline-none" />