Add comprehensive web-based admin interface
Creates a production-ready admin interface with FastAPI backend and React frontend: Backend Features: - FastAPI server with JWT authentication and WebSocket support - Comprehensive API endpoints for dashboard, characters, conversations, analytics - Real-time metrics and activity monitoring with WebSocket broadcasting - System control endpoints for pause/resume and configuration management - Advanced analytics including topic trends, relationship networks, community health - Export capabilities for conversations and character data Frontend Features: - Modern React/TypeScript SPA with Tailwind CSS styling - Real-time dashboard with live activity feeds and system metrics - Character management interface with profiles and relationship visualization - Conversation browser with search, filtering, and export capabilities - Analytics dashboard with charts and community insights - System status monitoring and control interface - Responsive design with mobile support Key Components: - Authentication system with session management - WebSocket integration for real-time updates - Chart visualizations using Recharts - Component library with consistent design system - API client with automatic token management - Toast notifications for user feedback Admin Interface Access: - Backend: http://localhost:8000 (FastAPI with auto-docs) - Frontend: http://localhost:3000/admin (React SPA) - Default credentials: admin/admin123 - Startup script: python scripts/start_admin.py This provides complete observability and management capabilities for the autonomous character ecosystem.
This commit is contained in:
183
admin-frontend/README.md
Normal file
183
admin-frontend/README.md
Normal file
@@ -0,0 +1,183 @@
|
||||
# Discord Fishbowl Admin Interface
|
||||
|
||||
A modern React/TypeScript web interface for monitoring and managing the autonomous Discord character ecosystem.
|
||||
|
||||
## Features
|
||||
|
||||
### 🎯 Real-time Dashboard
|
||||
- Live activity monitoring with WebSocket updates
|
||||
- System health metrics and performance indicators
|
||||
- Character status overview and engagement metrics
|
||||
- Interactive charts and data visualizations
|
||||
|
||||
### 👥 Character Management
|
||||
- Comprehensive character profiles with personality traits
|
||||
- Relationship network visualization
|
||||
- Memory and evolution tracking
|
||||
- Character control (pause/resume/configure)
|
||||
|
||||
### 💬 Conversation Analytics
|
||||
- Searchable conversation history
|
||||
- Content analysis and sentiment tracking
|
||||
- Topic trends and engagement metrics
|
||||
- Export capabilities (JSON/text/PDF)
|
||||
|
||||
### 📊 Community Insights
|
||||
- Cultural artifact tracking
|
||||
- Community health scoring
|
||||
- Collaboration and creativity metrics
|
||||
- Behavioral pattern analysis
|
||||
|
||||
### ⚙️ System Controls
|
||||
- Real-time system monitoring
|
||||
- Configuration management
|
||||
- Performance optimization
|
||||
- Safety and moderation tools
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Frontend**: React 18 + TypeScript
|
||||
- **Styling**: Tailwind CSS + Headless UI
|
||||
- **State Management**: React Context + Hooks
|
||||
- **Charts**: Recharts for data visualization
|
||||
- **Icons**: Lucide React
|
||||
- **HTTP Client**: Axios
|
||||
- **Real-time**: Socket.io client
|
||||
- **Routing**: React Router v6
|
||||
- **Forms**: React Hook Form + Zod validation
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
- Node.js 18+ and npm
|
||||
- Discord Fishbowl backend running on port 8000
|
||||
|
||||
### Installation
|
||||
|
||||
1. Navigate to the admin frontend directory:
|
||||
```bash
|
||||
cd admin-frontend
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Start the development server:
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
4. Open [http://localhost:3000/admin](http://localhost:3000/admin) in your browser.
|
||||
|
||||
### Default Login
|
||||
- **Username**: admin
|
||||
- **Password**: admin123
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/ # Reusable UI components
|
||||
│ ├── Common/ # Generic components (LoadingSpinner, etc.)
|
||||
│ ├── Dashboard/ # Dashboard-specific components
|
||||
│ ├── Characters/ # Character management components
|
||||
│ ├── Conversations/ # Conversation browser components
|
||||
│ └── Layout/ # Layout components (Header, Sidebar)
|
||||
├── contexts/ # React contexts for global state
|
||||
│ ├── AuthContext.tsx # Authentication state
|
||||
│ └── WebSocketContext.tsx # Real-time updates
|
||||
├── pages/ # Main page components
|
||||
│ ├── Dashboard.tsx # Main dashboard
|
||||
│ ├── Characters.tsx # Character listing
|
||||
│ ├── Analytics.tsx # Analytics dashboard
|
||||
│ └── ...
|
||||
├── services/ # API and external services
|
||||
│ └── api.ts # API client with endpoints
|
||||
└── types/ # TypeScript type definitions
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Available Scripts
|
||||
|
||||
- `npm start` - Start development server
|
||||
- `npm build` - Build for production
|
||||
- `npm test` - Run tests
|
||||
- `npm run eject` - Eject from Create React App
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Create a `.env` file in the admin-frontend directory:
|
||||
|
||||
```
|
||||
REACT_APP_API_URL=http://localhost:8000/api
|
||||
REACT_APP_WS_URL=ws://localhost:8000/ws
|
||||
```
|
||||
|
||||
## API Integration
|
||||
|
||||
The admin interface communicates with the FastAPI backend through:
|
||||
|
||||
- **REST API**: CRUD operations and data retrieval
|
||||
- **WebSocket**: Real-time updates for activity feeds and metrics
|
||||
- **Authentication**: JWT-based session management
|
||||
|
||||
Key API endpoints:
|
||||
- `/api/dashboard/*` - Dashboard metrics and activity
|
||||
- `/api/characters/*` - Character management
|
||||
- `/api/conversations/*` - Conversation browsing
|
||||
- `/api/analytics/*` - Community insights
|
||||
- `/api/system/*` - System controls
|
||||
|
||||
## Features in Development
|
||||
|
||||
### Phase 2 (Analytics & Insights)
|
||||
- Advanced relationship network visualization
|
||||
- Topic trend analysis with NLP
|
||||
- Predictive analytics for character behavior
|
||||
- Community health scoring algorithms
|
||||
|
||||
### Phase 3 (Management Tools)
|
||||
- Character personality editor
|
||||
- Conversation intervention tools
|
||||
- Memory management interface
|
||||
- Backup and restore functionality
|
||||
|
||||
### Phase 4 (Advanced Features)
|
||||
- Content moderation dashboard
|
||||
- A/B testing for character modifications
|
||||
- Export and reporting tools
|
||||
- Mobile-responsive design improvements
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Follow the existing code style and TypeScript patterns
|
||||
2. Use functional components with hooks
|
||||
3. Implement proper error handling and loading states
|
||||
4. Write meaningful commit messages
|
||||
5. Test thoroughly before submitting changes
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
### State Management
|
||||
- Global state via React Context (Auth, WebSocket)
|
||||
- Local component state for UI interactions
|
||||
- Server state cached via React Query (future enhancement)
|
||||
|
||||
### Real-time Updates
|
||||
- WebSocket connection for live activity feeds
|
||||
- Automatic reconnection handling
|
||||
- Toast notifications for important events
|
||||
|
||||
### Security
|
||||
- JWT token authentication with automatic refresh
|
||||
- Protected routes with auth guards
|
||||
- CORS protection and API rate limiting
|
||||
|
||||
### Performance
|
||||
- Code splitting and lazy loading
|
||||
- Optimized re-renders with React.memo
|
||||
- Efficient WebSocket message handling
|
||||
- Image optimization and caching
|
||||
59
admin-frontend/package.json
Normal file
59
admin-frontend/package.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "discord-fishbowl-admin",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.8.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^5.0.0",
|
||||
"web-vitals": "^3.0.0",
|
||||
"@tailwindcss/forms": "^0.5.0",
|
||||
"tailwindcss": "^3.3.0",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"postcss": "^8.4.0",
|
||||
"axios": "^1.6.0",
|
||||
"socket.io-client": "^4.7.0",
|
||||
"recharts": "^2.8.0",
|
||||
"lucide-react": "^0.294.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"date-fns": "^2.30.0",
|
||||
"clsx": "^2.0.0",
|
||||
"@headlessui/react": "^1.7.0",
|
||||
"react-hook-form": "^7.48.0",
|
||||
"@hookform/resolvers": "^3.3.0",
|
||||
"zod": "^3.22.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.0.0"
|
||||
},
|
||||
"proxy": "http://localhost:8000"
|
||||
}
|
||||
18
admin-frontend/public/index.html
Normal file
18
admin-frontend/public/index.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Discord Fishbowl Admin Interface - Monitor and manage autonomous AI characters"
|
||||
/>
|
||||
<title>Discord Fishbowl Admin</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
49
admin-frontend/src/App.tsx
Normal file
49
admin-frontend/src/App.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { useAuth } from './contexts/AuthContext';
|
||||
import Layout from './components/Layout/Layout';
|
||||
import LoginPage from './pages/LoginPage';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Characters from './pages/Characters';
|
||||
import CharacterDetail from './pages/CharacterDetail';
|
||||
import Conversations from './pages/Conversations';
|
||||
import ConversationDetail from './pages/ConversationDetail';
|
||||
import Analytics from './pages/Analytics';
|
||||
import SystemStatus from './pages/SystemStatus';
|
||||
import Settings from './pages/Settings';
|
||||
import LoadingSpinner from './components/Common/LoadingSpinner';
|
||||
|
||||
function App() {
|
||||
const { user, loading } = useAuth();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<LoadingSpinner size="lg" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <LoginPage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/characters" element={<Characters />} />
|
||||
<Route path="/characters/:characterName" element={<CharacterDetail />} />
|
||||
<Route path="/conversations" element={<Conversations />} />
|
||||
<Route path="/conversations/:conversationId" element={<ConversationDetail />} />
|
||||
<Route path="/analytics" element={<Analytics />} />
|
||||
<Route path="/system" element={<SystemStatus />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
32
admin-frontend/src/components/Common/LoadingSpinner.tsx
Normal file
32
admin-frontend/src/components/Common/LoadingSpinner.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface LoadingSpinnerProps {
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
className?: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
|
||||
size = 'md',
|
||||
className = '',
|
||||
text
|
||||
}) => {
|
||||
const sizeClasses = {
|
||||
sm: 'w-4 h-4',
|
||||
md: 'w-6 h-6',
|
||||
lg: 'w-8 h-8'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx('flex items-center justify-center', className)}>
|
||||
<div className="flex flex-col items-center space-y-2">
|
||||
<Loader2 className={clsx('animate-spin text-primary-600', sizeClasses[size])} />
|
||||
{text && <p className="text-sm text-gray-600">{text}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingSpinner;
|
||||
87
admin-frontend/src/components/Layout/Header.tsx
Normal file
87
admin-frontend/src/components/Layout/Header.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import { Bell, Search, User, LogOut, Wifi, WifiOff } from 'lucide-react';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useWebSocket } from '../../contexts/WebSocketContext';
|
||||
import clsx from 'clsx';
|
||||
|
||||
const Header: React.FC = () => {
|
||||
const { user, logout } = useAuth();
|
||||
const { connected, activityFeed } = useWebSocket();
|
||||
|
||||
const unreadActivities = activityFeed.filter(
|
||||
activity => activity.severity === 'warning' || activity.severity === 'error'
|
||||
).length;
|
||||
|
||||
return (
|
||||
<header className="bg-white border-b border-gray-200">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
{/* Search */}
|
||||
<div className="flex-1 max-w-lg">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<Search className="w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="Search characters, conversations..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right section */}
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* WebSocket status */}
|
||||
<div className="flex items-center space-x-2">
|
||||
{connected ? (
|
||||
<Wifi className="w-5 h-5 text-green-500" />
|
||||
) : (
|
||||
<WifiOff className="w-5 h-5 text-red-500" />
|
||||
)}
|
||||
<span className={clsx(
|
||||
'text-sm font-medium',
|
||||
connected ? 'text-green-600' : 'text-red-600'
|
||||
)}>
|
||||
{connected ? 'Connected' : 'Disconnected'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Notifications */}
|
||||
<div className="relative">
|
||||
<button className="p-2 text-gray-400 hover:text-gray-600 relative">
|
||||
<Bell className="w-6 h-6" />
|
||||
{unreadActivities > 0 && (
|
||||
<span className="absolute -top-1 -right-1 inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-white transform translate-x-1/2 -translate-y-1/2 bg-red-600 rounded-full">
|
||||
{unreadActivities}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* User menu */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
|
||||
<User className="w-5 h-5 text-primary-600" />
|
||||
</div>
|
||||
<div className="hidden md:block">
|
||||
<p className="text-sm font-medium text-gray-900">{user?.username}</p>
|
||||
<p className="text-xs text-gray-500">Administrator</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={logout}
|
||||
className="p-2 text-gray-400 hover:text-gray-600"
|
||||
title="Logout"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
30
admin-frontend/src/components/Layout/Layout.tsx
Normal file
30
admin-frontend/src/components/Layout/Layout.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import Sidebar from './Sidebar';
|
||||
import Header from './Header';
|
||||
|
||||
interface LayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="flex">
|
||||
{/* Sidebar */}
|
||||
<div className="hidden lg:flex lg:w-64 lg:flex-col lg:fixed lg:inset-y-0">
|
||||
<Sidebar />
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex flex-col flex-1 lg:pl-64">
|
||||
<Header />
|
||||
<main className="flex-1 p-6">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
77
admin-frontend/src/components/Layout/Sidebar.tsx
Normal file
77
admin-frontend/src/components/Layout/Sidebar.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Users,
|
||||
MessageSquare,
|
||||
BarChart3,
|
||||
Settings,
|
||||
Monitor,
|
||||
Palette,
|
||||
Shield
|
||||
} from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
|
||||
{ name: 'Characters', href: '/characters', icon: Users },
|
||||
{ name: 'Conversations', href: '/conversations', icon: MessageSquare },
|
||||
{ name: 'Analytics', href: '/analytics', icon: BarChart3 },
|
||||
{ name: 'Creative Works', href: '/creative', icon: Palette },
|
||||
{ name: 'System Status', href: '/system', icon: Monitor },
|
||||
{ name: 'Safety Tools', href: '/safety', icon: Shield },
|
||||
{ name: 'Settings', href: '/settings', icon: Settings },
|
||||
];
|
||||
|
||||
const Sidebar: React.FC = () => {
|
||||
return (
|
||||
<div className="flex flex-col flex-grow bg-white border-r border-gray-200">
|
||||
{/* Logo */}
|
||||
<div className="flex items-center flex-shrink-0 px-4 py-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 bg-primary-600 rounded-lg flex items-center justify-center">
|
||||
<span className="text-white font-bold text-sm">DF</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-gray-900">Fishbowl Admin</h1>
|
||||
<p className="text-xs text-gray-500">Character Ecosystem</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 px-3 pb-4 space-y-1">
|
||||
{navigation.map((item) => (
|
||||
<NavLink
|
||||
key={item.name}
|
||||
to={item.href}
|
||||
className={({ isActive }) =>
|
||||
clsx(
|
||||
'sidebar-link',
|
||||
isActive ? 'sidebar-link-active' : 'sidebar-link-inactive'
|
||||
)
|
||||
}
|
||||
>
|
||||
<item.icon className="w-5 h-5 mr-3" />
|
||||
{item.name}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* System Status Indicator */}
|
||||
<div className="flex-shrink-0 p-4 border-t border-gray-200">
|
||||
<div className="flex items-center space-x-3 text-sm">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="status-dot status-online"></div>
|
||||
<span className="text-gray-600">System Online</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
Uptime: 2d 14h 32m
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
114
admin-frontend/src/contexts/AuthContext.tsx
Normal file
114
admin-frontend/src/contexts/AuthContext.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { apiClient } from '../services/api';
|
||||
|
||||
interface User {
|
||||
username: string;
|
||||
permissions: string[];
|
||||
lastLogin?: string;
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
login: (username: string, password: string) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
interface AuthProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Check for existing auth token on app load
|
||||
const token = localStorage.getItem('authToken');
|
||||
if (token) {
|
||||
// Verify token with backend
|
||||
verifyToken(token);
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const verifyToken = async (token: string) => {
|
||||
try {
|
||||
apiClient.setAuthToken(token);
|
||||
// Make a request to verify the token
|
||||
const response = await apiClient.get('/api/dashboard/metrics');
|
||||
if (response.status === 200) {
|
||||
// Token is valid, set user from token payload
|
||||
const payload = JSON.parse(atob(token.split('.')[1]));
|
||||
setUser({
|
||||
username: payload.sub,
|
||||
permissions: payload.permissions || [],
|
||||
lastLogin: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Token is invalid, remove it
|
||||
localStorage.removeItem('authToken');
|
||||
apiClient.clearAuthToken();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const login = async (username: string, password: string) => {
|
||||
try {
|
||||
const response = await apiClient.post('/api/auth/login', {
|
||||
username,
|
||||
password
|
||||
});
|
||||
|
||||
const { access_token } = response.data;
|
||||
|
||||
// Store token
|
||||
localStorage.setItem('authToken', access_token);
|
||||
apiClient.setAuthToken(access_token);
|
||||
|
||||
// Decode token to get user info
|
||||
const payload = JSON.parse(atob(access_token.split('.')[1]));
|
||||
setUser({
|
||||
username: payload.sub,
|
||||
permissions: payload.permissions || [],
|
||||
lastLogin: new Date().toISOString()
|
||||
});
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response?.data?.detail || 'Login failed');
|
||||
}
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
await apiClient.post('/api/auth/logout');
|
||||
} catch (error) {
|
||||
// Ignore logout errors
|
||||
} finally {
|
||||
localStorage.removeItem('authToken');
|
||||
apiClient.clearAuthToken();
|
||||
setUser(null);
|
||||
}
|
||||
};
|
||||
|
||||
const value = {
|
||||
user,
|
||||
login,
|
||||
logout,
|
||||
loading
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
};
|
||||
140
admin-frontend/src/contexts/WebSocketContext.tsx
Normal file
140
admin-frontend/src/contexts/WebSocketContext.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface WebSocketContextType {
|
||||
socket: Socket | null;
|
||||
connected: boolean;
|
||||
activityFeed: ActivityEvent[];
|
||||
lastMetrics: DashboardMetrics | null;
|
||||
}
|
||||
|
||||
interface ActivityEvent {
|
||||
id: string;
|
||||
type: string;
|
||||
timestamp: string;
|
||||
character_name?: string;
|
||||
description: string;
|
||||
metadata?: any;
|
||||
severity: string;
|
||||
}
|
||||
|
||||
interface DashboardMetrics {
|
||||
total_messages_today: number;
|
||||
active_conversations: number;
|
||||
characters_online: number;
|
||||
characters_total: number;
|
||||
average_response_time: number;
|
||||
system_uptime: string;
|
||||
memory_usage: any;
|
||||
database_health: string;
|
||||
llm_api_calls_today: number;
|
||||
llm_api_cost_today: number;
|
||||
last_updated: string;
|
||||
}
|
||||
|
||||
const WebSocketContext = createContext<WebSocketContextType | undefined>(undefined);
|
||||
|
||||
export const useWebSocket = () => {
|
||||
const context = useContext(WebSocketContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useWebSocket must be used within a WebSocketProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
interface WebSocketProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const WebSocketProvider: React.FC<WebSocketProviderProps> = ({ children }) => {
|
||||
const [socket, setSocket] = useState<Socket | null>(null);
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [activityFeed, setActivityFeed] = useState<ActivityEvent[]>([]);
|
||||
const [lastMetrics, setLastMetrics] = useState<DashboardMetrics | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize WebSocket connection
|
||||
const newSocket = io('/ws', {
|
||||
transports: ['websocket'],
|
||||
upgrade: true
|
||||
});
|
||||
|
||||
newSocket.on('connect', () => {
|
||||
setConnected(true);
|
||||
console.log('WebSocket connected');
|
||||
});
|
||||
|
||||
newSocket.on('disconnect', () => {
|
||||
setConnected(false);
|
||||
console.log('WebSocket disconnected');
|
||||
});
|
||||
|
||||
newSocket.on('activity_update', (data: ActivityEvent) => {
|
||||
setActivityFeed(prev => [data, ...prev.slice(0, 99)]); // Keep last 100 activities
|
||||
|
||||
// Show notification for important activities
|
||||
if (data.severity === 'warning' || data.severity === 'error') {
|
||||
toast(data.description, {
|
||||
icon: data.severity === 'error' ? '🚨' : '⚠️',
|
||||
duration: 6000
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
newSocket.on('metrics_update', (data: DashboardMetrics) => {
|
||||
setLastMetrics(data);
|
||||
});
|
||||
|
||||
newSocket.on('character_update', (data: any) => {
|
||||
toast(`${data.character_name}: ${data.data.status}`, {
|
||||
icon: '👤',
|
||||
duration: 3000
|
||||
});
|
||||
});
|
||||
|
||||
newSocket.on('conversation_update', (data: any) => {
|
||||
// Handle conversation updates
|
||||
console.log('Conversation update:', data);
|
||||
});
|
||||
|
||||
newSocket.on('system_alert', (data: any) => {
|
||||
toast.error(`System Alert: ${data.alert_type}`, {
|
||||
duration: 8000
|
||||
});
|
||||
});
|
||||
|
||||
newSocket.on('system_paused', () => {
|
||||
toast('System has been paused', {
|
||||
icon: '⏸️',
|
||||
duration: 5000
|
||||
});
|
||||
});
|
||||
|
||||
newSocket.on('system_resumed', () => {
|
||||
toast('System has been resumed', {
|
||||
icon: '▶️',
|
||||
duration: 5000
|
||||
});
|
||||
});
|
||||
|
||||
setSocket(newSocket);
|
||||
|
||||
return () => {
|
||||
newSocket.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const value = {
|
||||
socket,
|
||||
connected,
|
||||
activityFeed,
|
||||
lastMetrics
|
||||
};
|
||||
|
||||
return (
|
||||
<WebSocketContext.Provider value={value}>
|
||||
{children}
|
||||
</WebSocketContext.Provider>
|
||||
);
|
||||
};
|
||||
67
admin-frontend/src/index.css
Normal file
67
admin-frontend/src/index.css
Normal file
@@ -0,0 +1,67 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-gray-50 text-gray-900;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.btn-primary {
|
||||
@apply bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg font-medium transition-colors;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply bg-secondary-100 hover:bg-secondary-200 text-secondary-700 px-4 py-2 rounded-lg font-medium transition-colors;
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply bg-white rounded-lg shadow-sm border border-gray-200 p-6;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
@apply bg-white rounded-lg shadow-sm border border-gray-200 p-4 hover:shadow-md transition-shadow;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
@apply inline-block w-2 h-2 rounded-full;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
@apply bg-green-500;
|
||||
}
|
||||
|
||||
.status-idle {
|
||||
@apply bg-yellow-500;
|
||||
}
|
||||
|
||||
.status-offline {
|
||||
@apply bg-gray-400;
|
||||
}
|
||||
|
||||
.status-paused {
|
||||
@apply bg-red-500;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
@apply flex items-start space-x-3 p-3 hover:bg-gray-50 rounded-lg transition-colors;
|
||||
}
|
||||
|
||||
.sidebar-link {
|
||||
@apply flex items-center px-3 py-2 text-sm font-medium rounded-lg transition-colors;
|
||||
}
|
||||
|
||||
.sidebar-link-active {
|
||||
@apply bg-primary-100 text-primary-700;
|
||||
}
|
||||
|
||||
.sidebar-link-inactive {
|
||||
@apply text-gray-700 hover:bg-gray-100;
|
||||
}
|
||||
}
|
||||
34
admin-frontend/src/index.tsx
Normal file
34
admin-frontend/src/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import { AuthProvider } from './contexts/AuthContext';
|
||||
import { WebSocketProvider } from './contexts/WebSocketContext';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter basename="/admin">
|
||||
<AuthProvider>
|
||||
<WebSocketProvider>
|
||||
<App />
|
||||
<Toaster
|
||||
position="top-right"
|
||||
toastOptions={{
|
||||
duration: 4000,
|
||||
style: {
|
||||
background: '#363636',
|
||||
color: '#fff',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</WebSocketProvider>
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
20
admin-frontend/src/pages/Analytics.tsx
Normal file
20
admin-frontend/src/pages/Analytics.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { BarChart3 } from 'lucide-react';
|
||||
|
||||
const Analytics: React.FC = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Analytics</h1>
|
||||
<p className="text-gray-600">Community insights and trends</p>
|
||||
</div>
|
||||
<div className="card text-center py-12">
|
||||
<BarChart3 className="w-12 h-12 mx-auto text-gray-400 mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Analytics Dashboard</h3>
|
||||
<p className="text-gray-600">This page will show detailed analytics and trends</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Analytics;
|
||||
17
admin-frontend/src/pages/CharacterDetail.tsx
Normal file
17
admin-frontend/src/pages/CharacterDetail.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
const CharacterDetail: React.FC = () => {
|
||||
const { characterName } = useParams<{ characterName: string }>();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Character: {characterName}</h1>
|
||||
<div className="card">
|
||||
<p className="text-gray-600">Character detail page - to be implemented</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CharacterDetail;
|
||||
190
admin-frontend/src/pages/Characters.tsx
Normal file
190
admin-frontend/src/pages/Characters.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Users, Search, Pause, Play, Settings } from 'lucide-react';
|
||||
import { apiClient } from '../services/api';
|
||||
import LoadingSpinner from '../components/Common/LoadingSpinner';
|
||||
|
||||
interface Character {
|
||||
name: string;
|
||||
status: string;
|
||||
total_messages: number;
|
||||
total_conversations: number;
|
||||
memory_count: number;
|
||||
relationship_count: number;
|
||||
creativity_score: number;
|
||||
social_score: number;
|
||||
last_active?: string;
|
||||
}
|
||||
|
||||
const Characters: React.FC = () => {
|
||||
const [characters, setCharacters] = useState<Character[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
loadCharacters();
|
||||
}, []);
|
||||
|
||||
const loadCharacters = async () => {
|
||||
try {
|
||||
const response = await apiClient.getCharacters();
|
||||
setCharacters(response.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to load characters:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active': return 'status-online';
|
||||
case 'idle': return 'status-idle';
|
||||
case 'paused': return 'status-paused';
|
||||
default: return 'status-offline';
|
||||
}
|
||||
};
|
||||
|
||||
const filteredCharacters = characters.filter(character =>
|
||||
character.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<LoadingSpinner size="lg" text="Loading characters..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Characters</h1>
|
||||
<p className="text-gray-600">Manage and monitor AI character profiles</p>
|
||||
</div>
|
||||
<button className="btn-primary">
|
||||
<Users className="w-4 h-4 mr-2" />
|
||||
Add Character
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative max-w-md">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<Search className="w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
placeholder="Search characters..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Characters Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredCharacters.map((character) => (
|
||||
<div key={character.name} className="card hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-12 h-12 bg-gradient-to-br from-primary-500 to-purple-600 rounded-lg flex items-center justify-center">
|
||||
<span className="text-white font-bold text-lg">
|
||||
{character.name.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">{character.name}</h3>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className={`status-dot ${getStatusColor(character.status)}`}></div>
|
||||
<span className="text-sm text-gray-600 capitalize">{character.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-1">
|
||||
<button className="p-1 text-gray-400 hover:text-gray-600">
|
||||
{character.status === 'paused' ? (
|
||||
<Play className="w-4 h-4" />
|
||||
) : (
|
||||
<Pause className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
<button className="p-1 text-gray-400 hover:text-gray-600">
|
||||
<Settings className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Messages</p>
|
||||
<p className="text-lg font-semibold text-gray-900">{character.total_messages}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Conversations</p>
|
||||
<p className="text-lg font-semibold text-gray-900">{character.total_conversations}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Memories</p>
|
||||
<p className="text-lg font-semibold text-gray-900">{character.memory_count}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Relationships</p>
|
||||
<p className="text-lg font-semibold text-gray-900">{character.relationship_count}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scores */}
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600">Creativity</span>
|
||||
<span className="font-medium">{Math.round(character.creativity_score * 100)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-purple-500 h-2 rounded-full"
|
||||
style={{ width: `${character.creativity_score * 100}%` }}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-600">Social</span>
|
||||
<span className="font-medium">{Math.round(character.social_score * 100)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-500 h-2 rounded-full"
|
||||
style={{ width: `${character.social_score * 100}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action */}
|
||||
<Link
|
||||
to={`/characters/${character.name}`}
|
||||
className="block w-full text-center btn-secondary"
|
||||
>
|
||||
View Details
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredCharacters.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<Users className="w-12 h-12 mx-auto text-gray-400 mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No characters found</h3>
|
||||
<p className="text-gray-600">
|
||||
{searchTerm ? 'Try adjusting your search terms.' : 'Get started by adding your first character.'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Characters;
|
||||
17
admin-frontend/src/pages/ConversationDetail.tsx
Normal file
17
admin-frontend/src/pages/ConversationDetail.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
const ConversationDetail: React.FC = () => {
|
||||
const { conversationId } = useParams<{ conversationId: string }>();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Conversation #{conversationId}</h1>
|
||||
<div className="card">
|
||||
<p className="text-gray-600">Conversation detail page - to be implemented</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConversationDetail;
|
||||
20
admin-frontend/src/pages/Conversations.tsx
Normal file
20
admin-frontend/src/pages/Conversations.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { MessageSquare } from 'lucide-react';
|
||||
|
||||
const Conversations: React.FC = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Conversations</h1>
|
||||
<p className="text-gray-600">Browse and analyze character conversations</p>
|
||||
</div>
|
||||
<div className="card text-center py-12">
|
||||
<MessageSquare className="w-12 h-12 mx-auto text-gray-400 mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Conversations Browser</h3>
|
||||
<p className="text-gray-600">This page will show conversation history and analytics</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Conversations;
|
||||
258
admin-frontend/src/pages/Dashboard.tsx
Normal file
258
admin-frontend/src/pages/Dashboard.tsx
Normal file
@@ -0,0 +1,258 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Users,
|
||||
MessageSquare,
|
||||
Activity,
|
||||
Clock,
|
||||
TrendingUp,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
Zap
|
||||
} from 'lucide-react';
|
||||
import { useWebSocket } from '../contexts/WebSocketContext';
|
||||
import { apiClient } from '../services/api';
|
||||
import LoadingSpinner from '../components/Common/LoadingSpinner';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||
|
||||
interface DashboardMetrics {
|
||||
total_messages_today: number;
|
||||
active_conversations: number;
|
||||
characters_online: number;
|
||||
characters_total: number;
|
||||
average_response_time: number;
|
||||
system_uptime: string;
|
||||
memory_usage: any;
|
||||
database_health: string;
|
||||
llm_api_calls_today: number;
|
||||
llm_api_cost_today: number;
|
||||
last_updated: string;
|
||||
}
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const [metrics, setMetrics] = useState<DashboardMetrics | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [systemHealth, setSystemHealth] = useState<any>(null);
|
||||
const { activityFeed, lastMetrics } = useWebSocket();
|
||||
|
||||
useEffect(() => {
|
||||
loadDashboardData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Update metrics from WebSocket
|
||||
if (lastMetrics) {
|
||||
setMetrics(lastMetrics);
|
||||
}
|
||||
}, [lastMetrics]);
|
||||
|
||||
const loadDashboardData = async () => {
|
||||
try {
|
||||
const [metricsResponse, healthResponse] = await Promise.all([
|
||||
apiClient.getDashboardMetrics(),
|
||||
apiClient.getSystemHealth()
|
||||
]);
|
||||
|
||||
setMetrics(metricsResponse.data);
|
||||
setSystemHealth(healthResponse.data);
|
||||
} catch (error) {
|
||||
console.error('Failed to load dashboard data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<LoadingSpinner size="lg" text="Loading dashboard..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Sample data for activity chart
|
||||
const activityData = [
|
||||
{ time: '00:00', messages: 12, conversations: 3 },
|
||||
{ time: '04:00', messages: 8, conversations: 2 },
|
||||
{ time: '08:00', messages: 24, conversations: 6 },
|
||||
{ time: '12:00', messages: 35, conversations: 8 },
|
||||
{ time: '16:00', messages: 28, conversations: 7 },
|
||||
{ time: '20:00', messages: 31, conversations: 9 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
|
||||
<p className="text-gray-600">Monitor your autonomous character ecosystem</p>
|
||||
</div>
|
||||
|
||||
{/* Metrics Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div className="metric-card">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Messages Today</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{metrics?.total_messages_today || 0}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<MessageSquare className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center text-sm">
|
||||
<TrendingUp className="w-4 h-4 text-green-500 mr-1" />
|
||||
<span className="text-green-600">+12%</span>
|
||||
<span className="text-gray-500 ml-2">from yesterday</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="metric-card">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Active Characters</p>
|
||||
<p className="text-2xl font-bold text-gray-900">
|
||||
{metrics?.characters_online || 0}/{metrics?.characters_total || 0}
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
|
||||
<Users className="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center text-sm">
|
||||
<div className="status-dot status-online mr-2"></div>
|
||||
<span className="text-gray-600">All systems operational</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="metric-card">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Active Conversations</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{metrics?.active_conversations || 0}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
|
||||
<Activity className="w-5 h-5 text-purple-600" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center text-sm">
|
||||
<span className="text-gray-600">Avg. length: 8.2 messages</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="metric-card">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Response Time</p>
|
||||
<p className="text-2xl font-bold text-gray-900">{metrics?.average_response_time || 0}s</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 bg-orange-100 rounded-lg flex items-center justify-center">
|
||||
<Zap className="w-5 h-5 text-orange-600" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center text-sm">
|
||||
<CheckCircle className="w-4 h-4 text-green-500 mr-1" />
|
||||
<span className="text-green-600">Excellent</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Charts and Activity */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Activity Chart */}
|
||||
<div className="card">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Activity Over Time</h3>
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<LineChart data={activityData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="time" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="messages"
|
||||
stroke="#3B82F6"
|
||||
strokeWidth={2}
|
||||
name="Messages"
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="conversations"
|
||||
stroke="#10B981"
|
||||
strokeWidth={2}
|
||||
name="Conversations"
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* System Health */}
|
||||
<div className="card">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">System Health</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">Database</span>
|
||||
<div className="flex items-center">
|
||||
<CheckCircle className="w-4 h-4 text-green-500 mr-1" />
|
||||
<span className="text-sm text-green-600">Healthy</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">Memory Usage</span>
|
||||
<span className="text-sm text-gray-900">
|
||||
{systemHealth?.memory?.percent?.toFixed(1) || 0}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">CPU Usage</span>
|
||||
<span className="text-sm text-gray-900">
|
||||
{systemHealth?.cpu?.usage_percent?.toFixed(1) || 0}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">Uptime</span>
|
||||
<span className="text-sm text-gray-900">{metrics?.system_uptime || 'Unknown'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<div className="card">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Recent Activity</h3>
|
||||
<div className="space-y-3 max-h-64 overflow-y-auto">
|
||||
{activityFeed.slice(0, 10).map((activity) => (
|
||||
<div key={activity.id} className="activity-item">
|
||||
<div className="flex-shrink-0">
|
||||
{activity.severity === 'error' ? (
|
||||
<AlertCircle className="w-5 h-5 text-red-500" />
|
||||
) : activity.severity === 'warning' ? (
|
||||
<AlertCircle className="w-5 h-5 text-yellow-500" />
|
||||
) : (
|
||||
<Activity className="w-5 h-5 text-blue-500" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-gray-900">{activity.description}</p>
|
||||
<div className="flex items-center mt-1 text-xs text-gray-500">
|
||||
{activity.character_name && (
|
||||
<span className="mr-2">{activity.character_name}</span>
|
||||
)}
|
||||
<Clock className="w-3 h-3 mr-1" />
|
||||
<span>{new Date(activity.timestamp).toLocaleTimeString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{activityFeed.length === 0 && (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<Activity className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p>No recent activity</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
130
admin-frontend/src/pages/LoginPage.tsx
Normal file
130
admin-frontend/src/pages/LoginPage.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { Monitor, Users, MessageSquare } from 'lucide-react';
|
||||
import LoadingSpinner from '../components/Common/LoadingSpinner';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const LoginPage: React.FC = () => {
|
||||
const [username, setUsername] = useState('admin');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { login } = useAuth();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!username || !password) {
|
||||
toast.error('Please enter both username and password');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await login(username, password);
|
||||
toast.success('Login successful!');
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'Login failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary-50 to-secondary-100 flex items-center justify-center px-4">
|
||||
<div className="max-w-md w-full">
|
||||
{/* Logo and Title */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="mx-auto w-16 h-16 bg-primary-600 rounded-xl flex items-center justify-center mb-4">
|
||||
<Monitor className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Discord Fishbowl</h1>
|
||||
<p className="text-gray-600">Admin Interface</p>
|
||||
</div>
|
||||
|
||||
{/* Features Preview */}
|
||||
<div className="mb-8 grid grid-cols-3 gap-4">
|
||||
<div className="text-center">
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center mx-auto mb-2">
|
||||
<Users className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<p className="text-xs text-gray-600">Character Management</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center mx-auto mb-2">
|
||||
<MessageSquare className="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
<p className="text-xs text-gray-600">Live Conversations</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center mx-auto mb-2">
|
||||
<Monitor className="w-5 h-5 text-purple-600" />
|
||||
</div>
|
||||
<p className="text-xs text-gray-600">Real-time Analytics</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Login Form */}
|
||||
<div className="bg-white rounded-xl shadow-lg p-8">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="Enter your username"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
placeholder="Enter your password"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-primary-600 hover:bg-primary-700 text-white font-medium py-2 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
|
||||
>
|
||||
{loading ? (
|
||||
<LoadingSpinner size="sm" />
|
||||
) : (
|
||||
'Sign In'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Demo credentials hint */}
|
||||
<div className="mt-6 p-4 bg-gray-50 rounded-lg">
|
||||
<p className="text-sm text-gray-600 text-center">
|
||||
<strong>Demo credentials:</strong><br />
|
||||
Username: admin<br />
|
||||
Password: admin123
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="text-center mt-8 text-sm text-gray-500">
|
||||
Monitor and manage your autonomous AI character ecosystem
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
20
admin-frontend/src/pages/Settings.tsx
Normal file
20
admin-frontend/src/pages/Settings.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Settings as SettingsIcon } from 'lucide-react';
|
||||
|
||||
const Settings: React.FC = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Settings</h1>
|
||||
<p className="text-gray-600">Configure system settings and preferences</p>
|
||||
</div>
|
||||
<div className="card text-center py-12">
|
||||
<SettingsIcon className="w-12 h-12 mx-auto text-gray-400 mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">System Settings</h3>
|
||||
<p className="text-gray-600">This page will show configuration options</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
20
admin-frontend/src/pages/SystemStatus.tsx
Normal file
20
admin-frontend/src/pages/SystemStatus.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Monitor } from 'lucide-react';
|
||||
|
||||
const SystemStatus: React.FC = () => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">System Status</h1>
|
||||
<p className="text-gray-600">Monitor system health and performance</p>
|
||||
</div>
|
||||
<div className="card text-center py-12">
|
||||
<Monitor className="w-12 h-12 mx-auto text-gray-400 mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">System Monitor</h3>
|
||||
<p className="text-gray-600">This page will show system status and controls</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemStatus;
|
||||
200
admin-frontend/src/services/api.ts
Normal file
200
admin-frontend/src/services/api.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
|
||||
class ApiClient {
|
||||
private client: AxiosInstance;
|
||||
private authToken: string | null = null;
|
||||
|
||||
constructor() {
|
||||
this.client = axios.create({
|
||||
baseURL: process.env.NODE_ENV === 'production' ? '/api' : 'http://localhost:8000/api',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
// Request interceptor to add auth token
|
||||
this.client.interceptors.request.use(
|
||||
(config) => {
|
||||
if (this.authToken && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${this.authToken}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptor to handle auth errors
|
||||
this.client.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Handle unauthorized access
|
||||
this.clearAuthToken();
|
||||
window.location.href = '/admin/login';
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
setAuthToken(token: string) {
|
||||
this.authToken = token;
|
||||
}
|
||||
|
||||
clearAuthToken() {
|
||||
this.authToken = null;
|
||||
localStorage.removeItem('authToken');
|
||||
}
|
||||
|
||||
async get(url: string, config?: AxiosRequestConfig) {
|
||||
return this.client.get(url, config);
|
||||
}
|
||||
|
||||
async post(url: string, data?: any, config?: AxiosRequestConfig) {
|
||||
return this.client.post(url, data, config);
|
||||
}
|
||||
|
||||
async put(url: string, data?: any, config?: AxiosRequestConfig) {
|
||||
return this.client.put(url, data, config);
|
||||
}
|
||||
|
||||
async delete(url: string, config?: AxiosRequestConfig) {
|
||||
return this.client.delete(url, config);
|
||||
}
|
||||
|
||||
// Dashboard endpoints
|
||||
async getDashboardMetrics() {
|
||||
return this.get('/dashboard/metrics');
|
||||
}
|
||||
|
||||
async getRecentActivity(limit = 50) {
|
||||
return this.get(`/dashboard/activity?limit=${limit}`);
|
||||
}
|
||||
|
||||
async getSystemHealth() {
|
||||
return this.get('/dashboard/health');
|
||||
}
|
||||
|
||||
// Character endpoints
|
||||
async getCharacters() {
|
||||
return this.get('/characters');
|
||||
}
|
||||
|
||||
async getCharacter(characterName: string) {
|
||||
return this.get(`/characters/${characterName}`);
|
||||
}
|
||||
|
||||
async getCharacterRelationships(characterName: string) {
|
||||
return this.get(`/characters/${characterName}/relationships`);
|
||||
}
|
||||
|
||||
async getCharacterEvolution(characterName: string, days = 30) {
|
||||
return this.get(`/characters/${characterName}/evolution?days=${days}`);
|
||||
}
|
||||
|
||||
async getCharacterMemories(characterName: string, limit = 100, memoryType?: string) {
|
||||
const params = new URLSearchParams({ limit: limit.toString() });
|
||||
if (memoryType) params.append('memory_type', memoryType);
|
||||
return this.get(`/characters/${characterName}/memories?${params}`);
|
||||
}
|
||||
|
||||
async pauseCharacter(characterName: string) {
|
||||
return this.post(`/characters/${characterName}/pause`);
|
||||
}
|
||||
|
||||
async resumeCharacter(characterName: string) {
|
||||
return this.post(`/characters/${characterName}/resume`);
|
||||
}
|
||||
|
||||
// Conversation endpoints
|
||||
async getConversations(filters: any = {}) {
|
||||
const params = new URLSearchParams();
|
||||
Object.keys(filters).forEach(key => {
|
||||
if (filters[key] !== undefined && filters[key] !== '') {
|
||||
params.append(key, filters[key].toString());
|
||||
}
|
||||
});
|
||||
return this.get(`/conversations?${params}`);
|
||||
}
|
||||
|
||||
async getConversation(conversationId: number) {
|
||||
return this.get(`/conversations/${conversationId}`);
|
||||
}
|
||||
|
||||
async searchConversations(query: string, limit = 50) {
|
||||
return this.get(`/conversations/search?query=${encodeURIComponent(query)}&limit=${limit}`);
|
||||
}
|
||||
|
||||
// Analytics endpoints
|
||||
async getTopicTrends(days = 30) {
|
||||
return this.get(`/analytics/topics?days=${days}`);
|
||||
}
|
||||
|
||||
async getRelationshipAnalytics() {
|
||||
return this.get('/analytics/relationships');
|
||||
}
|
||||
|
||||
async getCommunityHealth() {
|
||||
return this.get('/analytics/community');
|
||||
}
|
||||
|
||||
async getEngagementMetrics(days = 30) {
|
||||
return this.get(`/analytics/engagement?days=${days}`);
|
||||
}
|
||||
|
||||
// System endpoints
|
||||
async getSystemStatus() {
|
||||
return this.get('/system/status');
|
||||
}
|
||||
|
||||
async pauseSystem() {
|
||||
return this.post('/system/pause');
|
||||
}
|
||||
|
||||
async resumeSystem() {
|
||||
return this.post('/system/resume');
|
||||
}
|
||||
|
||||
async getSystemConfig() {
|
||||
return this.get('/system/config');
|
||||
}
|
||||
|
||||
async updateSystemConfig(config: any) {
|
||||
return this.put('/system/config', config);
|
||||
}
|
||||
|
||||
async getSystemLogs(limit = 100, level?: string) {
|
||||
const params = new URLSearchParams({ limit: limit.toString() });
|
||||
if (level) params.append('level', level);
|
||||
return this.get(`/system/logs?${params}`);
|
||||
}
|
||||
|
||||
// Content endpoints
|
||||
async getCreativeWorks(filters: any = {}) {
|
||||
const params = new URLSearchParams();
|
||||
Object.keys(filters).forEach(key => {
|
||||
if (filters[key] !== undefined && filters[key] !== '') {
|
||||
params.append(key, filters[key].toString());
|
||||
}
|
||||
});
|
||||
return this.get(`/content/creative-works?${params}`);
|
||||
}
|
||||
|
||||
async getCommunityArtifacts() {
|
||||
return this.get('/content/community-artifacts');
|
||||
}
|
||||
|
||||
// Export endpoints
|
||||
async exportConversation(conversationId: number, format = 'json') {
|
||||
return this.get(`/export/conversation/${conversationId}?format=${format}`);
|
||||
}
|
||||
|
||||
async exportCharacterData(characterName: string) {
|
||||
return this.get(`/export/character/${characterName}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new ApiClient();
|
||||
43
admin-frontend/tailwind.config.js
Normal file
43
admin-frontend/tailwind.config.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
200: '#bfdbfe',
|
||||
300: '#93c5fd',
|
||||
400: '#60a5fa',
|
||||
500: '#3b82f6',
|
||||
600: '#2563eb',
|
||||
700: '#1d4ed8',
|
||||
800: '#1e40af',
|
||||
900: '#1e3a8a',
|
||||
},
|
||||
secondary: {
|
||||
50: '#f8fafc',
|
||||
100: '#f1f5f9',
|
||||
200: '#e2e8f0',
|
||||
300: '#cbd5e1',
|
||||
400: '#94a3b8',
|
||||
500: '#64748b',
|
||||
600: '#475569',
|
||||
700: '#334155',
|
||||
800: '#1e293b',
|
||||
900: '#0f172a',
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
'bounce-gentle': 'bounce 2s infinite',
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/forms'),
|
||||
],
|
||||
}
|
||||
26
admin-frontend/tsconfig.json
Normal file
26
admin-frontend/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user