diff --git a/Endpoints/AgentEndpoint.cs b/Endpoints/AgentEndpoint.cs index 674e917..ab2b904 100644 --- a/Endpoints/AgentEndpoint.cs +++ b/Endpoints/AgentEndpoint.cs @@ -42,6 +42,10 @@ public static class AgentEndpoint var avatarRaw = body.TryGetProperty("avatar", out var av) ? av.GetString() : null; + string customCommands = null; + if (body.TryGetProperty("customCommands", out var ccEl)) + customCommands = ccEl.ValueKind == JsonValueKind.Null ? null : ccEl.GetRawText(); + var agent = new AgentRecord { Name = body.TryGetProperty("name", out var n) ? n.GetString() ?? "Neuer Agent" : "Neuer Agent", @@ -50,7 +54,8 @@ public static class AgentEndpoint Model = body.TryGetProperty("model", out var m) ? m.GetString() ?? "sonnet" : "sonnet", Persona = body.TryGetProperty("persona", out var p) ? p.GetString() : null, Instructions = body.TryGetProperty("instructions", out var i) ? i.GetString() : null, - ToolChoice = body.TryGetProperty("toolChoice", out var tc) ? tc.GetString() ?? "any" : "any" + ToolChoice = body.TryGetProperty("toolChoice", out var tc) ? tc.GetString() ?? "any" : "any", + CustomCommands = customCommands }; agent = await db.CreateAgent(agent); @@ -74,6 +79,8 @@ public static class AgentEndpoint if (body.TryGetProperty("persona", out var p)) agent.Persona = p.GetString(); if (body.TryGetProperty("instructions", out var i)) agent.Instructions = i.GetString(); if (body.TryGetProperty("toolChoice", out var tc)) agent.ToolChoice = tc.GetString(); + if (body.TryGetProperty("customCommands", out var cc)) + agent.CustomCommands = cc.ValueKind == JsonValueKind.Null ? null : cc.GetRawText(); await db.UpdateAgent(agent); agent = await db.GetAgent(agentId); diff --git a/Models/AgentRecord.cs b/Models/AgentRecord.cs index 783e36d..0f10068 100644 --- a/Models/AgentRecord.cs +++ b/Models/AgentRecord.cs @@ -11,6 +11,7 @@ public class AgentRecord public string Persona { get; set; } public string Instructions { get; set; } public string ToolChoice { get; set; } // "any" oder "auto" + public string CustomCommands { get; set; } public bool IsDeleted { get; set; } public string CreatedAt { get; set; } public string UpdatedAt { get; set; } diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 218b50b..0039c56 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -127,6 +127,7 @@ public class DatabaseService TryAddColumn(conn, "messages", "attachment_count", "INTEGER NOT NULL DEFAULT 0"); TryAddColumn(conn, "messages", "attachment_names", "TEXT"); TryAddColumn(conn, "agents", "tool_choice", "TEXT DEFAULT 'any'"); + TryAddColumn(conn, "agents", "custom_commands", "TEXT"); } private void TryAddColumn(SqliteConnection conn, string table, string column, string type) @@ -145,8 +146,8 @@ public class DatabaseService agent.CreatedAt = now; agent.UpdatedAt = now; await conn.ExecuteAsync(@" - INSERT INTO agents (agent_id, name, avatar, description, model, persona, instructions, tool_choice, is_deleted, created_at, updated_at) - VALUES (@AgentId, @Name, @Avatar, @Description, @Model, @Persona, @Instructions, @ToolChoice, 0, @CreatedAt, @UpdatedAt) + INSERT INTO agents (agent_id, name, avatar, description, model, persona, instructions, tool_choice, custom_commands, is_deleted, created_at, updated_at) + VALUES (@AgentId, @Name, @Avatar, @Description, @Model, @Persona, @Instructions, @ToolChoice, @CustomCommands, 0, @CreatedAt, @UpdatedAt) ", agent); return agent; } @@ -171,7 +172,8 @@ public class DatabaseService agent.UpdatedAt = DateTime.UtcNow.ToString("o"); await conn.ExecuteAsync(@" UPDATE agents SET name = @Name, avatar = @Avatar, description = @Description, - model = @Model, persona = @Persona, instructions = @Instructions, tool_choice = @ToolChoice, updated_at = @UpdatedAt + model = @Model, persona = @Persona, instructions = @Instructions, tool_choice = @ToolChoice, + custom_commands = @CustomCommands, updated_at = @UpdatedAt WHERE agent_id = @AgentId ", agent); } diff --git a/wwwroot/css/chat.css b/wwwroot/css/chat.css index 576ce78..dcb8d18 100644 --- a/wwwroot/css/chat.css +++ b/wwwroot/css/chat.css @@ -27,6 +27,12 @@ display: none; } +.cmd-bar-label { + font-size: var(--font-size); + opacity: var(--semi-transparent); + white-space: nowrap; +} + .custom-cmd-btn:hover { background: var(--f3-bg); border-color: var(--f3-border); diff --git a/wwwroot/index.html b/wwwroot/index.html index 5d8c9be..f96a669 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -130,6 +130,9 @@ Tool-Nutzung erzwingen (tool_choice: any) + +
+
@@ -219,6 +222,7 @@ + diff --git a/wwwroot/js/commands-ui.js b/wwwroot/js/commands-ui.js new file mode 100644 index 0000000..285c893 --- /dev/null +++ b/wwwroot/js/commands-ui.js @@ -0,0 +1,58 @@ +// ── Custom Commands UI (shared between session-page and agent-page) ── + +function createCommandRow(container, name, command, type) { + name = name || ''; + command = command || ''; + type = type || 'background'; + + const item = document.createElement('div'); + item.className = 'custom-command-item'; + item.innerHTML = ` +
+ + + +
+ + `; + var typeSelect = item.querySelector('.cmd-type'); + typeSelect.value = type; + typeSelect.addEventListener('change', function() { + var cmdInput = item.querySelector('.cmd-command'); + cmdInput.placeholder = typeSelect.value === 'message' ? 'Nachricht...' : 'Befehl (z.B. ./deploy.sh)'; + }); + item.querySelector('.remove-cmd-btn').addEventListener('click', function() { item.remove(); }); + container.appendChild(item); +} + +function getCommandsFromUI(container) { + var items = container.querySelectorAll('.custom-command-item'); + var commands = []; + for (var i = 0; i < items.length; i++) { + var item = items[i]; + var name = item.querySelector('.cmd-name').value.trim(); + var command = item.querySelector('.cmd-command').value.trim(); + var type = item.querySelector('.cmd-type').value; + if (name || command) commands.push({ name: name, command: command, type: type }); + } + return commands.length > 0 ? commands : null; +} + +function loadCommandsIntoUI(container, customCommands) { + if (!customCommands) return; + try { + var cmds = typeof customCommands === 'string' ? JSON.parse(customCommands) : customCommands; + for (var i = 0; i < cmds.length; i++) { + var cmd = cmds[i]; + createCommandRow(container, cmd.name || '', cmd.command || '', cmd.type || 'background'); + } + } catch (e) { /* ignore parse errors */ } +} + +function _escCmdAttr(str) { + return str.replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>'); +} diff --git a/wwwroot/js/pages/agent-page.js b/wwwroot/js/pages/agent-page.js index a9117fe..4c96720 100644 --- a/wwwroot/js/pages/agent-page.js +++ b/wwwroot/js/pages/agent-page.js @@ -28,6 +28,8 @@ registerPage('agent', { const saveBtn = document.getElementById('agentSaveBtn'); const cancelBtn = document.getElementById('agentCancelBtn'); const deleteBtn = document.getElementById('agentDeleteBtn'); + const commandsList = document.getElementById('agentCommandsList'); + const addCommandBtn = document.getElementById('agentAddCommandBtn'); _agentAvatarData = null; @@ -45,8 +47,14 @@ registerPage('agent', { if (agent.avatar) { avatarPreview.innerHTML = `Avatar`; } + + // Custom Commands laden + loadCommandsIntoUI(commandsList, agent.customCommands); } + // Befehl hinzufügen + addCommandBtn.addEventListener('click', () => createCommandRow(commandsList)); + // Avatar Upload avatarBtn.addEventListener('click', () => avatarInput.click()); avatarInput.addEventListener('change', () => { @@ -95,7 +103,8 @@ registerPage('agent', { persona: personaEl.value, instructions: instructionsEl.value, toolChoice: toolChoiceEl.checked ? 'any' : 'auto', - avatar: _agentAvatarData + avatar: _agentAvatarData, + customCommands: getCommandsFromUI(commandsList) }; if (isNew) { diff --git a/wwwroot/js/pages/chat-page.js b/wwwroot/js/pages/chat-page.js index 418262f..b9b236e 100644 --- a/wwwroot/js/pages/chat-page.js +++ b/wwwroot/js/pages/chat-page.js @@ -57,13 +57,44 @@ registerPage('chat', { titleEl.textContent = session.name || 'Unbenannt'; } - // Custom Command Buttons + // Custom Command Buttons (Agent + Session) const cmdBar = document.getElementById('customCmdBar'); if (cmdBar) { cmdBar.innerHTML = ''; - let cmds = []; - try { cmds = JSON.parse(session.customCommands || '[]') || []; } catch(e) {} - cmds.forEach(cmd => { + let agentCmds = []; + if (session.agentId) { + try { + const agent = await apiGetAgent(session.agentId); + if (agent && agent.customCommands) { + agentCmds = JSON.parse(agent.customCommands) || []; + } + } catch(e) {} + } + let sessionCmds = []; + try { sessionCmds = JSON.parse(session.customCommands || '[]') || []; } catch(e) {} + + const allCmds = agentCmds.concat(sessionCmds); + const hasAgentCmds = agentCmds.length > 0; + const hasSessionCmds = sessionCmds.length > 0; + const hasBoth = hasAgentCmds && hasSessionCmds; + + if (hasBoth) { + const sep1 = document.createElement('span'); + sep1.className = 'cmd-bar-label'; + sep1.textContent = 'Agent:'; + cmdBar.appendChild(sep1); + } + + let sessionSepAdded = false; + let cmdIndex = 0; + allCmds.forEach((cmd, idx) => { + if (hasBoth && !sessionSepAdded && idx === agentCmds.length) { + const sep2 = document.createElement('span'); + sep2.className = 'cmd-bar-label'; + sep2.textContent = 'Session:'; + cmdBar.appendChild(sep2); + sessionSepAdded = true; + } if (!cmd.name || !cmd.command) return; const type = cmd.type || 'background'; const btn = document.createElement('button'); diff --git a/wwwroot/js/pages/session-page.js b/wwwroot/js/pages/session-page.js index dd7b23c..7759b0e 100644 --- a/wwwroot/js/pages/session-page.js +++ b/wwwroot/js/pages/session-page.js @@ -3,47 +3,6 @@ let _sessionImageData = null; let _sessionPasteHandler = null; -function _createCommandRow(container, name = '', command = '', type = 'background') { - const item = document.createElement('div'); - item.className = 'custom-command-item'; - item.innerHTML = ` -
- - - -
- - `; - const typeSelect = item.querySelector('.cmd-type'); - typeSelect.value = type; - typeSelect.addEventListener('change', () => { - const cmdInput = item.querySelector('.cmd-command'); - cmdInput.placeholder = typeSelect.value === 'message' ? 'Nachricht...' : 'Befehl (z.B. ./deploy.sh)'; - }); - item.querySelector('.remove-cmd-btn').addEventListener('click', () => item.remove()); - container.appendChild(item); -} - -function _escAttr(str) { - return str.replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>'); -} - -function _getCommandsFromUI(container) { - const items = container.querySelectorAll('.custom-command-item'); - const commands = []; - for (const item of items) { - const name = item.querySelector('.cmd-name').value.trim(); - const command = item.querySelector('.cmd-command').value.trim(); - const type = item.querySelector('.cmd-type').value; - if (name || command) commands.push({ name, command, type }); - } - return commands.length > 0 ? commands : null; -} - registerPage('session', { async init(params) { const sessionId = params.id; @@ -89,16 +48,7 @@ registerPage('session', { } // Custom Commands laden - if (session.customCommands) { - try { - const cmds = typeof session.customCommands === 'string' - ? JSON.parse(session.customCommands) - : session.customCommands; - for (const cmd of cmds) { - _createCommandRow(commandsList, cmd.name || '', cmd.command || '', cmd.type || 'background'); - } - } catch (e) { /* ignore parse errors */ } - } + loadCommandsIntoUI(commandsList, session.customCommands); } else { titleEl.textContent = 'Neue Session'; deleteBtn.hidden = true; @@ -106,7 +56,7 @@ registerPage('session', { } // Befehl hinzufügen - addCommandBtn.addEventListener('click', () => _createCommandRow(commandsList)); + addCommandBtn.addEventListener('click', () => createCommandRow(commandsList)); // Agents laden const agents = await apiGetAgents(); @@ -173,7 +123,7 @@ registerPage('session', { // Speichern saveBtn.addEventListener('click', async () => { - const commands = _getCommandsFromUI(commandsList); + const commands = getCommandsFromUI(commandsList); const data = { name: nameEl.value.trim() || 'Unbenannt', description: descEl.value.trim(),