Files
gott/wwwroot/js/api.js
T
Meleki 5f3e308975 feat: ask_user_choice Tool – interaktive Auswahlmöglichkeiten im Chat
Neues Tool 'ask_user_choice' analog zum AskUserQuestion aus dem Claude Agent SDK.
Claude kann dem User Multiple-Choice-Fragen stellen, wenn es mehrere sinnvolle
Wege gibt. Der User wählt per Button, Claude arbeitet mit der Antwort weiter.

Backend:
- ToolService: ask_user_choice Tool-Definition (question + choices[2-5])
- AnthropicService/ProcessStream: Tool wird NICHT ausgeführt, sondern als
  SSE-Event 'user_choice' an den Client gesendet. stopReason wird auf
  'awaiting_user_choice' gesetzt (mit Guard gegen Überschreibung durch
  message_delta)
- ChatEndpoint: Erkennung von Choice-Responses (choiceToolCallId + choiceResult),
  tool_result wird in die korrekte Chain gespeichert (wo der tool_use liegt),
  toolChoice wird auf 'auto' gesetzt damit Claude frei antworten kann
- ChatService: FindChainWithToolCall() zum Auffinden der richtigen Chain,
  BuildHistoryFromDb() erweitert um Choice-Chain-Erkennung (wenn letzte Chain
  leer/nur tool_result → vorletzte Chain vollständig rekonstruieren)
- ChatModels: ChatRequest um ChoiceToolCallId + ChoiceResult erweitert

Frontend:
- api.js: apiChatChoice() für Choice-Responses
- chat.js: handleStreamEvent() als shared Handler extrahiert (sendMessage +
  handleChoiceClick nutzen denselben Code), handleChoiceClick() startet neuen
  Stream nach Button-Klick
- ui.js: addChoiceBubble() (live/interaktiv) + addChoiceBubbleStatic() (History)
- history.js: Choice-Bubbles in _renderMessagesIntoFragment + renderMessages,
  Voraus-Suche nach zugehörigem tool_result, unbeantwortet = klickbare Buttons
- chat.css: Styling für Choice-Bubbles (Primary-Color-Outline-Buttons, Hover,
  Selected/Unselected States)

Robustheit:
- Backend-Fallback wenn Claude choices als String statt Array sendet
- Frontend-Guard: Array.isArray(choices) an allen 4 Render-Stellen
- Page-Refresh mit offener Choice → aktive Buttons werden korrekt gerendert
2026-03-07 11:30:55 +01:00

563 lines
16 KiB
JavaScript

// ── Global 401 Handler ──
(function() {
var _origFetch = window.fetch;
window.fetch = function(url, opts) {
return _origFetch.apply(this, arguments).then(function(resp) {
if (resp.status === 401 && typeof url === 'string' && !url.includes('/api/auth/')) {
if (typeof currentPage !== 'undefined' && currentPage !== 'login') {
navigateTo('login');
}
}
return resp;
});
};
})();
// ── Auth API ──
async function apiLogin(username, password) {
const resp = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
if (!resp.ok) {
const err = await resp.json();
throw new Error(err.error || 'Login failed');
}
return resp.json();
}
async function apiLogout() {
await fetch('/api/auth/logout', { method: 'POST' });
}
async function apiMe() {
const resp = await fetch('/api/auth/me');
if (!resp.ok) return null;
return resp.json();
}
// ── API Calls ──
async function apiChat(content, sessionId, signal) {
return fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content, sessionId }),
signal
});
}
async function apiChatChoice(sessionId, choiceToolCallId, choiceResult, signal) {
return fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId, choiceToolCallId, choiceResult }),
signal
});
}
// ── Agent API ──
async function apiGetAgents() {
const resp = await fetch('/api/agents');
return resp.json();
}
async function apiGetAgent(agentId) {
const resp = await fetch(`/api/agents/${agentId}`);
if (!resp.ok) return null;
return resp.json();
}
async function apiCreateAgent(data) {
const resp = await fetch('/api/agents', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return resp.json();
}
async function apiUpdateAgent(agentId, data) {
const resp = await fetch(`/api/agents/${agentId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return resp.json();
}
async function apiDeleteAgent(agentId) {
return fetch(`/api/agents/${agentId}`, { method: 'DELETE' });
}
async function apiHistory(sessionId, beforeId, includeDeleted = false) {
let url = `/api/history?sessionId=${sessionId}`;
if (beforeId) url += `&beforeId=${beforeId}`;
if (includeDeleted) url += `&includeDeleted=true`;
const resp = await fetch(url);
return resp.json();
}
async function apiHistoryPoll(sessionId, afterId, includeDeleted = false) {
let url = `/api/history/poll?sessionId=${sessionId}&afterId=${afterId}`;
if (includeDeleted) url += `&includeDeleted=true`;
const resp = await fetch(url);
return resp.json();
}
async function apiArchive(sessionId) {
const resp = await fetch(`/api/archive?sessionId=${sessionId}`, { method: 'POST' });
if (resp.status === 204) return null;
return resp.json();
}
async function apiListArchives(sessionId) {
const resp = await fetch(`/api/archives?sessionId=${sessionId}`);
return resp.json();
}
async function apiGetArchive(archiveId) {
const resp = await fetch(`/api/archive/${archiveId}`);
return resp.json();
}
// ── Session API ──
async function apiGetSessions() {
const resp = await fetch('/api/sessions');
return resp.json();
}
async function apiCreateSession(data = {}) {
const resp = await fetch('/api/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return resp.json();
}
async function apiGetSession(sessionId) {
const resp = await fetch(`/api/sessions/${sessionId}`);
if (!resp.ok) return null;
return resp.json();
}
async function apiDeleteSession(sessionId) {
return fetch(`/api/sessions/${sessionId}`, { method: 'DELETE' });
}
async function apiUpdateSession(sessionId, data) {
const resp = await fetch(`/api/sessions/${sessionId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
return resp.json();
}
async function apiExecCommand(sessionId, command, mode = 'background') {
const resp = await fetch(`/api/sessions/${sessionId}/exec`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command, mode })
});
if (mode === 'foreground') return resp.json();
return resp;
}
async function apiGetSystemPrompt(sessionId) {
const resp = await fetch(`/api/sessions/${sessionId}/system-prompt`);
if (!resp.ok) throw new Error('System-Prompt konnte nicht geladen werden');
return resp.text();
}
async function apiGetSystemPromptBlocks(sessionId) {
const resp = await fetch(`/api/sessions/${sessionId}/system-prompt-blocks`);
if (!resp.ok) throw new Error('System-Prompt konnte nicht geladen werden');
return resp.json();
}
async function apiIsStreaming(sessionId) {
const resp = await fetch(`/api/sessions/${sessionId}/streaming`);
return resp.json();
}
async function apiStopChat(sessionId) {
const resp = await fetch('/api/chat/stop', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId })
});
return resp.json();
}
async function apiGetChain(chainId) {
const resp = await fetch(`/api/chains/${chainId}`);
if (!resp.ok) return null;
return resp.json();
}
async function apiToggleMessageMark(messageId) {
const resp = await fetch(`/api/messages/${messageId}/mark`, { method: 'PUT' });
return resp.json();
}
async function apiToggleMessageDelete(messageId) {
const resp = await fetch(`/api/messages/${messageId}/delete`, { method: 'PUT' });
return resp.json();
}
async function apiGitStatus(sessionId) {
const resp = await fetch(`/api/sessions/${sessionId}/git-status`);
if (!resp.ok) return null;
return resp.json();
}
// ── Skills API ──
async function apiGetSkills() {
const resp = await fetch('/api/skills');
return resp.json();
}
async function apiGetAgentSkills(agentId) {
const resp = await fetch(`/api/agents/${agentId}/skills`);
return resp.json();
}
async function apiSetAgentSkills(agentId, skills) {
const resp = await fetch(`/api/agents/${agentId}/skills`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ skills })
});
return resp.json();
}
async function apiAddAgentSkill(agentId, slug) {
return fetch(`/api/agents/${agentId}/skills/${slug}`, { method: 'POST' });
}
async function apiRemoveAgentSkill(agentId, slug) {
return fetch(`/api/agents/${agentId}/skills/${slug}`, { method: 'DELETE' });
}
// ── Profile API ──
async function apiGetProfile() {
const resp = await fetch('/api/profile');
if (!resp.ok) return null;
return resp.json();
}
async function apiUpdateProfile(data) {
const resp = await fetch('/api/profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!resp.ok) {
const err = await resp.json();
throw new Error(err.error || 'Fehler beim Speichern');
}
return resp.json();
}
async function apiChangePassword(currentPassword, newPassword) {
const resp = await fetch('/api/profile/password', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ currentPassword, newPassword })
});
if (!resp.ok) {
const err = await resp.json();
throw new Error(err.error || 'Fehler beim Ändern');
}
return resp.json();
}
// ── Telegram API (pro Session) ──
async function apiSessionTelegramGet(sessionId) {
const resp = await fetch(`/api/sessions/${sessionId}/telegram`);
if (!resp.ok) return { configured: false };
return resp.json();
}
async function apiSessionTelegramSave(sessionId, botToken, webhookSecret, allowedChatIds) {
const resp = await fetch(`/api/sessions/${sessionId}/telegram`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ botToken, webhookSecret, allowedChatIds })
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || 'Telegram-Config speichern fehlgeschlagen');
}
return resp.json();
}
async function apiSessionTelegramRegisterWebhook(sessionId, baseUrl) {
const resp = await fetch(`/api/sessions/${sessionId}/telegram/register-webhook`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ baseUrl })
});
if (!resp.ok) throw new Error('Webhook-Registrierung fehlgeschlagen');
return resp.json();
}
async function apiSessionTelegramWebhookInfo(sessionId) {
const resp = await fetch(`/api/sessions/${sessionId}/telegram/webhook-info`);
if (!resp.ok) throw new Error('Webhook-Info fehlgeschlagen');
return resp.json();
}
async function apiSessionTelegramDeleteWebhook(sessionId) {
const resp = await fetch(`/api/sessions/${sessionId}/telegram/webhook`, { method: 'DELETE' });
if (!resp.ok) throw new Error('Webhook-Löschung fehlgeschlagen');
return resp.json();
}
async function apiGetSettings() {
const resp = await fetch('/api/settings');
if (!resp.ok) return {};
return resp.json();
}
async function apiSaveSettings(settings) {
const resp = await fetch('/api/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
});
return resp.json();
}
// ── Cronjob API ──
async function apiGetCronjobs() {
const resp = await fetch('/api/cronjobs');
return resp.json();
}
async function apiGetCronjob(id) {
const resp = await fetch(`/api/cronjobs/${id}`);
if (!resp.ok) return null;
return resp.json();
}
async function apiCreateCronjob(data) {
const resp = await fetch('/api/cronjobs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || 'Cronjob erstellen fehlgeschlagen');
}
return resp.json();
}
async function apiUpdateCronjob(id, data) {
const resp = await fetch(`/api/cronjobs/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || 'Cronjob speichern fehlgeschlagen');
}
return resp.json();
}
async function apiDeleteCronjob(id) {
return fetch(`/api/cronjobs/${id}`, { method: 'DELETE' });
}
async function apiToggleCronjob(id) {
const resp = await fetch(`/api/cronjobs/${id}/toggle`, { method: 'POST' });
return resp.json();
}
async function apiRunCronjob(id) {
const resp = await fetch(`/api/cronjobs/${id}/run`, { method: 'POST' });
return resp.json();
}
// ── Shared Message API ──
async function apiListShares() {
const resp = await fetch('/api/shared');
return resp.json();
}
async function apiShareMessage(messageId, includeContext = false) {
const resp = await fetch(`/api/messages/${messageId}/share`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ includeContext })
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || 'Teilen fehlgeschlagen');
}
return resp.json();
}
async function apiGetShared(token) {
const resp = await fetch(`/api/shared/${token}`);
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || 'Geteilte Nachricht nicht gefunden');
}
return resp.json();
}
async function apiDeleteShare(token) {
return fetch(`/api/shared/${token}`, { method: 'DELETE' });
}
// ── Admin API ──
async function apiAdminGetUsers() {
const resp = await fetch('/api/admin/users');
if (!resp.ok) throw new Error('Fehler beim Laden der Benutzer');
return resp.json();
}
async function apiAdminCreateUser(data) {
const resp = await fetch('/api/admin/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!resp.ok) {
const err = await resp.json();
throw new Error(err.error || 'Fehler beim Erstellen');
}
return resp.json();
}
async function apiAdminUpdateUser(userId, data) {
const resp = await fetch('/api/admin/users/' + userId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!resp.ok) {
const err = await resp.json();
throw new Error(err.error || 'Fehler beim Aktualisieren');
}
return resp.json();
}
async function apiAdminResetPassword(userId, password) {
const resp = await fetch('/api/admin/users/' + userId + '/password', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password })
});
if (!resp.ok) {
const err = await resp.json();
throw new Error(err.error || 'Fehler beim Zurücksetzen');
}
return resp.json();
}
async function apiAdminDeleteUser(userId) {
const resp = await fetch('/api/admin/users/' + userId, { method: 'DELETE' });
if (!resp.ok) {
const err = await resp.json();
throw new Error(err.error || 'Fehler beim Löschen');
}
return resp.json();
}
async function apiAdminImpersonate(userId) {
const resp = await fetch('/api/admin/users/' + userId + '/impersonate', { method: 'POST' });
if (!resp.ok) {
const err = await resp.json();
throw new Error(err.error || 'Fehler beim Anmelden');
}
return resp.json();
}
async function apiAdminGetLoginLink(userId) {
const resp = await fetch('/api/admin/users/' + userId + '/login-link', { method: 'POST' });
if (!resp.ok) {
const err = await resp.json();
throw new Error(err.error || 'Fehler beim Erstellen des Login-Links');
}
return resp.json();
}
// ── Server Settings API (Admin) ──
async function apiGetServerSettings() {
const resp = await fetch('/api/admin/server-settings');
if (!resp.ok) throw new Error('Fehler beim Laden der Servereinstellungen');
return resp.json();
}
async function apiSaveServerSettings(data) {
const resp = await fetch('/api/admin/server-settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || 'Fehler beim Speichern');
}
return resp.json();
}
// ── Krea Image API ──
async function apiKreaStatus() {
const resp = await fetch('/api/images/status');
if (!resp.ok) return { configured: false };
return resp.json();
}
async function apiKreaGenerate(prompt, model, width, height) {
const resp = await fetch('/api/images/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, model: model || 'flux', width: width || 512, height: height || 512, batchSize: 1 })
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || 'Bildgenerierung fehlgeschlagen');
}
return resp.json();
}
// ── Backup API ──
async function apiBackupDb() {
const resp = await fetch('/api/backup', { method: 'POST', credentials: 'include' });
if (!resp.ok) throw new Error('Backup fehlgeschlagen');
const blob = await resp.blob();
const cd = resp.headers.get('content-disposition') || '';
const filename = cd.match(/filename\*=UTF-8''(.+?)(?:;|$)/i)?.[1]
|| cd.match(/filename="?([^";]+)"?/)?.[1]
|| 'gott-backup.tar.gz';
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}