Add AI chatbot and theme toggle functionality for improved user experience

Integrates a new AI chatbot using OpenAI, adds a theme toggle for light/dark modes, and includes a live activity feed for administrators.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 279f1558-c0e3-40e4-8217-be7e9f4c6eca
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: c19480f5-d8f1-4a0b-98ea-7bfe9144b25d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/b984cb14-1d19-4944-922b-bc79e821ed35/279f1558-c0e3-40e4-8217-be7e9f4c6eca/yVjwaR4
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sirpiglr 2025-12-16 00:00:42 +00:00
parent 38bc0c8663
commit 9901ea3e2d
14 changed files with 966 additions and 1 deletions

View file

@ -41,3 +41,4 @@ waitForPort = 5000
[agent]
mockupState = "FULLSTACK"
integrations = ["javascript_openai_ai_integrations:1.0.0"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -21,6 +21,9 @@ import AdminSites from "@/pages/admin-sites";
import AdminLogs from "@/pages/admin-logs";
import AdminAchievements from "@/pages/admin-achievements";
import AdminApplications from "@/pages/admin-applications";
import AdminActivity from "@/pages/admin-activity";
import AdminNotifications from "@/pages/admin-notifications";
import { Chatbot } from "@/components/Chatbot";
function Router() {
return (
@ -40,6 +43,8 @@ function Router() {
<Route path="/admin/logs" component={AdminLogs} />
<Route path="/admin/achievements" component={AdminAchievements} />
<Route path="/admin/applications" component={AdminApplications} />
<Route path="/admin/activity" component={AdminActivity} />
<Route path="/admin/notifications" component={AdminNotifications} />
<Route path="/pitch" component={Pitch} />
<Route component={NotFound} />
</Switch>
@ -53,6 +58,7 @@ function App() {
<TutorialProvider>
<Toaster />
<Router />
<Chatbot />
</TutorialProvider>
</AuthProvider>
</QueryClientProvider>

View file

@ -0,0 +1,210 @@
import { useState, useRef, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { MessageCircle, X, Send, Bot, User, Loader2 } from "lucide-react";
interface Message {
id: string;
role: "user" | "assistant";
content: string;
timestamp: Date;
}
export function Chatbot() {
const [isOpen, setIsOpen] = useState(false);
const [messages, setMessages] = useState<Message[]>([
{
id: "welcome",
role: "assistant",
content: "Hi! I'm the AeThex assistant. I can help you navigate the platform, explain our certification system, or answer questions about the ecosystem. How can I help you today?",
timestamp: new Date(),
},
]);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
const sendMessage = async () => {
if (!input.trim() || isLoading) return;
const userMessage: Message = {
id: Date.now().toString(),
role: "user",
content: input.trim(),
timestamp: new Date(),
};
setMessages((prev) => [...prev, userMessage]);
setInput("");
setIsLoading(true);
try {
const conversationHistory = messages.slice(-10).map(m => ({
role: m.role,
content: m.content
}));
const response = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
message: userMessage.content,
history: conversationHistory
}),
});
if (!response.ok) throw new Error("Failed to get response");
const data = await response.json();
const assistantMessage: Message = {
id: (Date.now() + 1).toString(),
role: "assistant",
content: data.response || "I apologize, but I'm having trouble responding right now. Please try again.",
timestamp: new Date(),
};
setMessages((prev) => [...prev, assistantMessage]);
} catch (error) {
const errorMessage: Message = {
id: (Date.now() + 1).toString(),
role: "assistant",
content: "I'm sorry, I encountered an error. Please try again in a moment.",
timestamp: new Date(),
};
setMessages((prev) => [...prev, errorMessage]);
} finally {
setIsLoading(false);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};
return (
<>
{/* Chat Button */}
<AnimatePresence>
{!isOpen && (
<motion.button
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0, opacity: 0 }}
onClick={() => setIsOpen(true)}
className="fixed bottom-6 left-6 z-50 bg-secondary text-background p-4 rounded-full shadow-lg hover:bg-secondary/90 transition-colors"
data-testid="button-open-chatbot"
>
<MessageCircle className="w-6 h-6" />
</motion.button>
)}
</AnimatePresence>
{/* Chat Window */}
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.95 }}
className="fixed bottom-6 left-6 z-50 w-96 max-w-[calc(100vw-3rem)] h-[500px] max-h-[70vh] bg-card border border-white/10 shadow-2xl flex flex-col"
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-white/10">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-secondary/20 rounded-full flex items-center justify-center">
<Bot className="w-4 h-4 text-secondary" />
</div>
<div>
<div className="text-sm font-bold text-white">AeThex Assistant</div>
<div className="text-xs text-green-500 flex items-center gap-1">
<span className="w-1.5 h-1.5 bg-green-500 rounded-full" /> Online
</div>
</div>
</div>
<button
onClick={() => setIsOpen(false)}
className="text-muted-foreground hover:text-white transition-colors"
data-testid="button-close-chatbot"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((message) => (
<div
key={message.id}
className={`flex gap-3 ${message.role === "user" ? "flex-row-reverse" : ""}`}
>
<div
className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
message.role === "user" ? "bg-primary/20" : "bg-secondary/20"
}`}
>
{message.role === "user" ? (
<User className="w-4 h-4 text-primary" />
) : (
<Bot className="w-4 h-4 text-secondary" />
)}
</div>
<div
className={`max-w-[75%] p-3 text-sm ${
message.role === "user"
? "bg-primary/10 text-white border border-primary/30"
: "bg-white/5 text-muted-foreground border border-white/10"
}`}
>
{message.content}
</div>
</div>
))}
{isLoading && (
<div className="flex gap-3">
<div className="w-8 h-8 bg-secondary/20 rounded-full flex items-center justify-center">
<Bot className="w-4 h-4 text-secondary" />
</div>
<div className="bg-white/5 border border-white/10 p-3 text-sm text-muted-foreground">
<Loader2 className="w-4 h-4 animate-spin" />
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<div className="p-4 border-t border-white/10">
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Ask me anything..."
className="flex-1 bg-black/20 border border-white/10 px-4 py-2 text-sm text-white placeholder:text-muted-foreground focus:outline-none focus:border-secondary/50"
data-testid="input-chat-message"
/>
<button
onClick={sendMessage}
disabled={!input.trim() || isLoading}
className="bg-secondary text-background px-4 py-2 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-secondary/90 transition-colors"
data-testid="button-send-message"
>
<Send className="w-4 h-4" />
</button>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</>
);
}

View file

@ -0,0 +1,58 @@
import { useState, useEffect } from "react";
import { Moon, Sun } from "lucide-react";
export function useTheme() {
const [theme, setTheme] = useState<"light" | "dark">("dark");
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
if (typeof window !== "undefined") {
const stored = localStorage.getItem("aethex_theme");
if (stored === "light" || stored === "dark") {
setTheme(stored);
}
}
}, []);
useEffect(() => {
if (!mounted) return;
const root = document.documentElement;
if (theme === "light") {
root.classList.remove("dark");
root.classList.add("light");
} else {
root.classList.remove("light");
root.classList.add("dark");
}
if (typeof window !== "undefined") {
localStorage.setItem("aethex_theme", theme);
}
}, [theme, mounted]);
const toggleTheme = () => {
setTheme((prev) => (prev === "dark" ? "light" : "dark"));
};
return { theme, setTheme, toggleTheme };
}
export function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
className="p-2 rounded-lg border border-white/10 bg-card/50 hover:bg-card transition-colors"
data-testid="button-theme-toggle"
aria-label={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
>
{theme === "dark" ? (
<Sun className="w-4 h-4 text-primary" />
) : (
<Moon className="w-4 h-4 text-primary" />
)}
</button>
);
}

View file

@ -96,6 +96,43 @@
--chart-5: 0 100% 50%;
}
/* LIGHT MODE - AE-THEX THEME */
.light {
--background: 0 0% 98%;
--foreground: 240 10% 10%;
--card: 0 0% 100%;
--card-foreground: 240 10% 10%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 10%;
--primary: 45 100% 40%;
--primary-foreground: 0 0% 100%;
--secondary: 180 80% 35%;
--secondary-foreground: 0 0% 100%;
--muted: 240 5% 90%;
--muted-foreground: 240 5% 40%;
--accent: 200 70% 90%;
--accent-foreground: 200 80% 25%;
--destructive: 0 80% 50%;
--destructive-foreground: 0 0% 100%;
--border: 240 5% 85%;
--input: 240 5% 85%;
--ring: 45 100% 40%;
--chart-1: 45 100% 45%;
--chart-2: 180 80% 45%;
--chart-3: 200 80% 45%;
--chart-4: 280 80% 45%;
--chart-5: 0 80% 45%;
}
@layer base {
* {
@apply border-border;

View file

@ -0,0 +1,277 @@
import { useEffect, useState } from "react";
import { Link, useLocation } from "wouter";
import { useQuery } from "@tanstack/react-query";
import { useAuth } from "@/lib/auth";
import {
Users, FileCode, Shield, Activity, LogOut,
BarChart3, User, Globe, Award, Key, Inbox,
Circle, Clock, TrendingUp, Wifi
} from "lucide-react";
interface ActivityEvent {
id: string;
type: string;
user: string;
action: string;
timestamp: Date;
details?: string;
}
export default function AdminActivity() {
const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth();
const [, setLocation] = useLocation();
const [liveEvents, setLiveEvents] = useState<ActivityEvent[]>([]);
const [lastRefresh, setLastRefresh] = useState(new Date());
const [seenEventIds, setSeenEventIds] = useState<Set<string>>(new Set());
useEffect(() => {
if (!authLoading && !isAuthenticated) {
setLocation("/login");
}
}, [authLoading, isAuthenticated, setLocation]);
const { data: profiles } = useQuery({
queryKey: ["profiles"],
queryFn: async () => {
const res = await fetch("/api/profiles", { credentials: "include" });
if (!res.ok) return [];
return res.json();
},
enabled: isAuthenticated,
refetchInterval: 10000,
});
const { data: authLogs } = useQuery({
queryKey: ["auth-logs"],
queryFn: async () => {
const res = await fetch("/api/auth-logs", { credentials: "include" });
if (!res.ok) return [];
return res.json();
},
enabled: isAuthenticated,
refetchInterval: 5000,
});
const { data: metrics } = useQuery({
queryKey: ["metrics"],
queryFn: async () => {
const res = await fetch("/api/metrics", { credentials: "include" });
return res.json();
},
refetchInterval: 5000,
});
useEffect(() => {
if (authLogs && authLogs.length > 0) {
const newEvents: ActivityEvent[] = [];
const newIds = new Set(seenEventIds);
for (const log of authLogs.slice(0, 30)) {
const eventId = log.id || `log-${log.created_at}`;
if (!newIds.has(eventId)) {
newIds.add(eventId);
newEvents.push({
id: eventId,
type: log.event_type?.includes('success') ? 'login' : log.event_type?.includes('fail') ? 'failed' : 'auth',
user: log.username || log.user_id || 'Unknown',
action: log.event_type || 'Activity',
timestamp: new Date(log.created_at),
details: log.ip_address,
});
}
}
if (newEvents.length > 0) {
setLiveEvents(prev => [...newEvents, ...prev].slice(0, 50));
setSeenEventIds(newIds);
}
setLastRefresh(new Date());
}
}, [authLogs]);
if (authLoading || !isAuthenticated) {
return (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-primary animate-pulse">Loading...</div>
</div>
);
}
const handleLogout = async () => {
await logout();
setLocation("/");
};
const onlineProfiles = profiles?.filter((p: any) => p.status === 'online') || [];
const recentlyActive = profiles?.filter((p: any) => {
if (!p.last_seen) return false;
const lastSeen = new Date(p.last_seen);
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
return lastSeen > fiveMinutesAgo;
}) || [];
return (
<div className="min-h-screen bg-background text-foreground font-mono flex">
<Sidebar user={user} onLogout={handleLogout} active="activity" />
<div className="flex-1 overflow-auto">
<div className="p-8">
<div className="flex justify-between items-start mb-8">
<div>
<h2 className="text-2xl font-display font-bold text-white uppercase tracking-wider">
Real-Time Activity
</h2>
<p className="text-muted-foreground text-sm mt-1">
Live monitoring of user activity across the platform
</p>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Clock className="w-3 h-3" />
Last updated: {lastRefresh.toLocaleTimeString()}
</div>
<div className="flex items-center gap-2 bg-green-500/10 text-green-500 px-3 py-1 text-xs">
<Wifi className="w-3 h-3 animate-pulse" />
Live
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="bg-card/50 border border-white/10 p-6">
<div className="flex items-center justify-between mb-2">
<div className="text-xs text-muted-foreground uppercase tracking-wider">Online Now</div>
<Circle className="w-3 h-3 text-green-500 fill-green-500 animate-pulse" />
</div>
<div className="text-4xl font-display font-bold text-green-500">{metrics?.onlineUsers || 0}</div>
<div className="text-xs text-muted-foreground mt-1">Active users</div>
</div>
<div className="bg-card/50 border border-white/10 p-6">
<div className="text-xs text-muted-foreground uppercase tracking-wider mb-2">Total Architects</div>
<div className="text-4xl font-display font-bold text-primary">{metrics?.totalProfiles || 0}</div>
<div className="text-xs text-muted-foreground mt-1">Registered</div>
</div>
<div className="bg-card/50 border border-white/10 p-6">
<div className="text-xs text-muted-foreground uppercase tracking-wider mb-2">Auth Events</div>
<div className="text-4xl font-display font-bold text-white">{authLogs?.length || 0}</div>
<div className="text-xs text-muted-foreground mt-1">Last 100</div>
</div>
<div className="bg-card/50 border border-white/10 p-6">
<div className="text-xs text-muted-foreground uppercase tracking-wider mb-2">Verified Users</div>
<div className="text-4xl font-display font-bold text-secondary">{metrics?.verifiedUsers || 0}</div>
<div className="text-xs text-muted-foreground mt-1">Certified</div>
</div>
</div>
<div className="grid md:grid-cols-2 gap-8">
<div className="bg-card/50 border border-white/10 p-6">
<h3 className="text-sm font-bold text-white uppercase tracking-widest mb-6 flex items-center gap-2">
<Circle className="w-3 h-3 text-green-500 fill-green-500" />
Online Users ({onlineProfiles.length})
</h3>
<div className="space-y-3 max-h-80 overflow-auto">
{onlineProfiles.length === 0 ? (
<div className="text-sm text-muted-foreground text-center py-8">No users currently online</div>
) : (
onlineProfiles.map((profile: any) => (
<div key={profile.id} className="flex items-center justify-between p-3 bg-black/20 border border-white/5">
<div className="flex items-center gap-3">
<div className="relative">
<div className="w-8 h-8 bg-primary/20 rounded-full flex items-center justify-center">
<User className="w-4 h-4 text-primary" />
</div>
<div className="absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-green-500 rounded-full border-2 border-card" />
</div>
<div>
<div className="text-sm text-white font-bold">{profile.username || profile.display_name}</div>
<div className="text-xs text-muted-foreground">Level {profile.level || 1}</div>
</div>
</div>
<div className="text-xs text-green-500">Online</div>
</div>
))
)}
</div>
</div>
<div className="bg-card/50 border border-white/10 p-6">
<h3 className="text-sm font-bold text-white uppercase tracking-widest mb-6 flex items-center gap-2">
<Activity className="w-4 h-4 text-primary" />
Live Activity Feed
</h3>
<div className="space-y-2 max-h-80 overflow-auto">
{liveEvents.length === 0 ? (
<div className="text-sm text-muted-foreground text-center py-8">No recent activity</div>
) : (
liveEvents.map((event) => (
<div key={event.id} className="flex items-center gap-3 p-2 bg-black/20 border border-white/5 text-xs">
<div className={`w-2 h-2 rounded-full ${
event.type === 'login' ? 'bg-green-500' :
event.type === 'failed' ? 'bg-destructive' :
'bg-blue-500'
}`} />
<div className="flex-1">
<span className="text-white font-bold">{event.user}</span>
<span className="text-muted-foreground"> {event.action}</span>
</div>
<div className="text-muted-foreground">
{event.timestamp.toLocaleTimeString()}
</div>
</div>
))
)}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
function Sidebar({ user, onLogout, active }: { user: any; onLogout: () => void; active: string }) {
return (
<div className="w-64 bg-card border-r border-white/10 flex flex-col">
<div className="p-6 border-b border-white/10">
<h1 className="text-xl font-display font-bold text-white uppercase tracking-wider">AeThex</h1>
<p className="text-xs text-primary mt-1">Command Center</p>
</div>
<nav className="flex-1 p-4 space-y-2">
<NavItem icon={<BarChart3 className="w-4 h-4" />} label="Dashboard" href="/admin" active={active === 'dashboard'} />
<NavItem icon={<Activity className="w-4 h-4" />} label="Live Activity" href="/admin/activity" active={active === 'activity'} />
<NavItem icon={<Users className="w-4 h-4" />} label="Architects" href="/admin/architects" active={active === 'architects'} />
<NavItem icon={<Inbox className="w-4 h-4" />} label="Applications" href="/admin/applications" active={active === 'applications'} />
<NavItem icon={<Award className="w-4 h-4" />} label="Achievements" href="/admin/achievements" active={active === 'achievements'} />
<NavItem icon={<FileCode className="w-4 h-4" />} label="Credentials" href="/admin/credentials" active={active === 'credentials'} />
<NavItem icon={<Globe className="w-4 h-4" />} label="Sites" href="/admin/sites" active={active === 'sites'} />
<NavItem icon={<Key className="w-4 h-4" />} label="Auth Logs" href="/admin/logs" active={active === 'logs'} />
<NavItem icon={<Shield className="w-4 h-4" />} label="Aegis Monitor" href="/admin/aegis" active={active === 'aegis'} />
</nav>
<div className="p-4 border-t border-white/10">
<div className="flex items-center gap-3 mb-4">
<div className="w-8 h-8 bg-primary/20 rounded-full flex items-center justify-center">
<User className="w-4 h-4 text-primary" />
</div>
<div>
<div className="text-sm text-white font-bold">{user?.username}</div>
<div className="text-xs text-muted-foreground">{user?.isAdmin ? "Administrator" : "Member"}</div>
</div>
</div>
<button onClick={onLogout} className="w-full flex items-center gap-2 text-muted-foreground hover:text-white text-sm py-2 px-3 hover:bg-white/5 transition-colors">
<LogOut className="w-4 h-4" /> Sign Out
</button>
</div>
</div>
);
}
function NavItem({ icon, label, href, active = false }: { icon: React.ReactNode; label: string; href: string; active?: boolean }) {
return (
<Link href={href}>
<div className={`flex items-center gap-3 px-3 py-2 text-sm transition-colors cursor-pointer ${active ? 'bg-primary/10 text-primary border-l-2 border-primary' : 'text-muted-foreground hover:text-white hover:bg-white/5'}`}>
{icon}
{label}
</div>
</Link>
);
}

View file

@ -0,0 +1,251 @@
import { useEffect, useState } from "react";
import { Link, useLocation } from "wouter";
import { useAuth } from "@/lib/auth";
import {
Users, FileCode, Shield, Activity, LogOut,
BarChart3, User, Globe, Award, Key, Inbox,
Bell, Mail, AlertTriangle, CheckCircle, Settings
} from "lucide-react";
interface NotificationSetting {
id: string;
name: string;
description: string;
enabled: boolean;
category: "security" | "users" | "system";
}
const defaultSettings: NotificationSetting[] = [
{ id: "failed_logins", name: "Failed Login Attempts", description: "Alert when multiple failed login attempts are detected", enabled: true, category: "security" },
{ id: "new_user", name: "New User Registration", description: "Notify when a new architect joins the platform", enabled: true, category: "users" },
{ id: "critical_alert", name: "Critical Security Alerts", description: "Immediate notification for high-severity security events", enabled: true, category: "security" },
{ id: "verification", name: "Verification Requests", description: "Alert when an architect requests credential verification", enabled: false, category: "users" },
{ id: "system_down", name: "System Downtime", description: "Notify when any AeThex service goes offline", enabled: true, category: "system" },
{ id: "weekly_report", name: "Weekly Summary Report", description: "Receive a weekly digest of platform activity", enabled: false, category: "system" },
{ id: "application", name: "New Applications", description: "Alert when new job applications are submitted", enabled: true, category: "users" },
{ id: "threat_detected", name: "Threat Detection", description: "Real-time alerts from Aegis threat monitoring", enabled: true, category: "security" },
];
export default function AdminNotifications() {
const { user, isAuthenticated, isLoading: authLoading, logout } = useAuth();
const [, setLocation] = useLocation();
const [settings, setSettings] = useState<NotificationSetting[]>(() => {
if (typeof window !== "undefined") {
const saved = localStorage.getItem("aethex_notification_settings");
if (saved) return JSON.parse(saved);
}
return defaultSettings;
});
const [email, setEmail] = useState(() => {
if (typeof window !== "undefined") {
return localStorage.getItem("aethex_notification_email") || "";
}
return "";
});
const [saved, setSaved] = useState(false);
useEffect(() => {
if (!authLoading && !isAuthenticated) {
setLocation("/login");
}
}, [authLoading, isAuthenticated, setLocation]);
const toggleSetting = (id: string) => {
setSettings((prev) =>
prev.map((s) => (s.id === id ? { ...s, enabled: !s.enabled } : s))
);
setSaved(false);
};
const saveSettings = () => {
localStorage.setItem("aethex_notification_settings", JSON.stringify(settings));
localStorage.setItem("aethex_notification_email", email);
setSaved(true);
setTimeout(() => setSaved(false), 3000);
};
if (authLoading || !isAuthenticated) {
return (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-primary animate-pulse">Loading...</div>
</div>
);
}
const handleLogout = async () => {
await logout();
setLocation("/");
};
const groupedSettings = {
security: settings.filter((s) => s.category === "security"),
users: settings.filter((s) => s.category === "users"),
system: settings.filter((s) => s.category === "system"),
};
return (
<div className="min-h-screen bg-background text-foreground font-mono flex">
<Sidebar user={user} onLogout={handleLogout} active="notifications" />
<div className="flex-1 overflow-auto">
<div className="p-8 max-w-4xl">
<div className="mb-8">
<h2 className="text-2xl font-display font-bold text-white uppercase tracking-wider">
Notification Settings
</h2>
<p className="text-muted-foreground text-sm mt-1">
Configure email alerts for critical system events
</p>
</div>
<div className="bg-card/50 border border-white/10 p-6 mb-8">
<h3 className="text-sm font-bold text-white uppercase tracking-widest mb-4 flex items-center gap-2">
<Mail className="w-4 h-4 text-primary" />
Email Configuration
</h3>
<div className="space-y-4">
<div>
<label className="text-xs text-muted-foreground uppercase tracking-wider block mb-2">
Notification Email Address
</label>
<input
type="email"
value={email}
onChange={(e) => { setEmail(e.target.value); setSaved(false); }}
placeholder="admin@example.com"
className="w-full max-w-md bg-black/20 border border-white/10 px-4 py-3 text-white placeholder:text-muted-foreground focus:outline-none focus:border-primary/50"
data-testid="input-notification-email"
/>
</div>
</div>
</div>
<div className="space-y-8">
<div className="bg-card/50 border border-white/10 p-6">
<h3 className="text-sm font-bold text-white uppercase tracking-widest mb-6 flex items-center gap-2">
<Shield className="w-4 h-4 text-destructive" />
Security Alerts
</h3>
<div className="space-y-4">
{groupedSettings.security.map((setting) => (
<SettingRow key={setting.id} setting={setting} onToggle={toggleSetting} />
))}
</div>
</div>
<div className="bg-card/50 border border-white/10 p-6">
<h3 className="text-sm font-bold text-white uppercase tracking-widest mb-6 flex items-center gap-2">
<Users className="w-4 h-4 text-secondary" />
User Activity
</h3>
<div className="space-y-4">
{groupedSettings.users.map((setting) => (
<SettingRow key={setting.id} setting={setting} onToggle={toggleSetting} />
))}
</div>
</div>
<div className="bg-card/50 border border-white/10 p-6">
<h3 className="text-sm font-bold text-white uppercase tracking-widest mb-6 flex items-center gap-2">
<Settings className="w-4 h-4 text-blue-500" />
System Notifications
</h3>
<div className="space-y-4">
{groupedSettings.system.map((setting) => (
<SettingRow key={setting.id} setting={setting} onToggle={toggleSetting} />
))}
</div>
</div>
</div>
<div className="mt-8 flex items-center gap-4">
<button
onClick={saveSettings}
className="bg-primary text-background px-6 py-3 font-bold uppercase tracking-wider hover:bg-primary/90 transition-colors"
data-testid="button-save-notifications"
>
Save Settings
</button>
{saved && (
<span className="text-green-500 text-sm flex items-center gap-2">
<CheckCircle className="w-4 h-4" /> Settings saved
</span>
)}
</div>
</div>
</div>
</div>
);
}
function SettingRow({ setting, onToggle }: { setting: NotificationSetting; onToggle: (id: string) => void }) {
return (
<div className="flex items-center justify-between p-4 bg-black/20 border border-white/5">
<div>
<div className="text-white font-bold text-sm">{setting.name}</div>
<div className="text-xs text-muted-foreground">{setting.description}</div>
</div>
<button
onClick={() => onToggle(setting.id)}
className={`w-12 h-6 rounded-full relative transition-colors ${
setting.enabled ? "bg-primary" : "bg-white/10"
}`}
data-testid={`toggle-${setting.id}`}
>
<span
className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-transform ${
setting.enabled ? "left-7" : "left-1"
}`}
/>
</button>
</div>
);
}
function Sidebar({ user, onLogout, active }: { user: any; onLogout: () => void; active: string }) {
return (
<div className="w-64 bg-card border-r border-white/10 flex flex-col">
<div className="p-6 border-b border-white/10">
<h1 className="text-xl font-display font-bold text-white uppercase tracking-wider">AeThex</h1>
<p className="text-xs text-primary mt-1">Command Center</p>
</div>
<nav className="flex-1 p-4 space-y-2">
<NavItem icon={<BarChart3 className="w-4 h-4" />} label="Dashboard" href="/admin" active={active === 'dashboard'} />
<NavItem icon={<Activity className="w-4 h-4" />} label="Live Activity" href="/admin/activity" active={active === 'activity'} />
<NavItem icon={<Bell className="w-4 h-4" />} label="Notifications" href="/admin/notifications" active={active === 'notifications'} />
<NavItem icon={<Users className="w-4 h-4" />} label="Architects" href="/admin/architects" active={active === 'architects'} />
<NavItem icon={<Inbox className="w-4 h-4" />} label="Applications" href="/admin/applications" active={active === 'applications'} />
<NavItem icon={<Award className="w-4 h-4" />} label="Achievements" href="/admin/achievements" active={active === 'achievements'} />
<NavItem icon={<FileCode className="w-4 h-4" />} label="Credentials" href="/admin/credentials" active={active === 'credentials'} />
<NavItem icon={<Globe className="w-4 h-4" />} label="Sites" href="/admin/sites" active={active === 'sites'} />
<NavItem icon={<Key className="w-4 h-4" />} label="Auth Logs" href="/admin/logs" active={active === 'logs'} />
<NavItem icon={<Shield className="w-4 h-4" />} label="Aegis Monitor" href="/admin/aegis" active={active === 'aegis'} />
</nav>
<div className="p-4 border-t border-white/10">
<div className="flex items-center gap-3 mb-4">
<div className="w-8 h-8 bg-primary/20 rounded-full flex items-center justify-center">
<User className="w-4 h-4 text-primary" />
</div>
<div>
<div className="text-sm text-white font-bold">{user?.username}</div>
<div className="text-xs text-muted-foreground">{user?.isAdmin ? "Administrator" : "Member"}</div>
</div>
</div>
<button onClick={onLogout} className="w-full flex items-center gap-2 text-muted-foreground hover:text-white text-sm py-2 px-3 hover:bg-white/5 transition-colors">
<LogOut className="w-4 h-4" /> Sign Out
</button>
</div>
</div>
);
}
function NavItem({ icon, label, href, active = false }: { icon: React.ReactNode; label: string; href: string; active?: boolean }) {
return (
<Link href={href}>
<div className={`flex items-center gap-3 px-3 py-2 text-sm transition-colors cursor-pointer ${active ? 'bg-primary/10 text-primary border-l-2 border-primary' : 'text-muted-foreground hover:text-white hover:bg-white/5'}`}>
{icon}
{label}
</div>
</Link>
);
}

View file

@ -76,11 +76,11 @@ export default function Admin() {
<nav className="flex-1 p-4 space-y-2">
<NavItem icon={<BarChart3 className="w-4 h-4" />} label="Dashboard" href="/admin" active />
<NavItem icon={<Activity className="w-4 h-4" />} label="Live Activity" href="/admin/activity" />
<NavItem icon={<Users className="w-4 h-4" />} label="Architects" href="/admin/architects" />
<NavItem icon={<Inbox className="w-4 h-4" />} label="Applications" href="/admin/applications" />
<NavItem icon={<Award className="w-4 h-4" />} label="Achievements" href="/admin/achievements" />
<NavItem icon={<FileCode className="w-4 h-4" />} label="Credentials" href="/admin/credentials" />
<NavItem icon={<Activity className="w-4 h-4" />} label="Projects" href="/admin/projects" />
<NavItem icon={<Globe className="w-4 h-4" />} label="Sites" href="/admin/sites" />
<NavItem icon={<Key className="w-4 h-4" />} label="Auth Logs" href="/admin/logs" />
<NavItem icon={<Shield className="w-4 h-4" />} label="Aegis Monitor" href="/admin/aegis" />

View file

@ -9,6 +9,7 @@ import {
} from "lucide-react";
import gridBg from '@assets/generated_images/dark_subtle_digital_grid_texture.png';
import { useTutorial, homeTutorialSteps, TutorialButton } from "@/components/Tutorial";
import { ThemeToggle } from "@/components/ThemeToggle";
export default function Home() {
const { startTutorial, hasCompletedTutorial, isActive } = useTutorial();
@ -60,6 +61,7 @@ export default function Home() {
<Lock className="w-3 h-3" /> Admin
</button>
</Link>
<ThemeToggle />
</div>
</nav>

22
package-lock.json generated
View file

@ -57,6 +57,7 @@
"lucide-react": "^0.545.0",
"memorystore": "^1.6.7",
"next-themes": "^0.4.6",
"openai": "^6.13.0",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
"pg": "^8.16.3",
@ -6656,6 +6657,27 @@
"node": ">= 0.8"
}
},
"node_modules/openai": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-6.13.0.tgz",
"integrity": "sha512-yHbMo+EpNGPG3sRrXvmo0LhUPFN4bAURJw3G17bE+ax1G4tcTFCa9ZjvCWh3cvni0aHY0uWlk2IxcsPH4NR9Ow==",
"license": "Apache-2.0",
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.25 || ^4.0"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",

View file

@ -60,6 +60,7 @@
"lucide-react": "^0.545.0",
"memorystore": "^1.6.7",
"next-themes": "^0.4.6",
"openai": "^6.13.0",
"passport": "^0.7.0",
"passport-local": "^1.0.0",
"pg": "^8.16.3",

59
server/openai.ts Normal file
View file

@ -0,0 +1,59 @@
import OpenAI from "openai";
// This is using Replit's AI Integrations service, which provides OpenAI-compatible API access without requiring your own OpenAI API key.
// the newest OpenAI model is "gpt-5" which was released August 7, 2025. do not change this unless explicitly requested by the user
const openai = new OpenAI({
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY
});
const SYSTEM_PROMPT = `You are the AeThex Assistant, a helpful AI guide for the AeThex ecosystem - "The Operating System for the Metaverse."
About AeThex:
- AeThex is built on a dual-entity model: The Foundation (non-profit, training) and The Corporation (for-profit, security)
- The "Holy Trinity" consists of: Axiom (The Law - foundational protocol), Codex (The Standard - certification system), and Aegis (The Shield - security layer)
- Architects are certified professionals trained through the Codex curriculum
- The platform offers gamified learning, XP progression, and verified credentials
You help users with:
- Navigating the platform features (Passport, Terminal, Curriculum, Dashboard)
- Understanding the certification process and how to become an Architect
- Explaining the Aegis security features
- Answering questions about the ecosystem and its mission
Be concise, friendly, and helpful. Use the platform's terminology when appropriate. If you don't know something specific about the platform, be honest about it.`;
interface ChatMessage {
role: "user" | "assistant";
content: string;
}
export async function getChatResponse(userMessage: string, history?: ChatMessage[]): Promise<string> {
try {
const messages: Array<{ role: "system" | "user" | "assistant"; content: string }> = [
{ role: "system", content: SYSTEM_PROMPT }
];
if (history && Array.isArray(history)) {
for (const msg of history.slice(-8)) {
if (msg.role === "user" || msg.role === "assistant") {
messages.push({ role: msg.role, content: msg.content });
}
}
}
messages.push({ role: "user", content: userMessage });
const response = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages,
max_tokens: 500,
temperature: 0.7,
});
return response.choices[0]?.message?.content || "I'm sorry, I couldn't generate a response.";
} catch (error: any) {
console.error("OpenAI chat error:", error);
throw new Error("Failed to get AI response");
}
}

View file

@ -4,6 +4,7 @@ import { storage } from "./storage";
import { loginSchema } from "@shared/schema";
import bcrypt from "bcrypt";
import crypto from "crypto";
import { getChatResponse } from "./openai";
// Extend session type
declare module 'express-session' {
@ -296,5 +297,45 @@ export async function registerRoutes(
}
});
// ========== CHATBOT API (Auth + Rate limited) ==========
const chatRateLimits = new Map<number, { count: number; resetTime: number }>();
app.post("/api/chat", requireAuth, async (req, res) => {
try {
const userId = req.session?.userId;
if (!userId) {
return res.status(401).json({ error: "Authentication required" });
}
const now = Date.now();
const rateLimit = chatRateLimits.get(userId);
if (rateLimit) {
if (now < rateLimit.resetTime) {
if (rateLimit.count >= 30) {
return res.status(429).json({ error: "Rate limit exceeded. Please wait before sending more messages." });
}
rateLimit.count++;
} else {
chatRateLimits.set(userId, { count: 1, resetTime: now + 60000 });
}
} else {
chatRateLimits.set(userId, { count: 1, resetTime: now + 60000 });
}
const { message, history } = req.body;
if (!message || typeof message !== "string") {
return res.status(400).json({ error: "Message is required" });
}
const response = await getChatResponse(message, history);
res.json({ response });
} catch (err: any) {
console.error("Chat error:", err);
res.status(500).json({ error: "Failed to get response" });
}
});
return httpServer;
}