botserver/ui/suite/base.html
Rodrigo Rodriguez (Pragmatismo) e68a12176d Add Suite app documentation, templates, and Askama config
- Add askama.toml for template configuration (ui/ directory)
- Add Suite app documentation with flow diagrams (SVG)
  - App launcher, chat flow, drive flow, tasks flow
  - Individual app docs: chat, drive, tasks, mail, etc.
- Add HTML templates for Suite apps
  - Base template with header and app launcher
  - Auth login page
  - Chat, Drive, Mail, Meet, Tasks templates
  - Partial templates for messages, sessions, notifications
- Add Extensions type to AppState for type-erased storage
- Add mTLS module for service-to-service authentication
- Update web handlers to use new template paths (suite/)
- Fix auth module to avoid axum-extra TypedHeader dependency
2025-11-30 21:00:48 -03:00

502 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}General Bots Suite{% endblock %}</title>
<!-- HTMX -->
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/ws.js"></script>
<!-- Styles -->
<link rel="stylesheet" href="/css/app.css">
<style>
:root {
--primary: #3b82f6;
--primary-hover: #2563eb;
--primary-light: rgba(59, 130, 246, 0.1);
--bg: #0f172a;
--surface: #1e293b;
--surface-hover: #334155;
--border: #334155;
--text: #f8fafc;
--text-secondary: #94a3b8;
--success: #22c55e;
--warning: #f59e0b;
--error: #ef4444;
--info: #3b82f6;
}
@media (prefers-color-scheme: light) {
:root {
--bg: #f8fafc;
--surface: #ffffff;
--surface-hover: #f1f5f9;
--border: #e2e8f0;
--text: #1e293b;
--text-secondary: #64748b;
}
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Header */
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1rem;
height: 64px;
background: var(--surface);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 1000;
}
.header-left {
display: flex;
align-items: center;
gap: 1rem;
}
.logo {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
font-size: 1.125rem;
color: var(--text);
text-decoration: none;
}
.logo-icon {
width: 32px;
height: 32px;
background: linear-gradient(135deg, var(--primary), #8b5cf6);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
}
.header-right {
display: flex;
align-items: center;
gap: 0.5rem;
}
.header-btn {
padding: 0.5rem;
background: transparent;
border: none;
color: var(--text-secondary);
border-radius: 8px;
cursor: pointer;
transition: background 0.2s, color 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.header-btn:hover {
background: var(--surface-hover);
color: var(--text);
}
.user-avatar {
width: 32px;
height: 32px;
background: var(--primary);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 500;
font-size: 0.875rem;
cursor: pointer;
}
/* Apps Menu */
.apps-menu {
position: relative;
}
.apps-dropdown {
position: absolute;
top: 100%;
right: 0;
margin-top: 0.5rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1rem;
min-width: 320px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
display: none;
z-index: 1001;
}
.apps-dropdown.show {
display: block;
}
.apps-dropdown-title {
font-size: 0.75rem;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.75rem;
}
.apps-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
}
.app-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.75rem;
border-radius: 8px;
text-decoration: none;
color: var(--text);
transition: background 0.2s;
}
.app-item:hover {
background: var(--surface-hover);
}
.app-item.active {
background: var(--primary-light);
color: var(--primary);
}
.app-item-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.app-item span {
font-size: 0.75rem;
font-weight: 500;
}
/* Main Content */
.app-main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
#main-content {
flex: 1;
overflow: auto;
}
/* HTMX Indicators */
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator {
display: inline-flex;
}
.htmx-request.htmx-indicator {
display: inline-flex;
}
/* Spinner */
.spinner {
width: 20px;
height: 20px;
border: 2px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Notifications */
.notifications-container {
position: fixed;
bottom: 1rem;
right: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
z-index: 2000;
max-width: 400px;
}
.notification {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 1rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: slideIn 0.3s ease-out;
}
.notification.success { border-left: 4px solid var(--success); }
.notification.error { border-left: 4px solid var(--error); }
.notification.warning { border-left: 4px solid var(--warning); }
.notification.info { border-left: 4px solid var(--info); }
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Utility Classes */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Responsive */
@media (max-width: 768px) {
.logo span {
display: none;
}
.apps-dropdown {
right: -1rem;
min-width: 280px;
}
}
</style>
{% block head %}{% endblock %}
</head>
<body>
<!-- Header -->
<header class="app-header">
<div class="header-left">
<a href="/" class="logo">
<div class="logo-icon">🤖</div>
<span>General Bots</span>
</a>
</div>
<div class="header-right">
<!-- Apps Menu -->
<div class="apps-menu">
<button class="header-btn" id="apps-btn" aria-label="Applications" aria-expanded="false">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<circle cx="5" cy="5" r="2"></circle>
<circle cx="12" cy="5" r="2"></circle>
<circle cx="19" cy="5" r="2"></circle>
<circle cx="5" cy="12" r="2"></circle>
<circle cx="12" cy="12" r="2"></circle>
<circle cx="19" cy="12" r="2"></circle>
<circle cx="5" cy="19" r="2"></circle>
<circle cx="12" cy="19" r="2"></circle>
<circle cx="19" cy="19" r="2"></circle>
</svg>
</button>
<nav class="apps-dropdown" id="apps-dropdown" role="menu">
<div class="apps-dropdown-title">Applications</div>
<div class="apps-grid">
<a href="#chat" class="app-item" role="menuitem" hx-get="/chat/chat.html" hx-target="#main-content" hx-push-url="true">
<div class="app-item-icon" style="background: linear-gradient(135deg, #3b82f6, #1d4ed8);">💬</div>
<span>Chat</span>
</a>
<a href="#drive" class="app-item" role="menuitem" hx-get="/drive/index.html" hx-target="#main-content" hx-push-url="true">
<div class="app-item-icon" style="background: linear-gradient(135deg, #f59e0b, #d97706);">📁</div>
<span>Drive</span>
</a>
<a href="#tasks" class="app-item" role="menuitem" hx-get="/tasks/tasks.html" hx-target="#main-content" hx-push-url="true">
<div class="app-item-icon" style="background: linear-gradient(135deg, #22c55e, #16a34a);"></div>
<span>Tasks</span>
</a>
<a href="#mail" class="app-item" role="menuitem" hx-get="/mail/mail.html" hx-target="#main-content" hx-push-url="true">
<div class="app-item-icon" style="background: linear-gradient(135deg, #ef4444, #dc2626);">✉️</div>
<span>Mail</span>
</a>
<a href="#calendar" class="app-item" role="menuitem" hx-get="/calendar/calendar.html" hx-target="#main-content" hx-push-url="true">
<div class="app-item-icon" style="background: linear-gradient(135deg, #a855f7, #7c3aed);">📅</div>
<span>Calendar</span>
</a>
<a href="#meet" class="app-item" role="menuitem" hx-get="/meet/meet.html" hx-target="#main-content" hx-push-url="true">
<div class="app-item-icon" style="background: linear-gradient(135deg, #06b6d4, #0891b2);">🎥</div>
<span>Meet</span>
</a>
<a href="#paper" class="app-item" role="menuitem" hx-get="/paper/paper.html" hx-target="#main-content" hx-push-url="true">
<div class="app-item-icon" style="background: linear-gradient(135deg, #eab308, #ca8a04);">📝</div>
<span>Paper</span>
</a>
<a href="#research" class="app-item" role="menuitem" hx-get="/research/research.html" hx-target="#main-content" hx-push-url="true">
<div class="app-item-icon" style="background: linear-gradient(135deg, #ec4899, #db2777);">🔍</div>
<span>Research</span>
</a>
<a href="#analytics" class="app-item" role="menuitem" hx-get="/analytics/analytics.html" hx-target="#main-content" hx-push-url="true">
<div class="app-item-icon" style="background: linear-gradient(135deg, #6366f1, #4f46e5);">📊</div>
<span>Analytics</span>
</a>
</div>
</nav>
</div>
<!-- Theme Toggle -->
<button class="header-btn" id="theme-btn" aria-label="Toggle theme">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</button>
<!-- User Avatar -->
<button class="user-avatar" aria-label="User menu">
{{ user_initial|default("U") }}
</button>
</div>
</header>
<!-- Main Content -->
<main class="app-main">
<div id="main-content">
{% block content %}{% endblock %}
</div>
</main>
<!-- Notifications Container -->
<div class="notifications-container" id="notifications"></div>
<script>
// Apps menu toggle
const appsBtn = document.getElementById('apps-btn');
const appsDropdown = document.getElementById('apps-dropdown');
appsBtn.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = appsDropdown.classList.toggle('show');
appsBtn.setAttribute('aria-expanded', isOpen);
});
document.addEventListener('click', (e) => {
if (!appsDropdown.contains(e.target) && !appsBtn.contains(e.target)) {
appsDropdown.classList.remove('show');
appsBtn.setAttribute('aria-expanded', 'false');
}
});
// Close on escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
appsDropdown.classList.remove('show');
appsBtn.setAttribute('aria-expanded', 'false');
}
});
// Keyboard shortcuts for apps
document.addEventListener('keydown', (e) => {
if (e.altKey && !e.ctrlKey && !e.shiftKey) {
const shortcuts = {
'1': 'chat',
'2': 'drive',
'3': 'tasks',
'4': 'mail',
'5': 'calendar',
'6': 'meet'
};
if (shortcuts[e.key]) {
e.preventDefault();
const link = document.querySelector(`a[href="#${shortcuts[e.key]}"]`);
if (link) link.click();
appsDropdown.classList.remove('show');
}
}
});
// Update active app in menu
document.body.addEventListener('htmx:afterSwap', (e) => {
if (e.detail.target.id === 'main-content') {
const hash = window.location.hash || '#chat';
document.querySelectorAll('.app-item').forEach(item => {
item.classList.toggle('active', item.getAttribute('href') === hash);
});
}
});
// Theme toggle
const themeBtn = document.getElementById('theme-btn');
themeBtn.addEventListener('click', () => {
document.body.classList.toggle('light-theme');
localStorage.setItem('theme', document.body.classList.contains('light-theme') ? 'light' : 'dark');
});
// Restore theme
if (localStorage.getItem('theme') === 'light') {
document.body.classList.add('light-theme');
}
// Notification helper
window.showNotification = function(message, type = 'info', duration = 5000) {
const container = document.getElementById('notifications');
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.innerHTML = `
<div class="notification-content">
<div class="notification-message">${message}</div>
</div>
<button class="notification-close" onclick="this.parentElement.remove()">×</button>
`;
container.appendChild(notification);
if (duration > 0) {
setTimeout(() => notification.remove(), duration);
}
};
</script>
{% block scripts %}{% endblock %}
</body>
</html>