mirror of
https://github.com/aljazceru/enclava.git
synced 2025-12-17 15:34:36 +01:00
adding README, cleanup
This commit is contained in:
118
README.md
Normal file
118
README.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Enclava
|
||||
|
||||
**Secure AI Platform with Privacy-First LLM Services**
|
||||
|
||||
Enclava is a comprehensive AI platform that provides secure chatbot services, document retrieval (RAG), and LLM integrations with Trusted Execution Environment (TEE) support via privateMode.ai.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **AI Chatbots** - Customizable chatbots with prompt templates and RAG integration (openai compatible)
|
||||
- **RAG System** - Document upload, processing, and semantic search with Qdrant
|
||||
- **TEE Security** - Privacy-protected LLM inference via confidential computing
|
||||
- **OpenAI Compatible** - Standard API endpoints for seamless integration with existing tools
|
||||
- **Budget Management** - Built-in spend tracking and usage limits
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker and Docker Compose
|
||||
- Git
|
||||
- [privatemode.ai](https://privatemode.ai) api key
|
||||
|
||||
### 1. Clone Repository
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd enclava
|
||||
```
|
||||
|
||||
### 2. Configure Environment
|
||||
|
||||
```bash
|
||||
# Copy example environment file
|
||||
cp .env.example .env
|
||||
|
||||
# Edit .env with your settings
|
||||
vim .env
|
||||
```
|
||||
|
||||
**Required Configuration:**
|
||||
```bash
|
||||
# Security
|
||||
JWT_SECRET=your-super-secret-jwt-key-here-change-in-production
|
||||
|
||||
# PrivateMode.ai API Key (optional but recommended)
|
||||
PRIVATEMODE_API_KEY=your-privatemode-api-key
|
||||
|
||||
# Base URL for CORS and frontend
|
||||
BASE_URL=localhost
|
||||
```
|
||||
|
||||
### 3. Deploy with Docker
|
||||
|
||||
```bash
|
||||
# Start all services
|
||||
docker compose up --build
|
||||
|
||||
# Or run in background
|
||||
docker compose up --build -d
|
||||
```
|
||||
|
||||
### 4. Access Application
|
||||
|
||||
- **Main Application**: http://localhost
|
||||
- **API Documentation**: http://localhost/docs (backend API)
|
||||
- **Qdrant Dashboard**: http://localhost:56333/dashboard
|
||||
|
||||
### 5. Default Login
|
||||
|
||||
- **Username**: `admin`
|
||||
- **Password**: `admin123`
|
||||
|
||||
*Change default credentials immediately in production!*
|
||||
|
||||
## Documentation
|
||||
|
||||
For comprehensive documentation, API references, and advanced configuration:
|
||||
|
||||
**[docs.enclava.ai](https://docs.enclava.ai)**
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Frontend**: Next.js (React/TypeScript) with Tailwind CSS
|
||||
- **Backend**: FastAPI (Python) with async/await patterns
|
||||
- **Database**: PostgreSQL with automatic migrations
|
||||
- **Vector DB**: Qdrant for document embeddings
|
||||
- **Cache**: Redis for sessions and performance
|
||||
- **LLM Service**: Native secure LLM service with TEE support
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Port | Purpose |
|
||||
|---------|------|---------|
|
||||
| Nginx (Main) | 80 | Reverse proxy and main access |
|
||||
| Backend API | 58000 | FastAPI application (internal) |
|
||||
| Frontend | 3000 | Next.js application (internal) |
|
||||
| PostgreSQL | 5432 | Primary database |
|
||||
| Redis | 6379 | Caching and sessions |
|
||||
| Qdrant | 56333 | Vector database for RAG |
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
See `.env.example` for all available configuration options.
|
||||
|
||||
|
||||
## Support
|
||||
|
||||
- **Documentation**: [docs.enclava.ai](https://docs.enclava.ai)
|
||||
- **Issues**: Use the GitHub issue tracker
|
||||
- **Security**: Report security issues privately
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -58,8 +58,7 @@ class Settings(BaseSettings):
|
||||
# LLM Service Configuration (replaced LiteLLM)
|
||||
# LLM service configuration is now handled in app/services/llm/config.py
|
||||
|
||||
# LLM Service Security
|
||||
LLM_ENCRYPTION_KEY: Optional[str] = None # Key for encrypting LLM provider API keys
|
||||
# LLM Service Security (removed encryption - credentials handled by proxy)
|
||||
|
||||
# Plugin System Security
|
||||
PLUGIN_ENCRYPTION_KEY: Optional[str] = None # Key for encrypting plugin secrets and configurations
|
||||
|
||||
@@ -126,9 +126,6 @@ def create_default_config() -> LLMServiceConfig:
|
||||
class EnvironmentVariables:
|
||||
"""Environment variables used by LLM service"""
|
||||
|
||||
# Encryption
|
||||
LLM_ENCRYPTION_KEY: Optional[str] = None
|
||||
|
||||
# Provider API keys
|
||||
PRIVATEMODE_API_KEY: Optional[str] = None
|
||||
OPENAI_API_KEY: Optional[str] = None
|
||||
@@ -140,7 +137,6 @@ class EnvironmentVariables:
|
||||
|
||||
def __post_init__(self):
|
||||
"""Load values from environment"""
|
||||
self.LLM_ENCRYPTION_KEY = os.getenv("LLM_ENCRYPTION_KEY")
|
||||
self.PRIVATEMODE_API_KEY = os.getenv("PRIVATEMODE_API_KEY")
|
||||
self.OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
||||
self.ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
|
||||
@@ -204,15 +200,9 @@ class ConfigurationManager:
|
||||
config = self.get_config()
|
||||
return [name for name, provider in config.providers.items() if provider.enabled]
|
||||
|
||||
def get_api_key(self, provider_name: str, encrypted: bool = False) -> Optional[str]:
|
||||
def get_api_key(self, provider_name: str) -> Optional[str]:
|
||||
"""Get API key for provider"""
|
||||
api_key = self._env_vars.get_api_key(provider_name)
|
||||
|
||||
if api_key and encrypted:
|
||||
from .security import security_manager
|
||||
return security_manager.encrypt_api_key(api_key)
|
||||
|
||||
return api_key
|
||||
return self._env_vars.get_api_key(provider_name)
|
||||
|
||||
def _validate_configuration(self):
|
||||
"""Validate current configuration"""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
LLM Security Manager
|
||||
|
||||
Handles API key encryption, prompt injection detection, and audit logging.
|
||||
Handles prompt injection detection and audit logging.
|
||||
Provides comprehensive security for LLM interactions.
|
||||
"""
|
||||
|
||||
@@ -12,10 +12,6 @@ import logging
|
||||
import hashlib
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
from datetime import datetime
|
||||
from cryptography.fernet import Fernet
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
import base64
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
@@ -26,51 +22,8 @@ class SecurityManager:
|
||||
"""Manages security for LLM operations"""
|
||||
|
||||
def __init__(self):
|
||||
self._fernet = None
|
||||
self._setup_encryption()
|
||||
self._setup_prompt_injection_patterns()
|
||||
|
||||
def _setup_encryption(self):
|
||||
"""Setup Fernet encryption for API keys"""
|
||||
try:
|
||||
# Get encryption key from environment or generate one
|
||||
encryption_key = os.getenv("LLM_ENCRYPTION_KEY")
|
||||
|
||||
if not encryption_key:
|
||||
# Generate a key if none exists (for development)
|
||||
# In production, this should be set as an environment variable
|
||||
logger.warning("LLM_ENCRYPTION_KEY not set, generating temporary key")
|
||||
key = Fernet.generate_key()
|
||||
encryption_key = key.decode()
|
||||
logger.info(f"Generated temporary encryption key: {encryption_key}")
|
||||
else:
|
||||
# Validate the key format
|
||||
try:
|
||||
key = encryption_key.encode()
|
||||
Fernet(key) # Test if key is valid
|
||||
except Exception:
|
||||
# Key might be a password, derive Fernet key from it
|
||||
key = self._derive_key_from_password(encryption_key)
|
||||
|
||||
self._fernet = Fernet(key if isinstance(key, bytes) else key.encode())
|
||||
logger.info("Encryption system initialized successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to setup encryption: {e}")
|
||||
raise RuntimeError("Encryption setup failed")
|
||||
|
||||
def _derive_key_from_password(self, password: str) -> bytes:
|
||||
"""Derive Fernet key from password using PBKDF2"""
|
||||
# Use a fixed salt for consistency (in production, store this securely)
|
||||
salt = b"enclava_llm_salt"
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=salt,
|
||||
iterations=100000,
|
||||
)
|
||||
key = base64.urlsafe_b64encode(kdf.derive(password.encode()))
|
||||
return key
|
||||
|
||||
def _setup_prompt_injection_patterns(self):
|
||||
"""Setup patterns for prompt injection detection"""
|
||||
@@ -131,30 +84,6 @@ class SecurityManager:
|
||||
self.compiled_patterns = [re.compile(pattern) for pattern in self.injection_patterns]
|
||||
logger.info(f"Initialized {len(self.injection_patterns)} prompt injection patterns")
|
||||
|
||||
def encrypt_api_key(self, api_key: str) -> str:
|
||||
"""Encrypt an API key for secure storage"""
|
||||
try:
|
||||
if not api_key:
|
||||
raise ValueError("API key cannot be empty")
|
||||
|
||||
encrypted = self._fernet.encrypt(api_key.encode())
|
||||
return base64.urlsafe_b64encode(encrypted).decode()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to encrypt API key: {e}")
|
||||
raise SecurityError("API key encryption failed")
|
||||
|
||||
def decrypt_api_key(self, encrypted_key: str) -> str:
|
||||
"""Decrypt an API key for use"""
|
||||
try:
|
||||
if not encrypted_key:
|
||||
raise ValueError("Encrypted key cannot be empty")
|
||||
|
||||
decoded = base64.urlsafe_b64decode(encrypted_key.encode())
|
||||
decrypted = self._fernet.decrypt(decoded)
|
||||
return decrypted.decode()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to decrypt API key: {e}")
|
||||
raise SecurityError("API key decryption failed")
|
||||
|
||||
def validate_prompt_security(self, messages: List[Dict[str, str]]) -> Tuple[bool, float, List[str]]:
|
||||
"""
|
||||
|
||||
@@ -327,9 +327,9 @@ export default function AdminPage() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex space-x-4">
|
||||
<Button onClick={() => window.open('/modules', '_blank')}>
|
||||
<Button onClick={() => window.open('/settings', '_blank')}>
|
||||
<Database className="mr-2 h-4 w-4" />
|
||||
Module Manager
|
||||
System Settings
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => window.open('/budgets', '_blank')}>
|
||||
<DollarSign className="mr-2 h-4 w-4" />
|
||||
|
||||
@@ -59,6 +59,22 @@ interface ApiKey {
|
||||
allowed_chatbots: string[];
|
||||
}
|
||||
|
||||
interface Model {
|
||||
id: string;
|
||||
object: string;
|
||||
created?: number;
|
||||
owned_by?: string;
|
||||
permission?: any[];
|
||||
root?: string;
|
||||
parent?: string;
|
||||
provider?: string;
|
||||
capabilities?: string[];
|
||||
context_window?: number;
|
||||
max_output_tokens?: number;
|
||||
supports_streaming?: boolean;
|
||||
supports_function_calling?: boolean;
|
||||
}
|
||||
|
||||
interface NewApiKeyData {
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -88,7 +104,7 @@ export default function ApiKeysPage() {
|
||||
const [newKeyVisible, setNewKeyVisible] = useState<string | null>(null);
|
||||
const [visibleKeys, setVisibleKeys] = useState<Record<string, boolean>>({});
|
||||
const [editKeyData, setEditKeyData] = useState<Partial<ApiKey>>({});
|
||||
const [availableModels, setAvailableModels] = useState<any[]>([]);
|
||||
const [availableModels, setAvailableModels] = useState<Model[]>([]);
|
||||
const [availableChatbots, setAvailableChatbots] = useState<any[]>([]);
|
||||
|
||||
const [newKeyData, setNewKeyData] = useState<NewApiKeyData>({
|
||||
@@ -108,9 +124,10 @@ export default function ApiKeysPage() {
|
||||
fetchAvailableModels();
|
||||
fetchAvailableChatbots();
|
||||
|
||||
// Check URL parameters for chatbot pre-selection
|
||||
// Check URL parameters for auto-opening create dialog
|
||||
const chatbotId = searchParams.get('chatbot');
|
||||
const chatbotName = searchParams.get('chatbot_name');
|
||||
const createParam = searchParams.get('create');
|
||||
|
||||
if (chatbotId && chatbotName) {
|
||||
// Pre-populate the form with the chatbot selected and required permissions
|
||||
@@ -128,6 +145,9 @@ export default function ApiKeysPage() {
|
||||
title: "Chatbot Selected",
|
||||
description: `Creating API key for ${decodeURIComponent(chatbotName)}`
|
||||
});
|
||||
} else if (createParam === 'true') {
|
||||
// Automatically open the create dialog for general API key creation
|
||||
setShowCreateDialog(true);
|
||||
}
|
||||
}, [searchParams, toast]);
|
||||
|
||||
@@ -135,7 +155,7 @@ export default function ApiKeysPage() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await apiClient.get("/api-internal/v1/api-keys") as any;
|
||||
setApiKeys(result.data || []);
|
||||
setApiKeys(result.api_keys || result.data || []);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch API keys:", error);
|
||||
toast({
|
||||
@@ -477,7 +497,7 @@ export default function ApiKeysPage() {
|
||||
className="rounded"
|
||||
/>
|
||||
<Label htmlFor={`model-${model.id}`} className="text-sm">
|
||||
{model.name}
|
||||
{model.id}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -53,8 +53,18 @@ interface APIKey {
|
||||
|
||||
interface Model {
|
||||
id: string
|
||||
name: string
|
||||
provider: string
|
||||
object: string
|
||||
created?: number
|
||||
owned_by?: string
|
||||
permission?: any[]
|
||||
root?: string
|
||||
parent?: string
|
||||
provider?: string
|
||||
capabilities?: string[]
|
||||
context_window?: number
|
||||
max_output_tokens?: number
|
||||
supports_streaming?: boolean
|
||||
supports_function_calling?: boolean
|
||||
}
|
||||
|
||||
export default function LLMPage() {
|
||||
@@ -108,12 +118,9 @@ function LLMPageContent() {
|
||||
})
|
||||
])
|
||||
|
||||
console.log('API keys data:', keysData)
|
||||
setApiKeys(keysData.api_keys || [])
|
||||
console.log('API keys state updated, count:', keysData.api_keys?.length || 0)
|
||||
setModels(modelsData.data || [])
|
||||
|
||||
console.log('Data fetch completed successfully')
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error)
|
||||
toast({
|
||||
@@ -315,7 +322,7 @@ function LLMPageContent() {
|
||||
<Key className="h-5 w-5" />
|
||||
API Keys
|
||||
</CardTitle>
|
||||
<Button onClick={() => router.push('/api-keys')}>
|
||||
<Button onClick={() => router.push('/api-keys?create=true')}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Create API Key
|
||||
</Button>
|
||||
@@ -440,15 +447,53 @@ function LLMPageContent() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{models.map((model) => (
|
||||
<div key={model.id} className="border rounded-lg p-4">
|
||||
<h3 className="font-medium">{model.id}</h3>
|
||||
<p className="text-sm text-muted-foreground">Provider: {model.owned_by}</p>
|
||||
<Badge variant="outline" className="mt-2">
|
||||
{model.object}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
{models.map((model) => {
|
||||
// Helper function to get provider from model ID
|
||||
const getProviderFromModel = (modelId: string): string => {
|
||||
if (modelId.startsWith('privatemode-')) return 'PrivateMode.ai'
|
||||
if (modelId.startsWith('gpt-') || modelId.includes('openai')) return 'OpenAI'
|
||||
if (modelId.startsWith('claude-') || modelId.includes('anthropic')) return 'Anthropic'
|
||||
if (modelId.startsWith('gemini-') || modelId.includes('google')) return 'Google'
|
||||
if (modelId.includes('cohere')) return 'Cohere'
|
||||
if (modelId.includes('mistral')) return 'Mistral'
|
||||
if (modelId.includes('llama') && !modelId.startsWith('privatemode-')) return 'Meta'
|
||||
return model.owned_by || 'Unknown'
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={model.id} className="border rounded-lg p-4">
|
||||
<h3 className="font-medium">{model.id}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Provider: {getProviderFromModel(model.id)}
|
||||
</p>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Badge variant="outline">
|
||||
{model.object || 'model'}
|
||||
</Badge>
|
||||
{model.supports_streaming && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Streaming
|
||||
</Badge>
|
||||
)}
|
||||
{model.supports_function_calling && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Functions
|
||||
</Badge>
|
||||
)}
|
||||
{model.capabilities?.includes('tee') && (
|
||||
<Badge variant="outline" className="text-xs border-green-500 text-green-700">
|
||||
TEE
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{model.context_window && (
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
Context: {model.context_window.toLocaleString()} tokens
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{models.length === 0 && (
|
||||
<div className="col-span-full text-center py-8 text-muted-foreground">
|
||||
No models available. Check your LLM platform configuration.
|
||||
|
||||
@@ -1,399 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import {
|
||||
Database,
|
||||
Play,
|
||||
Square,
|
||||
RefreshCw,
|
||||
Settings,
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
Cpu,
|
||||
BarChart3
|
||||
} from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { ProtectedRoute } from '@/components/auth/ProtectedRoute'
|
||||
import { useModules, triggerModuleRefresh } from '@/contexts/ModulesContext'
|
||||
import { apiClient } from '@/lib/api-client'
|
||||
|
||||
interface Module {
|
||||
name: string;
|
||||
status: "loaded" | "failed" | "disabled";
|
||||
dependencies: string[];
|
||||
config: Record<string, any>;
|
||||
metrics?: {
|
||||
requests_processed: number;
|
||||
average_response_time: number;
|
||||
error_rate: number;
|
||||
last_activity: string;
|
||||
};
|
||||
health?: {
|
||||
status: "healthy" | "warning" | "error";
|
||||
message: string;
|
||||
uptime: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface ModuleStats {
|
||||
total_modules: number;
|
||||
loaded_modules: number;
|
||||
failed_modules: number;
|
||||
system_health: "healthy" | "warning" | "error";
|
||||
}
|
||||
|
||||
export default function ModulesPage() {
|
||||
return (
|
||||
<ProtectedRoute>
|
||||
<ModulesPageContent />
|
||||
</ProtectedRoute>
|
||||
)
|
||||
}
|
||||
|
||||
function ModulesPageContent() {
|
||||
const { toast } = useToast();
|
||||
const { modules: contextModules, isLoading: contextLoading, refreshModules } = useModules();
|
||||
const [stats, setStats] = useState<ModuleStats | null>(null);
|
||||
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
||||
|
||||
// Transform context modules to match existing interface
|
||||
const modules: Module[] = contextModules.map(module => ({
|
||||
name: module.name,
|
||||
status: module.initialized && module.enabled ? "loaded" :
|
||||
!module.enabled ? "disabled" : "failed",
|
||||
dependencies: [], // Not provided in current API
|
||||
config: module.stats || {},
|
||||
metrics: {
|
||||
requests_processed: module.stats?.total_requests || 0,
|
||||
average_response_time: module.stats?.avg_analysis_time || 0,
|
||||
error_rate: module.stats?.errors || 0,
|
||||
last_activity: new Date().toISOString(),
|
||||
},
|
||||
health: {
|
||||
status: module.initialized && module.enabled ? "healthy" : "error",
|
||||
message: module.initialized && module.enabled ? "Module is running" :
|
||||
!module.enabled ? "Module is disabled" : "Module failed to initialize",
|
||||
uptime: module.stats?.uptime || 0,
|
||||
}
|
||||
}));
|
||||
|
||||
const loading = contextLoading;
|
||||
|
||||
useEffect(() => {
|
||||
// Calculate stats from context modules
|
||||
setStats({
|
||||
total_modules: contextModules.length,
|
||||
loaded_modules: contextModules.filter(m => m.initialized && m.enabled).length,
|
||||
failed_modules: contextModules.filter(m => !m.initialized || !m.enabled).length,
|
||||
system_health: contextModules.some(m => !m.initialized) ? "warning" : "healthy"
|
||||
});
|
||||
}, [contextModules]);
|
||||
|
||||
|
||||
const handleModuleAction = async (moduleName: string, action: "start" | "stop" | "restart" | "reload") => {
|
||||
try {
|
||||
setActionLoading(`${moduleName}-${action}`);
|
||||
|
||||
const responseData = await apiClient.post(`/api-internal/v1/modules/${moduleName}/${action}`, {});
|
||||
|
||||
toast({
|
||||
title: "Success",
|
||||
description: `Module ${moduleName} ${action}ed successfully`,
|
||||
});
|
||||
|
||||
// Refresh modules context and trigger navigation update
|
||||
await refreshModules();
|
||||
|
||||
// Trigger navigation refresh if the response indicates it's needed
|
||||
if (responseData.refreshRequired) {
|
||||
triggerModuleRefresh();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to ${action} module:`, error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: error instanceof Error ? error.message : `Failed to ${action} module`,
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setActionLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModuleToggle = async (moduleName: string, enabled: boolean) => {
|
||||
await handleModuleAction(moduleName, enabled ? "start" : "stop");
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case "loaded":
|
||||
return <CheckCircle className="h-4 w-4 text-green-500" />;
|
||||
case "failed":
|
||||
return <AlertTriangle className="h-4 w-4 text-red-500" />;
|
||||
case "disabled":
|
||||
return <Square className="h-4 w-4 text-gray-500" />;
|
||||
default:
|
||||
return <Clock className="h-4 w-4 text-yellow-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const variants: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
|
||||
loaded: "default",
|
||||
failed: "destructive",
|
||||
disabled: "secondary"
|
||||
};
|
||||
return <Badge variant={variants[status] || "outline"}>{status}</Badge>;
|
||||
};
|
||||
|
||||
const formatUptime = (seconds: number) => {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
return `${hours}h ${minutes}m`;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-empire-gold"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Module Manager</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Manage dynamic modules and monitor system performance
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button onClick={refreshModules} variant="outline">
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
<Button onClick={() => handleModuleAction("all", "reload")}>
|
||||
<Database className="mr-2 h-4 w-4" />
|
||||
Reload All
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Health Alert */}
|
||||
{stats && stats.system_health !== "healthy" && (
|
||||
<Alert className="mb-6">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
System health: {stats.system_health}. Some modules may not be functioning properly.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Statistics Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Modules</CardTitle>
|
||||
<Database className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats?.total_modules || 0}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Loaded</CardTitle>
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-600">{stats?.loaded_modules || 0}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Failed</CardTitle>
|
||||
<AlertTriangle className="h-4 w-4 text-red-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-red-600">{stats?.failed_modules || 0}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">System Health</CardTitle>
|
||||
{getStatusIcon(stats?.system_health || "unknown")}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold capitalize">{stats?.system_health || "Unknown"}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Modules List */}
|
||||
<Tabs defaultValue="overview" className="space-y-6">
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="performance">Performance</TabsTrigger>
|
||||
<TabsTrigger value="configuration">Configuration</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-4">
|
||||
{modules.map((module) => (
|
||||
<Card key={module.name}>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
{getStatusIcon(module.status)}
|
||||
<div>
|
||||
<CardTitle className="text-lg">{module.name}</CardTitle>
|
||||
<CardDescription>
|
||||
Dependencies: {module.dependencies.length > 0 ? module.dependencies.join(", ") : "None"}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
{getStatusBadge(module.status)}
|
||||
<Switch
|
||||
checked={module.status === "loaded"}
|
||||
onCheckedChange={(checked) => handleModuleToggle(module.name, checked)}
|
||||
disabled={actionLoading?.startsWith(module.name)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleModuleAction(module.name, "restart")}
|
||||
disabled={actionLoading === `${module.name}-restart`}
|
||||
>
|
||||
<RefreshCw className="mr-2 h-3 w-3" />
|
||||
Restart
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleModuleAction(module.name, "reload")}
|
||||
disabled={actionLoading === `${module.name}-reload`}
|
||||
>
|
||||
<Database className="mr-2 h-3 w-3" />
|
||||
Reload
|
||||
</Button>
|
||||
{module.name === 'signal' && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => window.location.href = '/signal'}
|
||||
>
|
||||
<Settings className="mr-2 h-3 w-3" />
|
||||
Configure
|
||||
</Button>
|
||||
)}
|
||||
{module.name === 'zammad' && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => window.location.href = '/zammad'}
|
||||
>
|
||||
<Settings className="mr-2 h-3 w-3" />
|
||||
Configure
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{module.health && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="font-medium">Health:</span> {module.health.status}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Uptime:</span> {formatUptime(module.health.uptime)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Message:</span> {module.health.message}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="performance" className="space-y-4">
|
||||
{modules
|
||||
.filter((module) => module.metrics)
|
||||
.map((module) => (
|
||||
<Card key={module.name}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<BarChart3 className="mr-2 h-5 w-5" />
|
||||
{module.name} Performance
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold">{module.metrics?.requests_processed || 0}</div>
|
||||
<p className="text-xs text-muted-foreground">Requests Processed</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold">{module.metrics?.average_response_time || 0}ms</div>
|
||||
<p className="text-xs text-muted-foreground">Avg Response Time</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold">{(module.metrics?.error_rate || 0).toFixed(2)}%</div>
|
||||
<p className="text-xs text-muted-foreground">Error Rate</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-sm font-medium">
|
||||
{module.metrics?.last_activity ?
|
||||
new Date(module.metrics.last_activity).toLocaleString() :
|
||||
"Never"
|
||||
}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Last Activity</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="configuration" className="space-y-4">
|
||||
{modules.map((module) => (
|
||||
<Card key={module.name}>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<Settings className="mr-2 h-5 w-5" />
|
||||
{module.name} Configuration
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="bg-muted p-4 rounded-lg text-sm overflow-auto">
|
||||
{JSON.stringify(module.config, null, 2)}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -25,10 +25,15 @@ import {
|
||||
Server,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
Info
|
||||
Info,
|
||||
Square,
|
||||
Clock,
|
||||
Play
|
||||
} from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { apiClient } from "@/lib/api-client";
|
||||
import { useModules, triggerModuleRefresh } from '@/contexts/ModulesContext';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
interface SystemSettings {
|
||||
// Security Settings
|
||||
@@ -95,6 +100,31 @@ interface SystemSettings {
|
||||
};
|
||||
}
|
||||
|
||||
interface Module {
|
||||
name: string;
|
||||
status: "loaded" | "failed" | "disabled";
|
||||
dependencies: string[];
|
||||
config: Record<string, any>;
|
||||
metrics?: {
|
||||
requests_processed: number;
|
||||
average_response_time: number;
|
||||
error_rate: number;
|
||||
last_activity: string;
|
||||
};
|
||||
health?: {
|
||||
status: "healthy" | "warning" | "error";
|
||||
message: string;
|
||||
uptime: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface ModuleStats {
|
||||
total_modules: number;
|
||||
loaded_modules: number;
|
||||
failed_modules: number;
|
||||
system_health: "healthy" | "warning" | "error";
|
||||
}
|
||||
|
||||
function SettingsPageContent() {
|
||||
const { toast } = useToast();
|
||||
const [settings, setSettings] = useState<SystemSettings | null>(null);
|
||||
@@ -102,10 +132,46 @@ function SettingsPageContent() {
|
||||
const [saving, setSaving] = useState<string | null>(null);
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
|
||||
// Modules state
|
||||
const { modules: contextModules, isLoading: modulesLoading, refreshModules } = useModules();
|
||||
const [moduleStats, setModuleStats] = useState<ModuleStats | null>(null);
|
||||
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
// Transform context modules to match existing interface
|
||||
const modules: Module[] = contextModules.map(module => ({
|
||||
name: module.name,
|
||||
status: module.initialized && module.enabled ? "loaded" :
|
||||
!module.enabled ? "disabled" : "failed",
|
||||
dependencies: [], // Not provided in current API
|
||||
config: module.stats || {},
|
||||
metrics: {
|
||||
requests_processed: module.stats?.total_requests || 0,
|
||||
average_response_time: module.stats?.avg_analysis_time || 0,
|
||||
error_rate: module.stats?.errors || 0,
|
||||
last_activity: new Date().toISOString(),
|
||||
},
|
||||
health: {
|
||||
status: module.initialized && module.enabled ? "healthy" : "error",
|
||||
message: module.initialized && module.enabled ? "Module is running" :
|
||||
!module.enabled ? "Module is disabled" : "Module failed to initialize",
|
||||
uptime: module.stats?.uptime || 0,
|
||||
}
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
// Calculate stats from context modules
|
||||
setModuleStats({
|
||||
total_modules: contextModules.length,
|
||||
loaded_modules: contextModules.filter(m => m.initialized && m.enabled).length,
|
||||
failed_modules: contextModules.filter(m => !m.initialized || !m.enabled).length,
|
||||
system_health: contextModules.some(m => !m.initialized) ? "warning" : "healthy"
|
||||
});
|
||||
}, [contextModules]);
|
||||
|
||||
const fetchSettings = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -202,6 +268,68 @@ function SettingsPageContent() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleModuleAction = async (moduleName: string, action: "start" | "stop" | "restart" | "reload") => {
|
||||
try {
|
||||
setActionLoading(`${moduleName}-${action}`);
|
||||
|
||||
const responseData = await apiClient.post(`/api-internal/v1/modules/${moduleName}/${action}`, {});
|
||||
|
||||
toast({
|
||||
title: "Success",
|
||||
description: `Module ${moduleName} ${action}ed successfully`,
|
||||
});
|
||||
|
||||
// Refresh modules context and trigger navigation update
|
||||
await refreshModules();
|
||||
|
||||
// Trigger navigation refresh if the response indicates it's needed
|
||||
if (responseData.refreshRequired) {
|
||||
triggerModuleRefresh();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to ${action} module:`, error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: error instanceof Error ? error.message : `Failed to ${action} module`,
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setActionLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModuleToggle = async (moduleName: string, enabled: boolean) => {
|
||||
await handleModuleAction(moduleName, enabled ? "start" : "stop");
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case "loaded":
|
||||
return <CheckCircle className="h-4 w-4 text-green-500" />;
|
||||
case "failed":
|
||||
return <AlertTriangle className="h-4 w-4 text-red-500" />;
|
||||
case "disabled":
|
||||
return <Square className="h-4 w-4 text-gray-500" />;
|
||||
default:
|
||||
return <Clock className="h-4 w-4 text-yellow-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const variants: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
|
||||
loaded: "default",
|
||||
failed: "destructive",
|
||||
disabled: "secondary"
|
||||
};
|
||||
return <Badge variant={variants[status] || "outline"}>{status}</Badge>;
|
||||
};
|
||||
|
||||
const formatUptime = (seconds: number) => {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
return `${hours}h ${minutes}m`;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
@@ -261,10 +389,11 @@ function SettingsPageContent() {
|
||||
)}
|
||||
|
||||
<Tabs defaultValue="security" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="security">Security</TabsTrigger>
|
||||
<TabsTrigger value="api">API</TabsTrigger>
|
||||
<TabsTrigger value="notifications">Notifications</TabsTrigger>
|
||||
<TabsTrigger value="modules">Modules</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="security" className="space-y-6">
|
||||
@@ -849,6 +978,135 @@ function SettingsPageContent() {
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="modules" className="space-y-6">
|
||||
{/* System Health Alert */}
|
||||
{moduleStats && moduleStats.system_health !== "healthy" && (
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
System health: {moduleStats.system_health}. Some modules may not be functioning properly.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Statistics Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Modules</CardTitle>
|
||||
<Database className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{moduleStats?.total_modules || 0}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Loaded</CardTitle>
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-green-600">{moduleStats?.loaded_modules || 0}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Failed</CardTitle>
|
||||
<AlertTriangle className="h-4 w-4 text-red-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-red-600">{moduleStats?.failed_modules || 0}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">System Health</CardTitle>
|
||||
{getStatusIcon(moduleStats?.system_health || "unknown")}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold capitalize">{moduleStats?.system_health || "Unknown"}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Modules List */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-medium">Module Overview</h3>
|
||||
<div className="flex space-x-2">
|
||||
<Button onClick={refreshModules} variant="outline" size="sm">
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{modules.map((module) => (
|
||||
<Card key={module.name}>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
{getStatusIcon(module.status)}
|
||||
<div>
|
||||
<CardTitle className="text-lg">{module.name}</CardTitle>
|
||||
<CardDescription>
|
||||
Dependencies: {module.dependencies.length > 0 ? module.dependencies.join(", ") : "None"}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
{getStatusBadge(module.status)}
|
||||
<Switch
|
||||
checked={module.status === "loaded"}
|
||||
onCheckedChange={(checked) => handleModuleToggle(module.name, checked)}
|
||||
disabled={actionLoading?.startsWith(module.name)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleModuleAction(module.name, "restart")}
|
||||
disabled={actionLoading === `${module.name}-restart`}
|
||||
>
|
||||
<RefreshCw className="mr-2 h-3 w-3" />
|
||||
Restart
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleModuleAction(module.name, "reload")}
|
||||
disabled={actionLoading === `${module.name}-reload`}
|
||||
>
|
||||
<Database className="mr-2 h-3 w-3" />
|
||||
Reload
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{module.health && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="font-medium">Health:</span> {module.health.status}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Uptime:</span> {formatUptime(module.health.uptime)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Message:</span> {module.health.message}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
Plus,
|
||||
Settings,
|
||||
Trash2,
|
||||
Copy,
|
||||
Play,
|
||||
Bot,
|
||||
Brain,
|
||||
@@ -27,7 +26,9 @@ import {
|
||||
BookOpen,
|
||||
Palette,
|
||||
Key,
|
||||
Globe
|
||||
Globe,
|
||||
Copy,
|
||||
Link
|
||||
} from "lucide-react"
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
import { ChatInterface } from "./ChatInterface"
|
||||
@@ -684,11 +685,12 @@ export function ChatbotManager() {
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs defaultValue="basic" className="mt-6">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsList className="grid w-full grid-cols-5">
|
||||
<TabsTrigger value="basic">Basic</TabsTrigger>
|
||||
<TabsTrigger value="personality">Personality</TabsTrigger>
|
||||
<TabsTrigger value="knowledge">Knowledge</TabsTrigger>
|
||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||
<TabsTrigger value="integration">Integration</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="basic" className="space-y-4 mt-6">
|
||||
@@ -925,6 +927,115 @@ export function ChatbotManager() {
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="integration" className="space-y-6 mt-6">
|
||||
{editingChatbot && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center space-x-3 p-4 bg-muted/50 rounded-lg">
|
||||
<Link className="h-5 w-5 text-primary" />
|
||||
<div>
|
||||
<h3 className="font-medium">API Integration</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Use this OpenAI-compatible endpoint to integrate your chatbot into external applications
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">OpenAI-Compatible Endpoint</Label>
|
||||
<p className="text-xs text-muted-foreground mb-2">
|
||||
Standard OpenAI API interface for seamless integration with existing tools and libraries
|
||||
</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input
|
||||
value={`${window.location.origin}/api/v1/chatbot/external/${editingChatbot.id}/chat/completions`}
|
||||
readOnly
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(`${window.location.origin}/api/v1/chatbot/external/${editingChatbot.id}/chat/completions`)
|
||||
toast({
|
||||
title: "Copied!",
|
||||
description: "OpenAI endpoint copied to clipboard"
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-blue-50 dark:bg-blue-950/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-start space-x-2">
|
||||
<div className="text-blue-600 dark:text-blue-400">
|
||||
<Globe className="h-4 w-4 mt-0.5" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-blue-900 dark:text-blue-100">
|
||||
Authentication Required
|
||||
</p>
|
||||
<p className="text-xs text-blue-700 dark:text-blue-200 mt-1">
|
||||
These endpoints require API key authentication. Create and manage API keys from the{" "}
|
||||
<button
|
||||
className="underline hover:no-underline"
|
||||
onClick={() => handleManageApiKeys(editingChatbot)}
|
||||
>
|
||||
API Keys page
|
||||
</button>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-medium">Example Usage</h4>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground">cURL Example</Label>
|
||||
<div className="relative">
|
||||
<pre className="text-xs bg-muted p-3 rounded-md overflow-x-auto">
|
||||
<code>{`curl -X POST "${window.location.origin}/api/v1/chatbot/external/${editingChatbot.id}/chat/completions" \\
|
||||
-H "Authorization: Bearer YOUR_API_KEY" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"messages": [{"role": "user", "content": "Hello!"}],
|
||||
"max_tokens": 150,
|
||||
"temperature": 0.7
|
||||
}'`}</code>
|
||||
</pre>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute top-2 right-2"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(`curl -X POST "${window.location.origin}/api/v1/chatbot/external/${editingChatbot.id}/chat/completions" \\
|
||||
-H "Authorization: Bearer YOUR_API_KEY" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"messages": [{"role": "user", "content": "Hello!"}],
|
||||
"max_tokens": 150,
|
||||
"temperature": 0.7
|
||||
}'`)
|
||||
toast({
|
||||
title: "Copied!",
|
||||
description: "cURL example copied to clipboard"
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="flex justify-end space-x-2 mt-6 pt-6 border-t">
|
||||
@@ -985,23 +1096,50 @@ export function ChatbotManager() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 mt-4 pt-4 border-t">
|
||||
<Button size="sm" className="flex-1" onClick={() => handleTestChat(chatbot)}>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
Test Chat
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleEditChat(chatbot)}>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleManageApiKeys(chatbot)}>
|
||||
<Key className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="outline">
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleDeleteChat(chatbot)}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="space-y-3 mt-4 pt-4 border-t">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button size="sm" className="flex-1" onClick={() => handleTestChat(chatbot)}>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
Test Chat
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleEditChat(chatbot)}>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleManageApiKeys(chatbot)}>
|
||||
<Key className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleDeleteChat(chatbot)}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground flex items-center">
|
||||
<Link className="h-3 w-3 mr-1" />
|
||||
Integration URL
|
||||
</Label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input
|
||||
value={`${window.location.origin}/api/v1/chatbot/external/${chatbot.id}/chat/completions`}
|
||||
readOnly
|
||||
className="font-mono text-xs h-8"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 px-2"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(`${window.location.origin}/api/v1/chatbot/external/${chatbot.id}/chat/completions`)
|
||||
toast({
|
||||
title: "Copied!",
|
||||
description: "Integration URL copied to clipboard"
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -79,7 +79,6 @@ const Navigation = () => {
|
||||
label: "Settings",
|
||||
children: [
|
||||
{ href: "/settings", label: "System Settings" },
|
||||
{ href: "/modules", label: "Modules" },
|
||||
{ href: "/plugins", label: "Plugins" },
|
||||
{ href: "/prompt-templates", label: "Prompt Templates" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user