test/moxie/web/templates/profile.html
Z User 18301108b3 Add OpenWebUI features and integrate admin settings into profile
- Enhanced chat UI with visible action buttons (TTS, delete, regenerate, thumbs up/down)
- Added admin settings section to profile page for admin users
- Added admin API endpoints for user management, config, and RAG documents
- Removed need for separate admin endpoint - admins access settings via profile
2026-03-24 15:02:47 +00:00

667 lines
34 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Profile - MOXIE</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
background: '#0B0C1C',
surface: '#1A1A1A',
'balloon-red': '#EA744C',
'balloon-yellow': '#ECDC67',
'balloon-blue': '#4F9AC3',
'balloon-purple': '#6A4477',
'text-main': '#F0F0F0',
}
}
}
}
</script>
</head>
<body class="bg-background text-text-main min-h-screen">
<!-- Header -->
<header class="border-b border-balloon-purple/20 bg-surface/50">
<div class="max-w-4xl mx-auto px-6 py-4 flex items-center justify-between">
<a href="/chat" class="flex items-center gap-3 hover:opacity-80 transition-opacity">
<img src="/static/moxie_logo.jpg" alt="MOXIE" class="w-8 h-8 rounded-full">
<span class="font-bold">MOXIE</span>
</a>
<a href="/chat" class="text-sm text-text-main/50 hover:text-text-main transition-colors">← Back to Chat</a>
</div>
</header>
<!-- Main content -->
<main class="max-w-4xl mx-auto px-6 py-8">
<!-- Profile header -->
<div class="flex items-center gap-6 mb-8">
<div class="w-20 h-20 bg-balloon-purple/30 rounded-full flex items-center justify-center text-3xl font-bold">
{{ user.username[0].upper() }}
</div>
<div>
<h1 class="text-2xl font-bold">{{ user.username }}</h1>
<p class="text-text-main/50">{{ user.email }}</p>
{% if user.is_admin %}
<span class="inline-block mt-1 px-2 py-0.5 bg-balloon-yellow/20 text-balloon-yellow text-xs rounded-full">Admin</span>
{% endif %}
</div>
</div>
<!-- Stats -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<div class="bg-surface/50 rounded-xl p-5 border border-balloon-purple/20">
<div class="text-3xl font-bold text-balloon-blue">{{ rate_info.remaining }}</div>
<div class="text-sm text-text-main/50">Requests remaining today</div>
</div>
<div class="bg-surface/50 rounded-xl p-5 border border-balloon-purple/20">
<div class="text-3xl font-bold text-balloon-yellow">{{ user.request_limit }}</div>
<div class="text-sm text-text-main/50">Daily request limit</div>
</div>
<div class="bg-surface/50 rounded-xl p-5 border border-balloon-purple/20">
<div class="text-3xl font-bold text-balloon-purple">{{ documents|length }}</div>
<div class="text-sm text-text-main/50">Documents uploaded</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Change password -->
<div class="bg-surface/50 rounded-xl p-6 border border-balloon-purple/20">
<h2 class="text-lg font-semibold mb-4 flex items-center gap-2">
<svg class="w-5 h-5 text-balloon-red" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
</svg>
Change Password
</h2>
<form id="passwordForm" class="space-y-4">
<div>
<label class="block text-sm text-text-main/70 mb-1">Current Password</label>
<input type="password" name="current_password" required class="w-full px-4 py-2 bg-background/50 border border-balloon-purple/30 rounded-lg focus:outline-none focus:border-balloon-blue transition-colors">
</div>
<div>
<label class="block text-sm text-text-main/70 mb-1">New Password</label>
<input type="password" name="new_password" required minlength="6" class="w-full px-4 py-2 bg-background/50 border border-balloon-purple/30 rounded-lg focus:outline-none focus:border-balloon-blue transition-colors">
</div>
<button type="submit" class="w-full py-2 bg-balloon-red hover:bg-balloon-red/80 text-white font-medium rounded-lg transition-colors">
Update Password
</button>
</form>
</div>
<!-- Upload documents -->
<div class="bg-surface/50 rounded-xl p-6 border border-balloon-purple/20">
<h2 class="text-lg font-semibold mb-4 flex items-center gap-2">
<svg class="w-5 h-5 text-balloon-blue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
My Documents
</h2>
<p class="text-sm text-text-main/50 mb-4">Upload documents to use in your conversations with MOXIE.</p>
<!-- Upload area -->
<div id="uploadArea" class="border-2 border-dashed border-balloon-purple/30 rounded-lg p-6 text-center cursor-pointer hover:border-balloon-blue transition-colors mb-4">
<input type="file" id="docInput" class="hidden" accept=".pdf,.doc,.docx,.txt,.md">
<svg class="w-10 h-10 mx-auto text-text-main/30 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
</svg>
<p class="text-sm text-text-main/50">Click to upload or drag and drop</p>
<p class="text-xs text-text-main/30 mt-1">PDF, DOC, DOCX, TXT, MD</p>
</div>
<!-- Documents list -->
<div id="documentsList" class="space-y-2">
{% for doc in documents %}
<div class="flex items-center justify-between p-3 bg-background/30 rounded-lg">
<div class="flex items-center gap-3">
<span class="text-xl">📄</span>
<div>
<div class="text-sm font-medium">{{ doc.filename }}</div>
<div class="text-xs text-text-main/40">{{ doc.size|filesizeformat }}</div>
</div>
</div>
<button onclick="deleteDocument('{{ doc.id }}')" class="p-2 text-balloon-red hover:bg-balloon-red/20 rounded-lg transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
{% endfor %}
{% if not documents %}
<p class="text-center text-text-main/30 text-sm py-4">No documents uploaded yet</p>
{% endif %}
</div>
</div>
</div>
<!-- Account info -->
<div class="mt-6 bg-surface/50 rounded-xl p-6 border border-balloon-purple/20">
<h2 class="text-lg font-semibold mb-4 flex items-center gap-2">
<svg class="w-5 h-5 text-balloon-yellow" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Account Information
</h2>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="text-text-main/50">Member since:</span>
<span class="ml-2">{{ user.created_at[:10] if user.created_at else 'N/A' }}</span>
</div>
<div>
<span class="text-text-main/50">Account type:</span>
<span class="ml-2">{{ 'Administrator' if user.is_admin else 'Free (5 req/day)' }}</span>
</div>
</div>
</div>
{% if user.is_admin %}
<!-- Admin Settings Section -->
<div class="mt-8 border-t border-balloon-purple/20 pt-8">
<h2 class="text-xl font-bold mb-6 flex items-center gap-2 text-balloon-yellow">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
Admin Settings
</h2>
<!-- System Status -->
<div class="bg-surface/50 rounded-xl p-6 border border-balloon-purple/20 mb-6">
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
<svg class="w-5 h-5 text-balloon-blue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
</svg>
System Status
</h3>
<div id="systemStatus" class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="bg-background/30 rounded-lg p-4 text-center">
<div class="text-sm text-text-main/50 mb-1">Ollama</div>
<div id="ollamaStatus" class="flex items-center justify-center gap-2">
<span class="w-2 h-2 bg-gray-500 rounded-full animate-pulse"></span>
<span class="text-sm">Checking...</span>
</div>
</div>
<div class="bg-background/30 rounded-lg p-4 text-center">
<div class="text-sm text-text-main/50 mb-1">ComfyUI</div>
<div id="comfyuiStatus" class="flex items-center justify-center gap-2">
<span class="w-2 h-2 bg-gray-500 rounded-full animate-pulse"></span>
<span class="text-sm">Checking...</span>
</div>
</div>
<div class="bg-background/30 rounded-lg p-4 text-center">
<div class="text-sm text-text-main/50 mb-1">Documents</div>
<div id="docsCount" class="text-xl font-bold text-balloon-purple">-</div>
</div>
<div class="bg-background/30 rounded-lg p-4 text-center">
<div class="text-sm text-text-main/50 mb-1">Total Users</div>
<div id="usersCount" class="text-xl font-bold text-balloon-yellow">-</div>
</div>
</div>
</div>
<!-- User Management -->
<div class="bg-surface/50 rounded-xl p-6 border border-balloon-purple/20 mb-6">
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
<svg class="w-5 h-5 text-balloon-red" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>
</svg>
User Management
</h3>
<div id="usersList" class="space-y-2 max-h-64 overflow-y-auto">
<div class="text-center text-text-main/30 py-4">Loading users...</div>
</div>
</div>
<!-- API Configuration -->
<div class="bg-surface/50 rounded-xl p-6 border border-balloon-purple/20 mb-6">
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
<svg class="w-5 h-5 text-balloon-purple" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path>
</svg>
API Configuration
</h3>
<form id="configForm" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm text-text-main/70 mb-1">Gemini API Key</label>
<input type="password" name="gemini_api_key" placeholder="AIza..." class="w-full px-4 py-2 bg-background/50 border border-balloon-purple/30 rounded-lg focus:outline-none focus:border-balloon-blue transition-colors">
</div>
<div>
<label class="block text-sm text-text-main/70 mb-1">Gemini Model</label>
<input type="text" name="gemini_model" value="gemini-1.5-flash" class="w-full px-4 py-2 bg-background/50 border border-balloon-purple/30 rounded-lg focus:outline-none focus:border-balloon-blue transition-colors">
</div>
<div>
<label class="block text-sm text-text-main/70 mb-1">OpenRouter API Key</label>
<input type="password" name="openrouter_api_key" placeholder="sk-or-..." class="w-full px-4 py-2 bg-background/50 border border-balloon-purple/30 rounded-lg focus:outline-none focus:border-balloon-blue transition-colors">
</div>
<div>
<label class="block text-sm text-text-main/70 mb-1">OpenRouter Model</label>
<input type="text" name="openrouter_model" value="meta-llama/llama-3-8b-instruct:free" class="w-full px-4 py-2 bg-background/50 border border-balloon-purple/30 rounded-lg focus:outline-none focus:border-balloon-blue transition-colors">
</div>
<div>
<label class="block text-sm text-text-main/70 mb-1">ComfyUI Host</label>
<input type="text" name="comfyui_host" value="http://127.0.0.1:8188" class="w-full px-4 py-2 bg-background/50 border border-balloon-purple/30 rounded-lg focus:outline-none focus:border-balloon-blue transition-colors">
</div>
<div>
<label class="block text-sm text-text-main/70 mb-1">Ollama Host</label>
<input type="text" name="ollama_host" value="http://127.0.0.1:11434" class="w-full px-4 py-2 bg-background/50 border border-balloon-purple/30 rounded-lg focus:outline-none focus:border-balloon-blue transition-colors">
</div>
</div>
<button type="submit" class="px-6 py-2 bg-balloon-purple hover:bg-balloon-purple/80 text-white font-medium rounded-lg transition-colors">
Save Configuration
</button>
</form>
</div>
<!-- RAG Documents -->
<div class="bg-surface/50 rounded-xl p-6 border border-balloon-purple/20">
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
<svg class="w-5 h-5 text-balloon-blue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
Knowledge Base Documents
</h3>
<p class="text-sm text-text-main/50 mb-4">Upload documents to the RAG knowledge base for all users.</p>
<!-- Upload area -->
<div id="adminUploadArea" class="border-2 border-dashed border-balloon-purple/30 rounded-lg p-6 text-center cursor-pointer hover:border-balloon-blue transition-colors mb-4">
<input type="file" id="adminDocInput" class="hidden" accept=".pdf,.doc,.docx,.txt,.md">
<svg class="w-10 h-10 mx-auto text-text-main/30 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
</svg>
<p class="text-sm text-text-main/50">Click to upload documents to knowledge base</p>
</div>
<!-- Documents list -->
<div id="ragDocumentsList" class="space-y-2">
<div class="text-center text-text-main/30 text-sm py-4">Loading documents...</div>
</div>
</div>
</div>
{% endif %}
</main>
<script>
// Password change
document.getElementById('passwordForm').addEventListener('submit', async (e) => {
e.preventDefault();
const form = e.target;
const data = {
current_password: form.current_password.value,
new_password: form.new_password.value
};
try {
const response = await fetch('/api/profile/password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
alert('Password updated successfully!');
form.reset();
} else {
alert(result.error || 'Failed to update password');
}
} catch (error) {
alert('An error occurred. Please try again.');
}
});
// Document upload
const uploadArea = document.getElementById('uploadArea');
const docInput = document.getElementById('docInput');
uploadArea.addEventListener('click', () => docInput.click());
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('border-balloon-blue');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('border-balloon-blue');
});
uploadArea.addEventListener('drop', async (e) => {
e.preventDefault();
uploadArea.classList.remove('border-balloon-blue');
const files = e.dataTransfer.files;
for (const file of files) {
await uploadDocument(file);
}
});
docInput.addEventListener('change', async (e) => {
for (const file of e.target.files) {
await uploadDocument(file);
}
});
async function uploadDocument(file) {
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/api/profile/documents', {
method: 'POST',
body: formData
});
if (response.ok) {
window.location.reload();
} else {
alert('Failed to upload document');
}
} catch (error) {
alert('An error occurred during upload');
}
}
async function deleteDocument(docId) {
if (!confirm('Delete this document?')) return;
try {
const response = await fetch(`/api/profile/documents/${docId}`, {
method: 'DELETE'
});
if (response.ok) {
window.location.reload();
}
} catch (error) {
alert('Failed to delete document');
}
}
// Admin functionality
{% if user.is_admin %}
// Load admin data
document.addEventListener('DOMContentLoaded', () => {
loadSystemStatus();
loadUsers();
loadConfig();
loadRAGDocuments();
setupAdminUpload();
});
async function loadSystemStatus() {
try {
const response = await fetch('/api/admin/status');
const data = await response.json();
// Update status indicators
updateStatusIndicator('ollamaStatus', data.ollama);
updateStatusIndicator('comfyuiStatus', data.comfyui);
document.getElementById('docsCount').textContent = data.documents_count || 0;
document.getElementById('usersCount').textContent = data.users_count || '-';
} catch (error) {
console.error('Failed to load status:', error);
}
}
function updateStatusIndicator(elementId, status) {
const el = document.getElementById(elementId);
if (!el) return;
const statusColors = {
'connected': 'bg-green-500',
'disconnected': 'bg-red-500',
'error': 'bg-yellow-500',
'unknown': 'bg-gray-500'
};
el.innerHTML = `
<span class="w-2 h-2 ${statusColors[status] || statusColors.unknown} rounded-full"></span>
<span class="text-sm capitalize">${status || 'Unknown'}</span>
`;
}
async function loadUsers() {
try {
const response = await fetch('/api/admin/users');
const users = await response.json();
const list = document.getElementById('usersList');
list.innerHTML = '';
users.forEach(user => {
const el = document.createElement('div');
el.className = 'flex items-center justify-between p-3 bg-background/30 rounded-lg';
el.innerHTML = `
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-balloon-purple/30 rounded-full flex items-center justify-center text-sm">
${user.username[0].toUpperCase()}
</div>
<div>
<div class="text-sm font-medium">${user.username}</div>
<div class="text-xs text-text-main/40">${user.email}</div>
</div>
</div>
<div class="flex items-center gap-2">
${user.is_admin
? '<span class="px-2 py-0.5 bg-balloon-yellow/20 text-balloon-yellow text-xs rounded-full">Admin</span>'
: `<button onclick="promoteUser('${user.id}')" class="px-2 py-1 text-xs bg-balloon-purple/20 hover:bg-balloon-purple/40 rounded transition-colors">Promote</button>`
}
<button onclick="updateUserLimit('${user.id}')" class="px-2 py-1 text-xs bg-surface hover:bg-surface/80 rounded transition-colors" title="Set daily limit">
${user.request_limit}/day
</button>
<button onclick="deleteUser('${user.id}')" class="p-1 text-balloon-red hover:bg-balloon-red/20 rounded transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
`;
list.appendChild(el);
});
} catch (error) {
console.error('Failed to load users:', error);
document.getElementById('usersList').innerHTML = '<div class="text-center text-text-main/30 py-4">Failed to load users</div>';
}
}
async function loadConfig() {
try {
const response = await fetch('/api/admin/config');
const config = await response.json();
const form = document.getElementById('configForm');
if (config.gemini_model) form.querySelector('[name="gemini_model"]').value = config.gemini_model;
if (config.openrouter_model) form.querySelector('[name="openrouter_model"]').value = config.openrouter_model;
if (config.comfyui_host) form.querySelector('[name="comfyui_host"]').value = config.comfyui_host;
if (config.ollama_host) form.querySelector('[name="ollama_host"]').value = config.ollama_host;
} catch (error) {
console.error('Failed to load config:', error);
}
}
async function loadRAGDocuments() {
try {
const response = await fetch('/api/admin/documents');
const documents = await response.json();
const list = document.getElementById('ragDocumentsList');
list.innerHTML = '';
if (documents.length === 0) {
list.innerHTML = '<div class="text-center text-text-main/30 text-sm py-4">No documents in knowledge base</div>';
return;
}
documents.forEach(doc => {
const el = document.createElement('div');
el.className = 'flex items-center justify-between p-3 bg-background/30 rounded-lg';
el.innerHTML = `
<div class="flex items-center gap-3">
<span class="text-xl">📄</span>
<div>
<div class="text-sm font-medium">${doc.filename || doc.id}</div>
<div class="text-xs text-text-main/40">${doc.chunks || 0} chunks</div>
</div>
</div>
<button onclick="deleteRAGDocument('${doc.id}')" class="p-2 text-balloon-red hover:bg-balloon-red/20 rounded-lg transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
`;
list.appendChild(el);
});
} catch (error) {
console.error('Failed to load RAG documents:', error);
}
}
function setupAdminUpload() {
const uploadArea = document.getElementById('adminUploadArea');
const docInput = document.getElementById('adminDocInput');
if (!uploadArea || !docInput) return;
uploadArea.addEventListener('click', () => docInput.click());
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('border-balloon-blue');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('border-balloon-blue');
});
uploadArea.addEventListener('drop', async (e) => {
e.preventDefault();
uploadArea.classList.remove('border-balloon-blue');
for (const file of e.dataTransfer.files) {
await uploadRAGDocument(file);
}
});
docInput.addEventListener('change', async (e) => {
for (const file of e.target.files) {
await uploadRAGDocument(file);
}
});
}
async function uploadRAGDocument(file) {
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/api/admin/documents/upload', {
method: 'POST',
body: formData
});
if (response.ok) {
loadRAGDocuments();
loadSystemStatus();
alert('Document uploaded successfully!');
} else {
alert('Failed to upload document');
}
} catch (error) {
alert('An error occurred during upload');
}
}
// Config form
document.getElementById('configForm')?.addEventListener('submit', async (e) => {
e.preventDefault();
const form = e.target;
const data = {
gemini_api_key: form.gemini_api_key.value,
gemini_model: form.gemini_model.value,
openrouter_api_key: form.openrouter_api_key.value,
openrouter_model: form.openrouter_model.value,
comfyui_host: form.comfyui_host.value,
ollama_host: form.ollama_host.value
};
try {
const response = await fetch('/api/admin/endpoints', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
alert('Configuration saved!');
} else {
alert('Failed to save configuration');
}
} catch (error) {
alert('An error occurred');
}
});
// User management functions
async function promoteUser(userId) {
if (!confirm('Promote this user to admin?')) return;
try {
const response = await fetch(`/api/admin/users/${userId}/promote`, { method: 'POST' });
if (response.ok) {
loadUsers();
}
} catch (error) {
alert('Failed to promote user');
}
}
async function updateUserLimit(userId) {
const limit = prompt('Enter new daily request limit:', '5');
if (!limit) return;
try {
const response = await fetch(`/api/admin/users/${userId}/limit`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `limit=${limit}`
});
if (response.ok) {
loadUsers();
}
} catch (error) {
alert('Failed to update limit');
}
}
async function deleteUser(userId) {
if (!confirm('Delete this user? This cannot be undone.')) return;
try {
const response = await fetch(`/api/admin/users/${userId}`, { method: 'DELETE' });
if (response.ok) {
loadUsers();
}
} catch (error) {
alert('Failed to delete user');
}
}
async function deleteRAGDocument(docId) {
if (!confirm('Delete this document from knowledge base?')) return;
try {
const response = await fetch(`/api/admin/documents/${docId}`, { method: 'DELETE' });
if (response.ok) {
loadRAGDocuments();
loadSystemStatus();
}
} catch (error) {
alert('Failed to delete document');
}
}
{% endif %}
</script>
</body>
</html>