- 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
667 lines
34 KiB
HTML
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>
|