Multi-Modell + Vision: OpenRouter (GLM 5.3 Flash), Bild-Upload, Tour-Feature, Dashboard-Fixes

- Modell-Registry (DeepSeek direkt + OpenRouter z-ai/glm-5.3-flash), Modellwahl pro Session, Reasoning-Effort max (env-konfigurierbar)
- Bild-Upload für Vision-Modelle (User) + Agent-screenshot()-Tool (nur Vision)
- Tour-Planung: geocode + save_tour mit GPX-Export und Karten-Sektion
- Fix: System-Prompt wird jetzt wirklich an die API gesendet, inkl. aktuellem Dashboard-Dateinamen für Folge-Updates
- Fix: History-Sanitizer gegen 400-Fehler (nie mitten in Tool-Gruppen kürzen)
- Fix: reasoning_content-Roundtrip (Pflicht bei DeepSeek mit tools)
- Fix: Dashboards ohne Browser-Cache ausliefern
This commit is contained in:
nico committed 2026-08-27 17:43:13 +02:00
1 parent eee59363d4
commit 87eb7029f1
10 files changed
+843 -70

No files matched your search

+141 -9
View File
@@ -5,6 +5,9 @@ const state = {
stream: null,
streamingEl: null,
activityShown: false,
models: [],
modelById: {},
pendingImage: null,
};
const $ = (id) => document.getElementById(id);
@@ -32,7 +35,7 @@ function mdLite(text) {
}
function toolLabel(msg) {
const icons = { search: '🔍', browse: '🌐', verify: '✅', save_dashboard: '📊' };
const icons = { search: '🔍', browse: '🌐', verify: '✅', save_dashboard: '📊', screenshot: '📷' };
const icon = icons[msg.tool_name] || '🔧';
const text = msg.tool_name === 'search' ? `Suche: ${msg.content}` : msg.content;
return { icon, text: text?.slice(0, 200) };
@@ -75,11 +78,13 @@ function renderSessions() {
// Chat-Rendering
// ---------------------------------------------------------------------------
function addUserMessage(text) {
function addUserMessage(text, image) {
$('emptyState')?.remove();
const el = document.createElement('div');
el.className = 'flex justify-end';
el.innerHTML = `<div class="max-w-[80%] px-4 py-2.5 rounded-2xl bg-blue-600 text-white whitespace-pre-wrap break-words">${mdLite(text)}</div>`;
el.innerHTML = `<div class="max-w-[80%] px-4 py-2.5 rounded-2xl bg-blue-600 text-white whitespace-pre-wrap break-words">${
image ? `<img src="${image}" alt="Angehängtes Bild" class="rounded-lg mb-2 max-h-48">` : ''
}${mdLite(text)}</div>`;
$('chat').appendChild(el);
scrollDown();
}
@@ -154,7 +159,7 @@ function renderMessages(messages, session) {
}
addDashboardCard(session);
for (const m of messages) {
if (m.role === 'user') addUserMessage(m.content);
if (m.role === 'user') addUserMessage(m.content, m.image || null);
else if (m.role === 'assistant') {
if (m.content) addAssistantBubble(m.content);
} else if (m.role === 'tool') {
@@ -348,11 +353,15 @@ async function openSession(id) {
state.stream?.close();
state.streamingEl = null;
hideActivity();
clearImage();
const res = await fetch(`/api/sessions/${id}`);
const data = await res.json();
state.current = data.session;
if (!state.current.model) state.current.model = state.models[0]?.id || null;
renderMessages(data.messages, data.session);
$('mobileTitle').textContent = data.session.title;
setSelectModel(state.current.model);
updateModelUI();
renderSessions();
const q = state.sessions.find((s) => s.id === id);
if (data.session.status === 'running' || q?.job === 'running' || q?.job === 'queued') {
@@ -370,6 +379,114 @@ async function newSession() {
$('input').focus();
}
// ---------------------------------------------------------------------------
// Modell-Auswahl
// ---------------------------------------------------------------------------
async function loadModels() {
const res = await fetch('/api/models');
const data = await res.json();
state.models = data.models || [];
state.modelById = Object.fromEntries(state.models.map((m) => [m.id, m]));
const opts = state.models.map((m) => `<option value="${escapeHtml(m.id)}">${escapeHtml(m.label)}</option>`).join('');
$('modelSelectDesktop').innerHTML = opts;
$('modelSelectMobile').innerHTML = opts;
return data.default || null;
}
function currentModel() {
return state.modelById[state.current?.model] || state.models[0] || null;
}
function setSelectModel(id) {
$('modelSelectDesktop').value = id || '';
$('modelSelectMobile').value = id || '';
}
async function changeModel(id) {
if (!state.current || state.current.model === id) return;
const res = await fetch(`/api/sessions/${state.current.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: id }),
});
if (!res.ok) {
const d = await res.json().catch(() => ({}));
addToolLine('⚠️', d.error || 'Modellwechsel fehlgeschlagen');
setSelectModel(state.current.model);
return;
}
state.current.model = id;
const s = state.sessions.find((x) => x.id === state.current.id);
if (s) s.model = id;
updateModelUI();
}
function updateModelUI() {
const m = currentModel();
const vision = !!m?.vision;
const btn = $('imageBtn');
btn.disabled = !vision;
btn.classList.toggle('opacity-40', !vision);
const visionLabel = state.models.find((x) => x.vision)?.label || 'einem Vision-Modell';
btn.title = vision ? 'Bild anhängen' : `Bilderkennung nur mit Vision-Modell möglich (z.B. ${visionLabel})`;
if (!vision && state.pendingImage) clearImage();
}
// ---------------------------------------------------------------------------
// Bild-Upload (Vision)
// ---------------------------------------------------------------------------
function fileToDataUrl(file, maxDim = 1280, quality = 0.82) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const img = new Image();
img.onload = () => {
const scale = Math.min(1, maxDim / Math.max(img.width, img.height));
const canvas = document.createElement('canvas');
canvas.width = Math.max(1, Math.round(img.width * scale));
canvas.height = Math.max(1, Math.round(img.height * scale));
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
resolve(canvas.toDataURL('image/jpeg', quality));
};
img.onerror = () => reject(new Error('Bild konnte nicht gelesen werden'));
img.src = reader.result;
};
reader.onerror = () => reject(new Error('Datei konnte nicht gelesen werden'));
reader.readAsDataURL(file);
});
}
function showImagePreview(dataUrl) {
state.pendingImage = dataUrl;
$('imagePreviewImg').src = dataUrl;
$('imagePreview').classList.remove('hidden');
}
function clearImage() {
state.pendingImage = null;
$('imageInput').value = '';
$('imagePreview').classList.add('hidden');
}
async function handleImageFile(file) {
if (!file) return;
try {
const dataUrl = await fileToDataUrl(file);
if (dataUrl.length > 3500000) {
addToolLine('⚠️', 'Bild zu groß – bitte ein kleineres Foto wählen.');
return;
}
showImagePreview(dataUrl);
} catch (e) {
addToolLine('⚠️', e.message || 'Bild konnte nicht verarbeitet werden');
}
}
function copyText(text, btn, label) {
try {
navigator.clipboard.writeText(text).then(
@@ -424,14 +541,14 @@ function updateShareHint() {
// Senden
// ---------------------------------------------------------------------------
async function sendMessage(text) {
async function sendMessage(text, image) {
if (!state.current) await newSession();
$('sendBtn').disabled = true;
addUserMessage(text);
addUserMessage(text, image || null);
const res = await fetch(`/api/sessions/${state.current.id}/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
body: JSON.stringify(image ? { text, image } : { text }),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
@@ -439,6 +556,7 @@ async function sendMessage(text) {
$('sendBtn').disabled = false;
return;
}
clearImage();
enableNotifications();
showActivity();
setActivity('In Warteschlange…', '⏳');
@@ -462,6 +580,7 @@ window.addEventListener('load', async () => {
navigator.serviceWorker.register('/sw.js').catch(() => {});
}
await loadModels();
await loadSessions();
const params = new URLSearchParams(location.search);
const target = params.get('session') || state.sessions.find((s) => s.status === 'running')?.id || state.sessions[0]?.id;
@@ -476,10 +595,11 @@ window.addEventListener('load', async () => {
$('chatForm').addEventListener('submit', (e) => {
e.preventDefault();
const text = $('input').value.trim();
if (!text) return;
const image = state.pendingImage;
if (!text && !image) return;
$('input').value = '';
$('input').style.height = 'auto';
sendMessage(text);
sendMessage(text, image);
});
$('input').addEventListener('input', () => {
@@ -494,6 +614,18 @@ $('logoutBtn').onclick = async () => {
location.href = '/login.html';
};
$('modelSelectDesktop').addEventListener('change', (e) => changeModel(e.target.value));
$('modelSelectMobile').addEventListener('change', (e) => changeModel(e.target.value));
$('imageBtn').onclick = () => {
if (!$('imageBtn').disabled) $('imageInput').click();
};
$('imageInput').addEventListener('change', (e) => {
handleImageFile(e.target.files?.[0]);
e.target.value = '';
});
$('imageRemoveBtn').onclick = clearImage;
// Enter sendet, Shift+Enter = Zeilenumbruch
$('input').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {