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:
2025-07-04 21:58:39 -07:00
parent 282eeb60ca
commit d6ec5ad29c
38 changed files with 4673 additions and 10 deletions

183
admin-frontend/README.md Normal file
View 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

View 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"
}

View 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>

View 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;

View 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;

View 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;

View 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;

View 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;

View 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>;
};

View 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>
);
};

View 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;
}
}

View 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>
);

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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();

View 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'),
],
}

View 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"
]
}