Agents: CRUD, DB table, session binding, replace model picker

- agents table with name, avatar, description, model, persona, instructions, soft-delete
- AgentEndpoint (GET/POST/PUT/DELETE /api/agents)
- sessions + chains now reference agent_id
- ChatEndpoint resolves agent from session (no agent = no chat)
- system prompt built from agent persona + instructions
- frontend: agent editor with avatar upload, agent cards on dashboard
- session: agent dropdown replaces model picker, choice persisted
This commit is contained in:
Meleki committed 2026-02-26 20:59:19 +01:00
1 parent 0cd2d51d32
commit 449c872296
16 files changed
+552 -46

No files matched your search

+37 -2
View File
@@ -1,13 +1,48 @@
// ── API Calls ──
async function apiChat(model, messages, systemPrompt, sessionId) {
async function apiChat(messages, systemPrompt, sessionId) {
return fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model, messages, systemPrompt, sessionId })
body: JSON.stringify({ messages, systemPrompt, sessionId })
});
}
// ── 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) {
const resp = await fetch(`/api/history?sessionId=${sessionId}`);
return resp.json();
+2 -3
View File
@@ -21,7 +21,6 @@ async function clearChat() {
async function sendMessage() {
const input = $('#input');
const btn = $('#send');
const model = $('#model').value;
const systemPrompt = $('#systemPrompt').value.trim();
const text = input.value.trim();
if (!text) return;
@@ -39,7 +38,7 @@ async function sendMessage() {
let currentToolBox = null;
try {
const resp = await apiChat(model, messages, systemPrompt, getCurrentSessionId());
const resp = await apiChat(messages, systemPrompt, getCurrentSessionId());
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
@@ -92,7 +91,7 @@ async function sendMessage() {
case 'done':
endStreamBubble();
let meta = `Model: ${data.model || model} | In: ${data.inputTokens || 0} | Out: ${data.outputTokens || 0}`;
let meta = `Model: ${data.model || '?'} | In: ${data.inputTokens || 0} | Out: ${data.outputTokens || 0}`;
if (data.iterations > 1) meta += ` | Iterations: ${data.iterations}`;
if (data.durationMs) meta += ` | ${(data.durationMs / 1000).toFixed(1)}s`;
setMeta(meta);
+98
View File
@@ -0,0 +1,98 @@
// ── Agent Page ──
let _agentAvatarData = null;
registerPage('agent', {
async init(params) {
const agentId = params.id;
const isNew = !agentId;
let agent = null;
if (!isNew) {
agent = await apiGetAgent(agentId);
if (!agent) return navigateTo('dashboard');
}
const title = document.getElementById('agentPageTitle');
const nameEl = document.getElementById('agentName');
const descEl = document.getElementById('agentDescription');
const modelEl = document.getElementById('agentModel');
const personaEl = document.getElementById('agentPersona');
const instructionsEl = document.getElementById('agentInstructions');
const avatarPreview = document.getElementById('avatarPreview');
const avatarInput = document.getElementById('avatarInput');
const avatarBtn = document.getElementById('avatarBtn');
const avatarClearBtn = document.getElementById('avatarClearBtn');
const saveBtn = document.getElementById('agentSaveBtn');
const cancelBtn = document.getElementById('agentCancelBtn');
const deleteBtn = document.getElementById('agentDeleteBtn');
_agentAvatarData = null;
if (agent) {
title.textContent = agent.name;
nameEl.value = agent.name || '';
descEl.value = agent.description || '';
modelEl.value = agent.model || 'sonnet';
personaEl.value = agent.persona || '';
instructionsEl.value = agent.instructions || '';
_agentAvatarData = agent.avatar || null;
deleteBtn.hidden = false;
if (agent.avatar) {
avatarPreview.innerHTML = `<img src="${agent.avatar}" alt="Avatar">`;
}
}
// Avatar Upload
avatarBtn.addEventListener('click', () => avatarInput.click());
avatarInput.addEventListener('change', () => {
const file = avatarInput.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
_agentAvatarData = reader.result;
avatarPreview.innerHTML = `<img src="${reader.result}" alt="Avatar">`;
};
reader.readAsDataURL(file);
});
avatarClearBtn.addEventListener('click', () => {
_agentAvatarData = null;
avatarPreview.innerHTML = '?';
avatarInput.value = '';
});
// Speichern
saveBtn.addEventListener('click', async () => {
const data = {
name: nameEl.value.trim() || 'Unbenannt',
description: descEl.value.trim(),
model: modelEl.value,
persona: personaEl.value,
instructions: instructionsEl.value,
avatar: _agentAvatarData
};
if (isNew) {
await apiCreateAgent(data);
} else {
await apiUpdateAgent(agentId, data);
}
navigateTo('dashboard');
});
// Abbrechen
cancelBtn.addEventListener('click', () => navigateTo('dashboard'));
// Löschen
deleteBtn.addEventListener('click', async () => {
if (!confirm('Agent wirklich löschen?')) return;
await apiDeleteAgent(agentId);
navigateTo('dashboard');
});
},
destroy() {
_agentAvatarData = null;
}
});
+33 -4
View File
@@ -2,6 +2,7 @@
registerPage('dashboard', {
async init() {
// Sessions laden
const sessions = await apiGetSessions();
const grid = document.getElementById('sessionCards');
grid.innerHTML = '';
@@ -19,14 +20,42 @@ registerPage('dashboard', {
grid.appendChild(card);
}
// Neue Session Button
const btn = document.getElementById('newSessionBtn');
if (btn) {
btn.addEventListener('click', async () => {
const newSessionBtn = document.getElementById('newSessionBtn');
if (newSessionBtn) {
newSessionBtn.addEventListener('click', async () => {
const session = await apiCreateSession();
navigateTo('session', { id: session.sessionId });
});
}
// Agents laden
const agents = await apiGetAgents();
const agentGrid = document.getElementById('agentCards');
agentGrid.innerHTML = '';
for (const a of agents) {
const card = document.createElement('div');
card.className = 'card clickable';
card.innerHTML = `
<div class="agent-card-avatar">${a.avatar ? `<img src="${a.avatar}" alt="">` : '?'}</div>
<div class="card-title">${escHtml(a.name)}</div>
<div class="card-sub">${escHtml(a.model || 'sonnet')}</div>
`;
card.addEventListener('click', () => {
navigateTo('agent', { id: a.agentId });
});
agentGrid.appendChild(card);
}
// Neuer Agent Placeholder
const placeholder = document.createElement('div');
placeholder.className = 'card placeholder clickable';
placeholder.innerHTML = `
<div class="card-icon">+</div>
<div class="card-title">Neuer Agent</div>
`;
placeholder.addEventListener('click', () => navigateTo('agent'));
agentGrid.appendChild(placeholder);
},
destroy() {}
});
+39 -10
View File
@@ -1,7 +1,5 @@
// ── Session Page ──
let _sessionPageCleanup = [];
registerPage('session', {
async init(params) {
const sessionId = params.id;
@@ -9,7 +7,7 @@ registerPage('session', {
currentSessionId = sessionId;
// Session-Name laden
// Session laden
const session = await apiGetSession(sessionId);
const titleEl = document.getElementById('sessionTitle');
if (session && titleEl) {
@@ -17,6 +15,42 @@ registerPage('session', {
titleEl.addEventListener('dblclick', () => editTitle(sessionId, titleEl));
}
// Agent-Auswahl laden
const agentSelect = document.getElementById('agentSelect');
const sendBtn = document.getElementById('send');
const inputEl = document.getElementById('input');
if (agentSelect) {
const agents = await apiGetAgents();
agentSelect.innerHTML = '<option value="">-- Agent wählen --</option>';
for (const a of agents) {
const opt = document.createElement('option');
opt.value = a.agentId;
opt.textContent = `${a.name} (${a.model})`;
agentSelect.appendChild(opt);
}
// Gespeicherten Agent setzen
if (session && session.agentId) {
agentSelect.value = session.agentId;
}
// Agent-Wechsel speichern
agentSelect.addEventListener('change', async () => {
const val = agentSelect.value || null;
await apiUpdateSession(sessionId, { agentId: val });
updateSendState();
});
}
// Send-Button State
function updateSendState() {
const hasAgent = agentSelect && agentSelect.value;
if (sendBtn) sendBtn.disabled = !hasAgent;
if (inputEl) inputEl.disabled = !hasAgent;
}
updateSendState();
// System Prompt Toggle
const showSystem = document.getElementById('showSystem');
const systemPrompt = document.getElementById('systemPrompt');
@@ -27,18 +61,13 @@ registerPage('session', {
}
// Send
const sendBtn = document.getElementById('send');
if (sendBtn) sendBtn.addEventListener('click', sendMessage);
// Ctrl+Enter
const inputEl = document.getElementById('input');
if (inputEl) {
const keyHandler = (e) => {
inputEl.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'Enter') sendMessage();
};
inputEl.addEventListener('keydown', keyHandler);
// Auto-Resize
});
inputEl.addEventListener('input', function () {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 150) + 'px';