botserver/src/designer/ui.rs
Rodrigo Rodriguez (Pragmatismo) 31777432b4 Implement TODO items: session auth, face API, task logs, intent storage
Learn Module:
- All 9 handlers now use AuthenticatedUser extractor

Security:
- validate_session_sync reads roles from SESSION_CACHE

AutoTask:
- get_task_logs reads from manifest with status logs
- store_compiled_intent saves to cache and database

Face API:
- AWS Rekognition, OpenCV, InsightFace implementations
- Detection, verification, analysis methods

Other fixes:
- Calendar/task integration database queries
- Recording database methods
- Analytics insights trends
- Email/folder monitoring mock data
2026-01-13 14:48:49 -03:00

220 lines
9.9 KiB
Rust

use axum::{
extract::{Path, State},
response::Html,
routing::get,
Router,
};
use std::sync::Arc;
use uuid::Uuid;
use crate::shared::state::AppState;
pub async fn handle_designer_list_page(
State(_state): State<Arc<AppState>>,
) -> Html<String> {
let html = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dialog Designer</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
.container { max-width: 1400px; margin: 0 auto; padding: 24px; }
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
.header h1 { font-size: 28px; color: #1a1a1a; }
.btn { padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; }
.btn-primary { background: #0066cc; color: white; }
.btn-primary:hover { background: #0052a3; }
.dialog-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 20px; }
.dialog-card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); cursor: pointer; }
.dialog-card:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.12); }
.dialog-icon { width: 48px; height: 48px; background: #e8f4ff; border-radius: 10px; display: flex; align-items: center; justify-content: center; margin-bottom: 12px; font-size: 24px; }
.dialog-name { font-size: 16px; font-weight: 600; color: #1a1a1a; margin-bottom: 8px; }
.dialog-meta { font-size: 12px; color: #999; }
.empty-state { text-align: center; padding: 80px 24px; color: #666; }
.empty-state h3 { margin-bottom: 8px; color: #1a1a1a; }
.search-box { padding: 10px 16px; border: 1px solid #ddd; border-radius: 8px; width: 300px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Dialog Designer</h1>
<div style="display: flex; gap: 12px;">
<input type="text" class="search-box" placeholder="Search dialogs..." id="searchInput">
<button class="btn btn-primary" onclick="window.location='/suite/designer/new'">New Dialog</button>
</div>
</div>
<div class="dialog-grid" id="dialogGrid">
<div class="empty-state"><h3>Loading...</h3></div>
</div>
</div>
<script>
async function loadDialogs() {
try {
const response = await fetch('/api/ui/designer/dialogs');
const data = await response.json();
renderDialogs(data.dialogs || data || []);
} catch (e) {
document.getElementById('dialogGrid').innerHTML = '<div class="empty-state"><h3>No dialogs yet</h3><p>Create your first dialog</p></div>';
}
}
function renderDialogs(dialogs) {
const grid = document.getElementById('dialogGrid');
if (!dialogs.length) {
grid.innerHTML = '<div class="empty-state"><h3>No dialogs yet</h3><p>Create your first dialog</p></div>';
return;
}
grid.innerHTML = dialogs.map(d => `
<div class="dialog-card" onclick="window.location='/suite/designer/edit/${d.id}'">
<div class="dialog-icon">💬</div>
<div class="dialog-name">${d.name}</div>
<div class="dialog-meta">Updated ${new Date(d.updated_at).toLocaleDateString()}</div>
</div>
`).join('');
}
loadDialogs();
</script>
</body>
</html>"#;
Html(html.to_string())
}
pub async fn handle_designer_edit_page(
State(_state): State<Arc<AppState>>,
Path(dialog_id): Path<Uuid>,
) -> Html<String> {
let html = format!(r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit Dialog</title>
<style>
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1e1e1e; color: #d4d4d4; height: 100vh; display: flex; flex-direction: column; }}
.toolbar {{ background: #2d2d2d; padding: 12px 20px; display: flex; justify-content: space-between; align-items: center; }}
.back-link {{ color: #0078d4; text-decoration: none; }}
.btn {{ padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; }}
.btn-primary {{ background: #0078d4; color: white; }}
.main {{ flex: 1; display: flex; }}
.sidebar {{ width: 250px; background: #252526; padding: 16px; }}
.canvas {{ flex: 1; background: #1e1e1e; position: relative; }}
.properties {{ width: 300px; background: #252526; padding: 16px; }}
.node-item {{ background: #2d2d2d; padding: 12px; margin-bottom: 8px; border-radius: 8px; cursor: grab; }}
.section-title {{ font-size: 12px; color: #888; margin-bottom: 12px; }}
.form-group {{ margin-bottom: 12px; }}
.form-group label {{ display: block; font-size: 12px; color: #888; margin-bottom: 4px; }}
.form-group input, .form-group textarea {{ width: 100%; padding: 8px; border: 1px solid #404040; background: #2d2d2d; color: #d4d4d4; border-radius: 4px; }}
</style>
</head>
<body>
<div class="toolbar">
<a href="/suite/designer" class="back-link">← Back</a>
<span id="dialogName">Loading...</span>
<button class="btn btn-primary" onclick="saveDialog()">Save</button>
</div>
<div class="main">
<div class="sidebar">
<div class="section-title">NODES</div>
<div class="node-item">💬 Message</div>
<div class="node-item">❓ Question</div>
<div class="node-item">🔀 Condition</div>
<div class="node-item">⚡ Action</div>
</div>
<div class="canvas" id="canvas"></div>
<div class="properties">
<div class="section-title">PROPERTIES</div>
<div id="propertiesContent">Select a node to edit</div>
</div>
</div>
<script>
const dialogId = '{dialog_id}';
async function loadDialog() {{
try {{
const response = await fetch('/api/ui/designer/dialogs/' + dialogId);
const data = await response.json();
document.getElementById('dialogName').textContent = data.name || 'Untitled';
}} catch (e) {{ console.error(e); }}
}}
async function saveDialog() {{
alert('Save functionality - implement based on canvas state');
}}
loadDialog();
</script>
</body>
</html>"#);
Html(html)
}
pub async fn handle_designer_new_page(
State(_state): State<Arc<AppState>>,
) -> Html<String> {
let html = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Create Dialog</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
.container { max-width: 600px; margin: 0 auto; padding: 24px; }
.back-link { color: #0066cc; text-decoration: none; display: inline-block; margin-bottom: 16px; }
.card { background: white; border-radius: 12px; padding: 32px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
h1 { font-size: 24px; margin-bottom: 24px; }
.form-group { margin-bottom: 20px; }
.form-group label { display: block; font-weight: 500; margin-bottom: 8px; }
.form-group input, .form-group textarea { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; }
.btn { padding: 12px 24px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; }
.btn-primary { background: #0066cc; color: white; }
</style>
</head>
<body>
<div class="container">
<a href="/suite/designer" class="back-link">← Back</a>
<div class="card">
<h1>Create New Dialog</h1>
<form id="createForm">
<div class="form-group">
<label>Name</label>
<input type="text" id="name" required placeholder="Dialog name">
</div>
<div class="form-group">
<label>Description</label>
<textarea id="description" placeholder="Description"></textarea>
</div>
<button type="submit" class="btn btn-primary">Create</button>
</form>
</div>
</div>
<script>
document.getElementById('createForm').addEventListener('submit', async (e) => {
e.preventDefault();
try {
const response = await fetch('/api/ui/designer/dialogs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: document.getElementById('name').value,
description: document.getElementById('description').value
})
});
const data = await response.json();
if (data.id) window.location = '/suite/designer/edit/' + data.id;
} catch (e) { alert('Error: ' + e.message); }
});
</script>
</body>
</html>"#;
Html(html.to_string())
}
pub fn configure_designer_ui_routes() -> Router<Arc<AppState>> {
Router::new()
.route("/suite/designer", get(handle_designer_list_page))
.route("/suite/designer/new", get(handle_designer_new_page))
.route("/suite/designer/edit/:id", get(handle_designer_edit_page))
}