frankenbot/templates/chat.html
pdyde e2a853ffde feat: Add live streaming to Chat page
- Replace blocking execute_agent_task() with live subprocess streaming
- Use Popen() to read opencode output line-by-line in real-time
- Send 'chunk' events to frontend as agent thinks
- Frontend appends chunks incrementally for live response
- Matches Orchestrator's streaming UX
- No more waiting for complete response before seeing output
2026-02-21 17:32:37 +01:00

239 lines
8.7 KiB
HTML

{% extends "base.html" %}
{% block title %}Chat{% endblock %}
{% block content %}
<div class="page-header">
<h1>Agenten Chat</h1>
<p>Direkte Anfrage an einen spezifischen Agenten</p>
</div>
<div class="row g-4">
<div class="col-lg-4">
<div class="card">
<div class="card-header bg-primary">
<span>💬 Neue Anfrage</span>
</div>
<div class="card-body">
<form id="chatForm" onsubmit="sendChat(event)">
<div class="mb-3">
<label for="agent" class="form-label">Agent</label>
<select class="form-select" id="agent" name="agent" required>
<option value="">— Agent wählen —</option>
{% for key, agent in agents.items() %}
<option value="{{ key }}">{{ agent.name }}</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="prompt" class="form-label">Ihre Anfrage</label>
<textarea class="form-control" id="prompt" name="prompt" rows="5"
placeholder="Was soll der Agent erledigen?" required></textarea>
</div>
<button type="submit" class="btn btn-primary w-100" id="sendBtn">
<span id="sendBtnText">Absenden</span>
<span id="sendBtnSpinner" class="d-none">⏳ Verarbeite...</span>
</button>
<div id="chatStatus" class="mt-2 text-center" style="font-size:0.875rem;"></div>
</form>
</div>
</div>
</div>
<div class="col-lg-8">
<div class="card">
<div class="card-header bg-secondary d-flex align-items-center justify-content-between">
<span>Chat-Verlauf</span>
{% if chat_history %}
<span class="badge bg-secondary">{{ chat_history|length }}</span>
{% endif %}
</div>
<div class="card-body chat-container" id="chatContainer">
{% if chat_history %}
{% for chat in chat_history %}
<div class="chat-message">
<div class="chat-timestamp">
{{ chat.timestamp }}
<span class="badge bg-primary">{{ chat.agent }}</span>
</div>
<div class="chat-prompt"><strong>Sie:</strong> {{ chat.prompt }}</div>
<div class="chat-response mt-1"><strong>Antwort:</strong> {{ chat.response }}</div>
</div>
{% endfor %}
{% else %}
<div class="text-center py-5" style="color:var(--text-muted);">
<div style="font-size:2.5rem;margin-bottom:.75rem;">💬</div>
<p style="margin:0;">Noch keine Nachrichten.<br>
<small>Starten Sie eine Konversation.</small>
</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let eventSource = null;
function sendChat(event) {
event.preventDefault();
const form = document.getElementById('chatForm');
const agent = document.getElementById('agent').value;
const prompt = document.getElementById('prompt').value.trim();
const sendBtn = document.getElementById('sendBtn');
const sendBtnText = document.getElementById('sendBtnText');
const sendBtnSpinner = document.getElementById('sendBtnSpinner');
const chatStatus = document.getElementById('chatStatus');
const chatContainer = document.getElementById('chatContainer');
if (!agent || !prompt) {
chatStatus.innerHTML = '<span style="color:var(--danger);">Bitte Agent und Anfrage auswählen</span>';
return;
}
// Button deaktivieren
sendBtn.disabled = true;
sendBtnText.classList.add('d-none');
sendBtnSpinner.classList.remove('d-none');
chatStatus.innerHTML = '<span style="color:var(--info);">Anfrage wird gesendet...</span>';
// EventSource für Streaming
if (eventSource) {
eventSource.close();
}
// Anfrage per fetch senden
fetch('/chat/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agent: agent, prompt: prompt })
})
.then(response => {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let currentResponse = '';
let currentTimestamp = new Date().toLocaleString('de-DE', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit'
});
let currentAgent = '';
// Neue Chat-Message erstellen
const chatMessage = document.createElement('div');
chatMessage.className = 'chat-message';
chatMessage.innerHTML = `
<div class="chat-timestamp">
${currentTimestamp}
<span class="badge bg-primary" id="chatAgentBadge">...</span>
</div>
<div class="chat-prompt"><strong>Sie:</strong> ${escapeHtml(prompt)}</div>
<div class="chat-response mt-1"><strong>Antwort:</strong> <span id="chatResponseText">⏳ Warte auf Antwort...</span></div>
`;
// Placeholder entfernen falls vorhanden
const placeholder = chatContainer.querySelector('.text-center.py-5');
if (placeholder) {
placeholder.remove();
}
chatContainer.appendChild(chatMessage);
chatContainer.scrollTop = chatContainer.scrollHeight;
const responseText = document.getElementById('chatResponseText');
const agentBadge = document.getElementById('chatAgentBadge');
function processChunk() {
reader.read().then(({ done, value }) => {
if (done) {
// Stream beendet
sendBtn.disabled = false;
sendBtnText.classList.remove('d-none');
sendBtnSpinner.classList.add('d-none');
chatStatus.innerHTML = '<span style="color:var(--success);">✓ Antwort erhalten</span>';
setTimeout(() => { chatStatus.innerHTML = ''; }, 3000);
// Formular zurücksetzen
document.getElementById('prompt').value = '';
// Nach kurzer Verzögerung Seite neu laden für aktualisierte History
setTimeout(() => { location.reload(); }, 1000);
return;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop();
lines.forEach(line => {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.substring(6));
if (data.type === 'agent_selected') {
currentAgent = data.agent;
agentBadge.textContent = currentAgent;
} else if (data.type === 'processing') {
chatStatus.innerHTML = `<span style="color:var(--info);">${data.message}</span>`;
} else if (data.type === 'chunk') {
// Live-Chunk empfangen - append to response
currentResponse += data.text;
responseText.textContent = currentResponse;
chatContainer.scrollTop = chatContainer.scrollHeight;
} else if (data.type === 'response') {
currentResponse = data.text;
responseText.textContent = currentResponse;
chatContainer.scrollTop = chatContainer.scrollHeight;
} else if (data.type === 'complete') {
chatStatus.innerHTML = `<span style="color:var(--success);">${data.message}</span>`;
// In Session speichern
fetch('/chat/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
timestamp: data.timestamp,
agent: currentAgent,
agent_key: agent,
prompt: prompt,
response: data.response
})
});
} else if (data.type === 'error') {
responseText.innerHTML = `<span style="color:var(--danger);">⚠️ Fehler: ${escapeHtml(data.message)}</span>`;
chatStatus.innerHTML = `<span style="color:var(--danger);">Fehler aufgetreten</span>`;
sendBtn.disabled = false;
sendBtnText.classList.remove('d-none');
sendBtnSpinner.classList.add('d-none');
}
} catch (e) {
console.error('Parse error:', e, line);
}
}
});
processChunk();
});
}
processChunk();
})
.catch(error => {
chatStatus.innerHTML = `<span style="color:var(--danger);">⚠️ ${error.message}</span>`;
sendBtn.disabled = false;
sendBtnText.classList.remove('d-none');
sendBtnSpinner.classList.add('d-none');
});
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
{% endblock %}