feat: Fix vector indexing stability, add Gitea linking, enhance admin dashboard
This commit is contained in:
@@ -1,201 +1,103 @@
|
||||
{% extends "home/base_home.html" %}
|
||||
|
||||
{% block title %}Crew - {{ deployment.home.hero.title }}{% endblock %}
|
||||
{% block title %}The Crumbforest Crew{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="container">
|
||||
<hgroup>
|
||||
<h1>{{ t.crew.title }}</h1>
|
||||
<p>{{ t.crew.subtitle }}</p>
|
||||
<h1>🌲 The Crumbforest Crew</h1>
|
||||
<p>Meet the 15 experts ready to help you learn and build.</p>
|
||||
</hgroup>
|
||||
|
||||
<div class="crew-grid">
|
||||
{% for character in characters %}
|
||||
<div class="character-card" onclick="showCharacter('{{ character.id }}')">
|
||||
<div class="icon">{{ character.icon }}</div>
|
||||
<h3>{{ character.name }}</h3>
|
||||
<p>{{ character.short }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Dialog for this character -->
|
||||
<dialog id="dialog-{{ character.id }}">
|
||||
<article>
|
||||
<header>
|
||||
<button aria-label="Close" rel="prev" onclick="closeCharacter('{{ character.id }}')"></button>
|
||||
<h2>{{ character.icon }} {{ character.name }}</h2>
|
||||
</header>
|
||||
<p>{{ character.description }}</p>
|
||||
|
||||
{% if character.id in ['eule', 'fox', 'bugsy'] %}
|
||||
<!-- Chat Interface for Eule, FunkFox and Bugsy -->
|
||||
<div class="chat-container" style="margin-top: 1rem;">
|
||||
<div id="chat-history-{{ character.id }}" class="chat-history" style="max-height: 300px; overflow-y: auto; padding: 1rem; background: rgba(0,0,0,0.1); border-radius: 0.5rem; margin-bottom: 1rem;">
|
||||
<p style="text-align: center; color: #888;"><small>Stelle mir eine Frage...</small></p>
|
||||
</div>
|
||||
|
||||
<form id="chat-form-{{ character.id }}" onsubmit="sendMessage(event, '{{ character.id }}'); return false;">
|
||||
<input
|
||||
type="text"
|
||||
id="chat-input-{{ character.id }}"
|
||||
placeholder="{% if character.id == 'eule' %}Deine Frage an die Krümeleule...{% elif character.id == 'fox' %}Deine Frage an FunkFox...{% elif character.id == 'bugsy' %}Deine Frage an Bugsy...{% else %}Deine Frage...{% endif %}"
|
||||
style="width: 100%; color: #fff; background: rgba(255,255,255,0.1); padding: 0.75rem; border: 1px solid rgba(255,255,255,0.2); border-radius: 0.25rem; margin-bottom: 0.5rem;"
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
<div style="text-align: right;">
|
||||
<button
|
||||
type="submit"
|
||||
id="chat-submit-{{ character.id }}"
|
||||
style="padding: 0.5rem 1.5rem; font-size: 0.875rem;"
|
||||
>
|
||||
Senden
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="chat-status-{{ character.id }}" style="margin-top: 0.5rem; text-align: center; min-height: 1.5rem;">
|
||||
<!-- Status messages appear here -->
|
||||
</div>
|
||||
</div>
|
||||
{% for id, role in roles.items() %}
|
||||
<article class="role-card" style="border-left: 5px solid {{ role.color }};">
|
||||
<header>
|
||||
<div class="role-icon">{{ role.icon }}</div>
|
||||
<h3 style="color: {{ role.color }}">{{ role.name }}</h3>
|
||||
<small>{{ role.title }}</small>
|
||||
</header>
|
||||
<p>{{ role.description }}</p>
|
||||
<footer>
|
||||
{% if id == 'eule' %}
|
||||
<!-- Eule is public -->
|
||||
<a href="/crumbforest/roles/{{ id }}" role="button" class="contrast"
|
||||
style="background-color: {{ role.color }}; border-color: {{ role.color }}; width: 100%;">
|
||||
Start Chat
|
||||
</a>
|
||||
{% else %}
|
||||
<!-- Others require login (or are locked) -->
|
||||
{% if user %}
|
||||
<a href="/crumbforest/roles/{{ id }}" role="button" class="outline"
|
||||
style="color: {{ role.color }}; border-color: {{ role.color }}; width: 100%;">
|
||||
Chat
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="/login?next=/crumbforest/roles/{{ id }}" role="button" class="secondary outline" style="width: 100%;">
|
||||
🔒 Login to Chat
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<footer>
|
||||
<small>{{ t.crew.tags_label }}
|
||||
{% for tag in character.tags %}
|
||||
<code>#{{ tag }}</code>{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</small>
|
||||
</footer>
|
||||
</article>
|
||||
</dialog>
|
||||
{% endif %}
|
||||
</footer>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
function showCharacter(id) {
|
||||
const dialog = document.getElementById('dialog-' + id);
|
||||
dialog.showModal();
|
||||
|
||||
// Focus input field for chat-enabled characters
|
||||
if (id === 'eule' || id === 'fox' || id === 'bugsy') {
|
||||
setTimeout(() => {
|
||||
const input = document.getElementById('chat-input-' + id);
|
||||
if (input) {
|
||||
input.focus();
|
||||
}
|
||||
}, 100);
|
||||
<style>
|
||||
.crew-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
function closeCharacter(id) {
|
||||
document.getElementById('dialog-' + id).close();
|
||||
}
|
||||
|
||||
async function sendMessage(event, characterId) {
|
||||
event.preventDefault();
|
||||
|
||||
const inputEl = document.getElementById('chat-input-' + characterId);
|
||||
const historyEl = document.getElementById('chat-history-' + characterId);
|
||||
const statusEl = document.getElementById('chat-status-' + characterId);
|
||||
const submitBtn = document.getElementById('chat-submit-' + characterId);
|
||||
|
||||
const question = inputEl.value.trim();
|
||||
if (!question) return;
|
||||
|
||||
// Disable input while processing
|
||||
inputEl.disabled = true;
|
||||
submitBtn.disabled = true;
|
||||
const statusMessages = {
|
||||
'eule': '🦉 Die Eule lauscht...',
|
||||
'fox': '🦊 FunkFox droppt den Beat...',
|
||||
'bugsy': '🐞 Bugsy analysiert...'
|
||||
};
|
||||
statusEl.innerHTML = '<small style="color: #888;">' + (statusMessages[characterId] || 'Denkt nach...') + '</small>';
|
||||
|
||||
// Add user message to history
|
||||
const userMsg = document.createElement('div');
|
||||
userMsg.style.cssText = 'margin-bottom: 0.5rem; padding: 0.5rem; background: rgba(16,185,129,0.1); border-radius: 0.25rem; text-align: right;';
|
||||
userMsg.innerHTML = '<strong>Du:</strong> ' + escapeHtml(question);
|
||||
historyEl.appendChild(userMsg);
|
||||
|
||||
// Clear welcome message if present
|
||||
if (historyEl.querySelector('p')) {
|
||||
const welcomeMsg = historyEl.querySelector('p');
|
||||
if (welcomeMsg.textContent.includes('Stelle mir eine Frage')) {
|
||||
welcomeMsg.remove();
|
||||
/* Tablet: 2 columns */
|
||||
@media (min-width: 768px) {
|
||||
.crew-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to bottom
|
||||
historyEl.scrollTop = historyEl.scrollHeight;
|
||||
|
||||
// Clear input
|
||||
inputEl.value = '';
|
||||
|
||||
try {
|
||||
// Call API
|
||||
const response = await fetch('/api/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
character_id: characterId,
|
||||
question: question,
|
||||
lang: '{{ lang }}'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('API request failed: ' + response.status);
|
||||
/* Desktop: Strictly 3 columns */
|
||||
@media (min-width: 1024px) {
|
||||
.crew-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Add assistant message to history
|
||||
const characterNames = {
|
||||
'eule': '🦉 Krümeleule',
|
||||
'fox': '🦊 FunkFox',
|
||||
'bugsy': '🐞 Bugsy'
|
||||
};
|
||||
const assistantMsg = document.createElement('div');
|
||||
assistantMsg.style.cssText = 'margin-bottom: 0.5rem; padding: 0.5rem; background: rgba(255,255,255,0.05); border-radius: 0.25rem;';
|
||||
assistantMsg.innerHTML = '<strong>' + (characterNames[characterId] || characterId) + ':</strong><br>' + escapeHtml(data.answer);
|
||||
|
||||
// Add sources if available
|
||||
if (data.sources && data.sources.length > 0) {
|
||||
const sourcesText = document.createElement('small');
|
||||
sourcesText.style.cssText = 'color: #888; display: block; margin-top: 0.5rem;';
|
||||
sourcesText.innerHTML = '📚 Quellen: ' + data.sources.length + ' Dokumente';
|
||||
assistantMsg.appendChild(sourcesText);
|
||||
}
|
||||
|
||||
historyEl.appendChild(assistantMsg);
|
||||
|
||||
// Scroll to bottom
|
||||
historyEl.scrollTop = historyEl.scrollHeight;
|
||||
|
||||
// Clear status
|
||||
statusEl.innerHTML = '';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Chat error:', error);
|
||||
statusEl.innerHTML = '<small style="color: #f87171;">❌ Fehler: ' + escapeHtml(error.message) + '</small>';
|
||||
} finally {
|
||||
// Re-enable input
|
||||
inputEl.disabled = false;
|
||||
submitBtn.disabled = false;
|
||||
inputEl.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
.role-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
margin-bottom: 0;
|
||||
/* Override pico */
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
/* Ensure rounded corners clip content */
|
||||
}
|
||||
|
||||
.role-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.role-card header {
|
||||
text-align: center;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.role-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.role-card p {
|
||||
flex-grow: 1;
|
||||
/* Push footer down */
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user