Research Chat: Web-Chatoberfläche für den Research-Agent (DeepSeek v4 Flash + Playwright, Docker/Podman)

This commit is contained in:
nico committed 2026-08-26 16:34:03 +02:00
commit a0243d494c
23 files changed
+2253

No files matched your search

+108
View File
@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Geteilte Recherche</title>
<link rel="icon" href="/icons/icon-192.png">
<meta name="theme-color" content="#0f172a">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = { darkMode: 'class' };
document.documentElement.classList.add('dark');
</script>
</head>
<body class="dark:bg-slate-950 bg-slate-100 dark:text-slate-100 text-slate-900 min-h-screen">
<div class="max-w-3xl mx-auto p-4 md:p-8">
<header class="flex items-center justify-between mb-6">
<div class="flex items-center gap-2">
<span class="text-2xl">🔎</span>
<span class="font-bold">Research Agent</span>
<span class="text-xs px-2 py-0.5 rounded-full bg-slate-700 text-slate-300">geteilt</span>
</div>
<span id="dateLabel" class="text-sm text-slate-400"></span>
</header>
<div id="loading" class="text-center py-16 text-slate-400">Lade Recherche…</div>
<div id="content" class="hidden space-y-4"></div>
<footer class="mt-8 text-center text-xs text-slate-500">
<p>Generiert mit dem Research Agent · DeepSeek v4 Flash</p>
</footer>
</div>
<script>
const content = document.getElementById('content');
const loading = document.getElementById('loading');
const escapeHtml = (s) => String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const mdLite = (text) => {
let s = escapeHtml(text);
s = s.replace(/`([^`]+)`/g, '<code class="px-1 py-0.5 rounded bg-slate-700 text-amber-200 text-xs">$1</code>');
s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
s = s.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer" class="text-blue-400 underline break-all">$1</a>');
s = s.replace(/https?:\/\/[^\s<]+/g, '<a href="$&" target="_blank" rel="noopener noreferrer" class="text-blue-400 underline break-all">$&</a>');
return s.replace(/\n/g, '<br>');
};
const toolLabel = (m) => {
const icons = { search: '🔍', browse: '🌐', verify: '✅', save_dashboard: '📊' };
const icon = icons[m.tool_name] || '🔧';
const text = m.tool_name === 'search' ? `Suche: ${m.content}` : m.content;
return { icon, text: (text || '').slice(0, 200) };
};
const renderDashboardCard = (url) => `
<div class="dark:bg-slate-800 bg-white rounded-2xl border dark:border-slate-700 border-slate-200 overflow-hidden">
<div class="p-4 flex flex-wrap items-center gap-3">
<div class="flex-1 min-w-[200px]">
<div class="text-sm font-semibold">📊 Dashboard</div>
<div class="text-xs text-slate-400">Analyse, verifizierte Quellen, Empfehlungen & Handlungsplan</div>
</div>
<a href="${url}" target="_blank" rel="noopener noreferrer"
class="px-4 py-2 rounded-lg bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-semibold">Dashboard öffnen ↗</a>
<a href="${url}" download
class="px-4 py-2 rounded-lg bg-slate-600 hover:bg-slate-500 text-white text-sm">Download</a>
</div>
<iframe src="${url}" class="w-full border-t dark:border-slate-700 border-slate-200" style="height: 65vh; min-height: 400px;" loading="lazy"></iframe>
</div>`;
async function load() {
const token = location.pathname.split('/').pop();
const res = await fetch(`/api/share/${token}`);
if (!res.ok) {
loading.textContent = '⚠️ Dieser Link ist ungültig oder wurde gelöscht.';
return;
}
const { session, messages } = await res.json();
loading.classList.add('hidden');
content.classList.remove('hidden');
document.title = `${session.title} – Research Agent`;
document.getElementById('dateLabel').textContent = new Date(session.created_at).toLocaleDateString('de-DE');
content.innerHTML = `
<h1 class="text-2xl font-bold">${escapeHtml(session.title)}</h1>
${session.dashboard ? renderDashboardCard(session.dashboard) : ''}`;
const chat = document.createElement('div');
chat.className = 'space-y-3 mt-2';
for (const m of messages) {
if (m.role === 'user') {
chat.innerHTML += `<div class="flex justify-end"><div class="max-w-[80%] px-4 py-2.5 rounded-2xl bg-blue-600 text-white whitespace-pre-wrap break-words">${mdLite(m.content)}</div></div>`;
} else if (m.role === 'assistant') {
if (m.content)
chat.innerHTML += `<div class="flex"><div class="max-w-[85%] px-4 py-2.5 rounded-2xl bg-slate-200 dark:bg-slate-800 whitespace-pre-wrap break-words">${mdLite(m.content)}</div></div>`;
} else if (m.role === 'tool') {
const { icon, text } = toolLabel(m);
chat.innerHTML += `<div class="flex"><div class="text-xs text-slate-400 px-1 flex items-center gap-1"><span>${icon}</span><span class="break-all">${escapeHtml(text)}</span></div></div>`;
}
}
content.appendChild(chat);
}
load();
</script>
</body>
</html>