frankenbot/templates/orchestrator.html
pdyde 7ee66397e1 feat: Enhanced Orchestrator Chat with notifications and better UX
- Add task creation notifications in Orchestrator Chat
- New messages appear at top (newest first)
- Add Clear Chat button with confirmation
- Improve timestamps: DD.MM.YYYY HH:MM:SS format
- Support for system notifications (task created, sub-agent responses)
- Add add_orchestrator_notification() helper function
- Auto-notify when agents create sub-tasks
- Clear Chat route: POST /orchestrator/clear
- Better visual distinction between messages and notifications
2026-02-21 18:02:01 +01:00

235 lines
8.6 KiB
HTML

{% extends "base.html" %}
{% block title %}Orchestrator{% endblock %}
{% block content %}
<div class="page-header">
<h1>Master-Orchestrator</h1>
<p>Delegiert automatisch an den passenden Agenten</p>
</div>
{% if knowledge_loaded %}
<div class="alert alert-success mb-4">
<strong>Wissensdatenbank geladen</strong> · Agenten-Prompts initialisiert
</div>
{% endif %}
<div class="row g-4">
<!-- Sidebar -->
<div class="col-lg-4">
<div class="card">
<div class="card-header bg-dark">
<h5 class="mb-0">💬 Prompt eingeben</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label for="prompt" class="form-label">Ihre Anfrage an den Orchestrator</label>
<textarea class="form-control" id="prompt" name="prompt" rows="8"
placeholder="Was soll erledigt werden?&#10;&#10;Beispiele:&#10;- Recherchiere Locations für den Diversity Ball&#10;- Erstelle eine Budget-Übersicht&#10;- Verteile diese Tasks an passende Agenten" required></textarea>
</div>
<button type="button" class="btn btn-primary w-100" id="streamBtn"
onclick="sendPromptWithStream()">
▶ Live-Antwort anfordern
</button>
<p class="form-text mt-2">
Der Orchestrator analysiert deine Anfrage und delegiert automatisch an die passenden Agenten.
</p>
</div>
</div>
</div>
<!-- Chat -->
<div class="col-lg-8">
<div class="card">
<div class="card-header bg-dark d-flex justify-content-between align-items-center">
<h5 class="mb-0">Orchestrator-Chat</h5>
<div class="d-flex align-items-center gap-2">
<small style="color:var(--text-muted);">Automatische Agenten-Delegation</small>
{% if chat_history %}
<button class="btn btn-sm btn-outline-secondary" onclick="clearOrchestratorChat()">🗑 Clear</button>
{% endif %}
</div>
</div>
<div class="card-body chat-container" id="chatContainer">
{% if chat_history %}
{% for chat in chat_history %}
{% if chat.is_notification %}
<!-- System Notification -->
<div class="alert alert-info mb-2" style="font-size:0.875rem;padding:0.75rem;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span>{{ chat.message }}</span>
<small style="color:var(--text-muted);">{{ chat.timestamp }}</small>
</div>
</div>
{% else %}
<!-- Regular Chat Message -->
<div class="chat-message">
<div class="chat-timestamp">
{{ chat.timestamp }} ·
<span class="badge bg-primary" style="font-size:.65rem;">{{ chat.agent }}</span>
</div>
<div class="chat-prompt mt-1"><strong>Sie:</strong> {{ chat.user_prompt }}</div>
<div class="chat-response mt-1"><strong>Orchestrator:</strong> {{ chat.response }}</div>
</div>
{% endif %}
{% endfor %}
{% else %}
<div class="text-center py-5" style="color:var(--text-muted);" id="emptyState">
<p style="font-size:2rem;margin-bottom:.5rem;">🤖</p>
<p>Noch keine Anfragen.<br>Der Orchestrator delegiert automatisch an den passenden Agenten.</p>
<hr>
<p style="font-size:.8rem;">
✓ Immer zuerst delegieren · ✓ Neuen Agenten erstellen falls nötig · ✓ Niemals selbst ausführen
</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function sendPromptWithStream() {
const prompt = document.getElementById('prompt').value.trim();
const streamBtn = document.getElementById('streamBtn');
if (!prompt) { alert('Bitte Anfrage eingeben!'); return; }
streamBtn.disabled = true;
streamBtn.textContent = '⏳ Agent arbeitet…';
const chatContainer = document.getElementById('chatContainer');
const emptyState = document.getElementById('emptyState');
if (emptyState) emptyState.remove();
const msgDiv = document.createElement('div');
msgDiv.className = 'chat-message';
msgDiv.innerHTML = `
<div class="chat-timestamp">${formatTimestamp()} · <span class="badge bg-primary" style="font-size:.65rem;" id="agentBadge">wird ausgewählt…</span></div>
<div class="chat-prompt mt-1"><strong>Sie:</strong> ${escapeHtml(prompt)}</div>
<div class="chat-response mt-1" id="responseDiv"><strong>Orchestrator:</strong> <span id="responseText">⏳ Agent arbeitet…</span></div>
`;
chatContainer.insertBefore(msgDiv, chatContainer.firstChild);
fetch('/api/agent-stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt })
}).then(response => {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
function read() {
reader.read().then(({ done, value }) => {
if (done) {
streamBtn.disabled = false;
streamBtn.textContent = 'Live-Antwort anfordern';
return;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
try {
const event = JSON.parse(line.slice(6));
if (event.type === 'agent_selected') {
document.getElementById('agentBadge').textContent = event.agent;
} else if (event.type === 'response_chunk') {
const t = document.getElementById('responseText');
if (t.textContent.startsWith('⏳')) t.textContent = '';
t.textContent += event.text;
} else if (event.type === 'error') {
document.getElementById('responseText').textContent = '❌ ' + event.message;
}
} catch(e) {}
}
read();
});
}
read();
}).catch(err => {
document.getElementById('responseText').textContent = '❌ ' + err.message;
streamBtn.disabled = false;
streamBtn.textContent = 'Live-Antwort anfordern';
});
}
function distributeTodos() {
const prompt = document.getElementById('todoPrompt').value.trim();
if (!prompt) { alert('Bitte Aufgaben eingeben!'); return; }
const selectedAgents = Array.from(document.querySelectorAll('.agent-checkbox:checked')).map(cb => cb.value);
if (selectedAgents.length === 0) { alert('Bitte mindestens einen Agenten auswählen!'); return; }
const status = document.getElementById('todoStatus');
status.textContent = 'Starte parallele Ausführung...';
status.className = 'form-text mt-2 text-info';
const tasks = prompt.split('\n').filter(t => t.trim());
fetch('/api/orchestrator-distribute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tasks, agents: selectedAgents })
})
.then(r => r.json())
.then(data => {
console.log('Response:', data);
if (data && data.success) {
status.textContent = '✓ ' + data.message;
status.className = 'form-text mt-2 text-success';
if (data.tasks && data.tasks.length > 0) {
location.reload();
}
} else if (data) {
status.textContent = 'Fehler: ' + (data.error || 'Unbekannter Fehler');
status.className = 'form-text mt-2 text-danger';
} else {
status.textContent = 'Fehler: Keine Antwort vom Server';
status.className = 'form-text mt-2 text-danger';
}
})
.catch(err => {
status.textContent = 'Fehler: ' + err.message;
status.className = 'form-text mt-2 text-danger';
});
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function clearOrchestratorChat() {
if (!confirm('Möchten Sie den gesamten Chat-Verlauf löschen?')) return;
fetch('/orchestrator/clear', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
}
})
.catch(err => {
alert('Fehler beim Löschen: ' + err.message);
});
}
function formatTimestamp() {
const now = new Date();
const day = String(now.getDate()).padStart(2, '0');
const month = String(now.getMonth() + 1).padStart(2, '0');
const year = now.getFullYear();
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
return `${day}.${month}.${year} ${hours}:${minutes}:${seconds}`;
}
</script>
{% endblock %}