- New web UI with OpenWebUI-like interface using Tailwind CSS - SQLite-based authentication with session management - User registration, login, and profile pages - Chat interface with conversation history - Streaming responses with visible thinking phase - File attachments support - User document uploads in profile - Rate limiting (5 requests/day for free users) - Admin panel user management with promote/demote/delete - Custom color theme matching balloon logo design - Compatible with Nuitka build system
258 lines
12 KiB
HTML
258 lines
12 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>
|
|
</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');
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|