mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-18 06:27:20 +00:00
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:
parent
38bc0c8663
commit
9901ea3e2d
14 changed files with 966 additions and 1 deletions
1
.replit
1
.replit
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
|
|||
210
client/src/components/Chatbot.tsx
Normal file
210
client/src/components/Chatbot.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
58
client/src/components/ThemeToggle.tsx
Normal file
58
client/src/components/ThemeToggle.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
277
client/src/pages/admin-activity.tsx
Normal file
277
client/src/pages/admin-activity.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
251
client/src/pages/admin-notifications.tsx
Normal file
251
client/src/pages/admin-notifications.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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
22
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
59
server/openai.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue