Phase 6: Web UI for configuration management and enhanced API server

This commit is contained in:
truetype committed 2026-03-20 18:25:22 +01:00
1 parent 5541770896
commit 6eace3abc0
3 files changed
+733 -11

No files matched your search

+21 -2
View File
@@ -15,6 +15,7 @@ Un agente configurabile basato su DeepAgents che supporta più provider LLM.
- **Test completi**: Suite di test per garantire la qualità del codice
- **Docker support**: Containerizzazione per deployment facile
- **API REST**: Server API per accesso remoto
- **Web UI**: Interfaccia web per gestione configurazioni
## Installazione
@@ -98,7 +99,7 @@ export OPENROUTER_API_KEY="la-tua-chiave-api-openrouter"
### 2. Configurazione Persistente (Consigliato)
Utilizza la CLI o la TUI per gestire le configurazioni che persistono tra le sessioni.
Utilizza la CLI, la TUI o l'interfaccia web per gestire le configurazioni che persistono tra le sessioni.
#### Utilizzo della CLI
```bash
@@ -147,6 +148,24 @@ La TUI ora include:
- Test di connessione diretto
- Navigazione migliorata
#### Utilizzo dell'Interfaccia Web
Avvia il server API e accedi all'interfaccia web:
```bash
# Avvia il server API
python api_server.py
# Accedi all'interfaccia web
# http://localhost:8000
```
L'interfaccia web include:
- Gestione completa delle configurazioni
- Chat con l'agente
- Monitoraggio dello stato del sistema
- Test di connessione
## Utilizzo
### Utilizzo Locale
@@ -182,7 +201,7 @@ L'API sarà disponibile su http://localhost:8000
#### Endpoint API Disponibili
- `GET /`: Informazioni sull'API
- `GET /`: Interfaccia web
- `GET /health`: Health check
- `POST /chat`: Chat con l'agente
- `GET /configurations`: Elenca tutte le configurazioni
+21 -9
View File
@@ -7,6 +7,8 @@ import sys
from typing import Optional, Dict, Any, List
from fastapi import FastAPI, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel
import uvicorn
@@ -33,6 +35,11 @@ app.add_middleware(
allow_headers=["*"],
)
# Mount static files for web UI
web_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "web")
if os.path.exists(web_dir):
app.mount("/static", StaticFiles(directory=web_dir), name="static")
# Global agent instance
agent = None
@@ -95,16 +102,21 @@ async def startup_event():
@app.get("/")
async def root():
"""Root endpoint"""
return {
"message": "MaxClaw Agent API",
"version": "1.0.0",
"endpoints": {
"chat": "/chat",
"configurations": "/configurations",
"health": "/health"
"""Root endpoint - serve web UI"""
web_index = os.path.join(web_dir, "index.html")
if os.path.exists(web_index):
return FileResponse(web_index)
else:
return {
"message": "MaxClaw Agent API",
"version": "1.0.0",
"endpoints": {
"chat": "/chat",
"configurations": "/configurations",
"health": "/health",
"web_ui": "/static/index.html"
}
}
}
@app.get("/health")
+691
View File
@@ -0,0 +1,691 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MaxClaw Agent - Configurazione</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
header {
text-align: center;
color: white;
margin-bottom: 30px;
}
header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
}
header p {
font-size: 1.1rem;
opacity: 0.9;
}
.card {
background: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
padding: 30px;
margin-bottom: 30px;
}
.card h2 {
color: #333;
margin-bottom: 20px;
border-bottom: 2px solid #667eea;
padding-bottom: 10px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #555;
}
input, select, textarea {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 6px;
font-size: 16px;
transition: border-color 0.3s;
}
input:focus, select:focus, textarea:focus {
border-color: #667eea;
outline: none;
}
.btn {
background: #667eea;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: background 0.3s;
}
.btn:hover {
background: #5a6fd8;
}
.btn-secondary {
background: #6c757d;
}
.btn-secondary:hover {
background: #5a6268;
}
.btn-danger {
background: #dc3545;
}
.btn-danger:hover {
background: #c82333;
}
.config-list {
margin-top: 30px;
}
.config-item {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 15px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.config-info h3 {
color: #333;
margin-bottom: 5px;
}
.config-info p {
color: #666;
font-size: 14px;
}
.config-actions {
display: flex;
gap: 10px;
}
.status-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
margin-left: 10px;
}
.status-active {
background: #d4edda;
color: #155724;
}
.status-inactive {
background: #f8d7da;
color: #721c24;
}
.chat-container {
height: 400px;
border: 1px solid #ddd;
border-radius: 6px;
padding: 15px;
overflow-y: auto;
margin-bottom: 20px;
background: #f8f9fa;
}
.message {
margin-bottom: 15px;
padding: 10px 15px;
border-radius: 18px;
max-width: 80%;
}
.user-message {
background: #667eea;
color: white;
margin-left: auto;
border-bottom-right-radius: 4px;
}
.agent-message {
background: white;
border: 1px solid #ddd;
border-bottom-left-radius: 4px;
}
.chat-input {
display: flex;
gap: 10px;
}
.chat-input input {
flex: 1;
}
.alert {
padding: 15px;
border-radius: 6px;
margin-bottom: 20px;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.hidden {
display: none;
}
.tabs {
display: flex;
border-bottom: 2px solid #dee2e6;
margin-bottom: 20px;
}
.tab {
padding: 10px 20px;
cursor: pointer;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
}
.tab.active {
border-bottom-color: #667eea;
font-weight: 600;
color: #667eea;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>MaxClaw Agent</h1>
<p>Interfaccia di configurazione per l'agente AI</p>
</header>
<div class="card">
<div class="tabs">
<div class="tab active" onclick="switchTab('config')">Configurazione</div>
<div class="tab" onclick="switchTab('chat')">Chat</div>
<div class="tab" onclick="switchTab('status')">Stato</div>
</div>
<div id="config-tab" class="tab-content active">
<h2>Gestione Configurazioni</h2>
<div id="alert-container"></div>
<form id="config-form">
<div class="form-group">
<label for="config-name">Nome Configurazione</label>
<input type="text" id="config-name" placeholder="es. mio-ollama" required>
</div>
<div class="form-group">
<label for="config-provider">Provider</label>
<select id="config-provider" required>
<option value="">Seleziona un provider</option>
<option value="ollama">Ollama</option>
<option value="openai">OpenAI</option>
<option value="openrouter">OpenRouter</option>
</select>
</div>
<div class="form-group">
<label for="config-model">Nome Modello</label>
<input type="text" id="config-model" placeholder="es. gemma2:9b" required>
</div>
<div class="form-group" id="api-key-group">
<label for="config-api-key">Chiave API</label>
<input type="password" id="config-api-key" placeholder="Inserisci la chiave API">
</div>
<div class="form-group" id="base-url-group">
<label for="config-base-url">URL Base</label>
<input type="text" id="config-base-url" placeholder="http://localhost:11434">
</div>
<div class="form-group">
<label>
<input type="checkbox" id="config-set-active">
Imposta come configurazione attiva
</label>
</div>
<button type="submit" class="btn">Salva Configurazione</button>
<button type="button" class="btn btn-secondary" onclick="clearForm()">Pulisci</button>
</form>
<div class="config-list">
<h3>Configurazioni Esistenti</h3>
<div id="config-list-container">
<!-- Configurations will be loaded here -->
</div>
</div>
</div>
<div id="chat-tab" class="tab-content">
<h2>Chat con MaxClaw</h2>
<div class="chat-container" id="chat-container">
<div class="message agent-message">
Ciao! Sono MaxClaw, il tuo agente AI. Come posso aiutarti oggi?
</div>
</div>
<div class="chat-input">
<input type="text" id="chat-input" placeholder="Scrivi un messaggio...">
<button class="btn" onclick="sendMessage()">Invia</button>
</div>
</div>
<div id="status-tab" class="tab-content">
<h2>Stato del Sistema</h2>
<div id="status-container">
<div class="alert alert-success">
<strong>✅ Sistema operativo</strong><br>
L'agente è pronto per l'uso.
</div>
<div class="config-item">
<div class="config-info">
<h3>Provider Attivo</h3>
<p id="active-provider">Nessun provider configurato</p>
</div>
</div>
<div class="config-item">
<div class="config-info">
<h3>Modello Attivo</h3>
<p id="active-model">Nessun modello configurato</p>
</div>
</div>
<div class="config-item">
<div class="config-info">
<h3>Connessione</h3>
<p id="connection-status">Verifica in corso...</p>
</div>
<button class="btn btn-secondary" onclick="testConnection()">Testa Connessione</button>
</div>
</div>
</div>
</div>
</div>
<script>
// API base URL
const API_BASE = 'http://localhost:8000';
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadConfigurations();
loadActiveConfiguration();
// Handle form submission
document.getElementById('config-form').addEventListener('submit', function(e) {
e.preventDefault();
saveConfiguration();
});
// Handle provider change
document.getElementById('config-provider').addEventListener('change', function() {
updateFormFields();
});
// Handle chat input
document.getElementById('chat-input').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendMessage();
}
});
});
// Switch tabs
function switchTab(tabName) {
// Update tab buttons
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
event.target.classList.add('active');
// Update tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(tabName + '-tab').classList.add('active');
}
// Load configurations
async function loadConfigurations() {
try {
const response = await fetch(API_BASE + '/configurations');
const configs = await response.json();
const container = document.getElementById('config-list-container');
container.innerHTML = '';
if (configs.length === 0) {
container.innerHTML = '<p>Nessuna configurazione trovata.</p>';
return;
}
configs.forEach(config => {
const item = document.createElement('div');
item.className = 'config-item';
item.innerHTML = `
<div class="config-info">
<h3>${config.name}
${config.is_active ? '<span class="status-badge status-active">Attivo</span>' : ''}
</h3>
<p>Provider: ${config.provider} | Modello: ${config.model_name}</p>
</div>
<div class="config-actions">
<button class="btn btn-secondary" onclick="activateConfig('${config.name}')">Attiva</button>
<button class="btn btn-secondary" onclick="testConfig('${config.name}')">Testa</button>
<button class="btn btn-danger" onclick="deleteConfig('${config.name}')">Elimina</button>
</div>
`;
container.appendChild(item);
});
} catch (error) {
showAlert('Errore nel caricamento delle configurazioni: ' + error.message, 'error');
}
}
// Load active configuration
async function loadActiveConfiguration() {
try {
const response = await fetch(API_BASE + '/configurations/active');
const config = await response.json();
if (config.name) {
document.getElementById('active-provider').textContent = config.provider;
document.getElementById('active-model').textContent = config.model_name;
} else {
document.getElementById('active-provider').textContent = 'Nessun provider configurato';
document.getElementById('active-model').textContent = 'Nessun modello configurato';
}
} catch (error) {
console.error('Error loading active configuration:', error);
}
}
// Save configuration
async function saveConfiguration() {
const name = document.getElementById('config-name').value;
const provider = document.getElementById('config-provider').value;
const model = document.getElementById('config-model').value;
const apiKey = document.getElementById('config-api-key').value;
const baseUrl = document.getElementById('config-base-url').value;
const setActive = document.getElementById('config-set-active').checked;
const config = {
name: name,
provider: provider,
model_name: model,
set_active: setActive
};
if (apiKey) {
config.api_key = apiKey;
}
if (baseUrl) {
config.base_url = baseUrl;
}
try {
const response = await fetch(API_BASE + '/configurations', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(config)
});
if (response.ok) {
showAlert('Configurazione salvata con successo!', 'success');
clearForm();
loadConfigurations();
loadActiveConfiguration();
} else {
const error = await response.json();
showAlert('Errore: ' + error.detail, 'error');
}
} catch (error) {
showAlert('Errore di connessione: ' + error.message, 'error');
}
}
// Activate configuration
async function activateConfig(name) {
try {
const response = await fetch(API_BASE + `/configurations/${name}/activate`, {
method: 'POST'
});
if (response.ok) {
showAlert(`Configurazione "${name}" attivata!`, 'success');
loadConfigurations();
loadActiveConfiguration();
} else {
const error = await response.json();
showAlert('Errore: ' + error.detail, 'error');
}
} catch (error) {
showAlert('Errore di connessione: ' + error.message, 'error');
}
}
// Test configuration
async function testConfig(name) {
try {
const response = await fetch(API_BASE + `/configurations/${name}/test`, {
method: 'POST'
});
const result = await response.json();
if (result.success) {
showAlert(`✅ ${result.message}`, 'success');
} else {
showAlert(`❌ ${result.message}`, 'error');
}
} catch (error) {
showAlert('Errore di connessione: ' + error.message, 'error');
}
}
// Delete configuration
async function deleteConfig(name) {
if (!confirm(`Sei sicuro di voler eliminare la configurazione "${name}"?`)) {
return;
}
try {
const response = await fetch(API_BASE + `/configurations/${name}`, {
method: 'DELETE'
});
if (response.ok) {
showAlert(`Configurazione "${name}" eliminata!`, 'success');
loadConfigurations();
loadActiveConfiguration();
} else {
const error = await response.json();
showAlert('Errore: ' + error.detail, 'error');
}
} catch (error) {
showAlert('Errore di connessione: ' + error.message, 'error');
}
}
// Test connection
async function testConnection() {
document.getElementById('connection-status').textContent = 'Test in corso...';
try {
const response = await fetch(API_BASE + '/health');
const result = await response.json();
if (result.status === 'healthy') {
document.getElementById('connection-status').textContent = '✅ Connessione riuscita';
showAlert('Connessione al server riuscita!', 'success');
} else {
document.getElementById('connection-status').textContent = '❌ Connessione fallita';
showAlert('Connessione al server fallita', 'error');
}
} catch (error) {
document.getElementById('connection-status').textContent = '❌ Errore di connessione';
showAlert('Errore di connessione: ' + error.message, 'error');
}
}
// Send chat message
async function sendMessage() {
const input = document.getElementById('chat-input');
const message = input.value.trim();
if (!message) return;
// Add user message to chat
addMessageToChat(message, 'user');
input.value = '';
try {
const response = await fetch(API_BASE + '/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ message: message })
});
const result = await response.json();
// Add agent response to chat
addMessageToChat(result.response, 'agent');
} catch (error) {
addMessageToChat('Errore: Impossibile connettersi al server', 'agent');
}
}
// Add message to chat
function addMessageToChat(message, sender) {
const container = document.getElementById('chat-container');
const messageDiv = document.createElement('div');
messageDiv.className = `message ${sender}-message`;
messageDiv.textContent = message;
container.appendChild(messageDiv);
container.scrollTop = container.scrollHeight;
}
// Update form fields based on provider
function updateFormFields() {
const provider = document.getElementById('config-provider').value;
const apiKeyGroup = document.getElementById('api-key-group');
const baseUrlGroup = document.getElementById('base-url-group');
const baseUrlInput = document.getElementById('config-base-url');
// Show/hide fields based on provider
if (provider === 'openai' || provider === 'openrouter') {
apiKeyGroup.style.display = 'block';
} else {
apiKeyGroup.style.display = 'none';
}
if (provider === 'ollama' || provider === 'openrouter') {
baseUrlGroup.style.display = 'block';
if (provider === 'ollama') {
baseUrlInput.placeholder = 'http://192.168.0.50:11434';
} else {
baseUrlInput.placeholder = 'https://openrouter.ai/api/v1';
}
} else {
baseUrlGroup.style.display = 'none';
}
}
// Clear form
function clearForm() {
document.getElementById('config-form').reset();
updateFormFields();
}
// Show alert
function showAlert(message, type) {
const container = document.getElementById('alert-container');
const alert = document.createElement('div');
alert.className = `alert alert-${type}`;
alert.textContent = message;
// Remove existing alerts
container.innerHTML = '';
container.appendChild(alert);
// Auto-remove after 5 seconds
setTimeout(() => {
if (alert.parentNode) {
alert.parentNode.removeChild(alert);
}
}, 5000);
}
</script>
</body>
</html>