From 7149904ae4a675a8e09fc7feb6a80f60a2f9a36f Mon Sep 17 00:00:00 2001 From: Meleki Date: Mon, 2 Mar 2026 15:02:39 +0100 Subject: [PATCH] =?UTF-8?q?Profil-Seite:=20Anzeigename,=20Passwort=20?= =?UTF-8?q?=C3=A4ndern,=20Persona=20(ersetzt=20USER.md)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neuer Menüpunkt 'Mein Profil' statt 'Logout' - Profil-Page: Anzeigename, Persona (Freitext), Passwort ändern, Logout - ProfileEndpoint: GET/PUT /api/profile, PUT /api/profile/password - User-Persona im System-Prompt statt USER.md (Fallback: Anzeigename/Username) - SystemPromptBuilder: dataDir-Parameter entfernt, userPersona-Parameter hinzugefügt - DB-Migration: persona-Spalte in users-Tabelle --- Endpoints/ChatEndpoint.cs | 4 +- Endpoints/ProfileEndpoint.cs | 96 ++++++++++++++++++++++++++ Endpoints/SessionEndpoint.cs | 8 +-- Models/UserRecord.cs | 10 +++ Program.cs | 1 + Services/DatabaseService.cs | 9 +++ Services/SystemPromptBuilder.cs | 21 ++---- wwwroot/css/form.css | 24 +++++++ wwwroot/index.html | 41 ++++++++++- wwwroot/js/api.js | 34 ++++++++++ wwwroot/js/pages/dashboard.js | 7 +- wwwroot/js/pages/profile-page.js | 112 +++++++++++++++++++++++++++++++ 12 files changed, 341 insertions(+), 26 deletions(-) create mode 100644 Endpoints/ProfileEndpoint.cs create mode 100644 wwwroot/js/pages/profile-page.js diff --git a/Endpoints/ChatEndpoint.cs b/Endpoints/ChatEndpoint.cs index 3dd9938..640929c 100644 --- a/Endpoints/ChatEndpoint.cs +++ b/Endpoints/ChatEndpoint.cs @@ -82,9 +82,9 @@ public static class ChatEndpoint var markedMessages = await db.GetMarkedMessages(sessionId); var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId); - var dataDir = memoryService.DataDir; var agentSkills = await db.GetAgentSkills(agent.AgentId); - var systemPromptText = SystemPromptBuilder.Build(agent, session, markedMessages, memoryService, agentMemoryDir, dataDir, agentSkills, skillService); + var user = await db.GetUser(userId); + var systemPromptText = SystemPromptBuilder.Build(agent, session, markedMessages, memoryService, agentMemoryDir, agentSkills, skillService, userPersona: user?.GetEffectivePersona()); var chainId = Guid.NewGuid().ToString("N"); diff --git a/Endpoints/ProfileEndpoint.cs b/Endpoints/ProfileEndpoint.cs new file mode 100644 index 0000000..0aeaf22 --- /dev/null +++ b/Endpoints/ProfileEndpoint.cs @@ -0,0 +1,96 @@ +using System.Text.Json; +using Gott.Extensions; +using Gott.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace Gott.Endpoints; + +public static class ProfileEndpoint +{ + public static void MapProfileEndpoints(this WebApplication app) + { + app.MapGet("/api/profile", HandleGet); + app.MapPut("/api/profile", HandleUpdate); + app.MapPut("/api/profile/password", HandlePasswordChange); + } + + private static async System.Threading.Tasks.Task HandleGet(HttpContext ctx, DatabaseService db) + { + var userId = ctx.GetUserId(); + var user = await db.GetUser(userId); + if (user == null) + { + ctx.Response.StatusCode = 404; + await ctx.Response.WriteAsJsonAsync(new { error = "User not found" }); + return; + } + + await ctx.Response.WriteAsJsonAsync(new + { + userId = user.UserId, + username = user.Username, + displayName = user.DisplayName, + persona = user.Persona, + isAdmin = user.IsAdmin + }); + } + + private static async System.Threading.Tasks.Task HandleUpdate(HttpContext ctx, DatabaseService db) + { + var userId = ctx.GetUserId(); + var body = await JsonSerializer.DeserializeAsync(ctx.Request.Body); + + var displayName = body.TryGetProperty("displayName", out var dn) ? dn.GetString() : null; + var persona = body.TryGetProperty("persona", out var p) ? p.GetString() : null; + + await db.UpdateUserProfile(userId, displayName, persona); + + var user = await db.GetUser(userId); + await ctx.Response.WriteAsJsonAsync(new + { + userId = user.UserId, + username = user.Username, + displayName = user.DisplayName, + persona = user.Persona, + isAdmin = user.IsAdmin + }); + } + + private static async System.Threading.Tasks.Task HandlePasswordChange(HttpContext ctx, DatabaseService db, AuthService auth) + { + var userId = ctx.GetUserId(); + var body = await JsonSerializer.DeserializeAsync(ctx.Request.Body); + + var currentPassword = body.TryGetProperty("currentPassword", out var cp) ? cp.GetString() : null; + var newPassword = body.TryGetProperty("newPassword", out var np) ? np.GetString() : null; + + if (string.IsNullOrEmpty(currentPassword) || string.IsNullOrEmpty(newPassword)) + { + ctx.Response.StatusCode = 400; + await ctx.Response.WriteAsJsonAsync(new { error = "Aktuelles und neues Passwort erforderlich" }); + return; + } + + if (newPassword.Length < 4) + { + ctx.Response.StatusCode = 400; + await ctx.Response.WriteAsJsonAsync(new { error = "Passwort muss mindestens 4 Zeichen lang sein" }); + return; + } + + var user = await db.GetUser(userId); + if (user == null || !auth.VerifyPassword(currentPassword, user.PasswordHash)) + { + ctx.Response.StatusCode = 401; + await ctx.Response.WriteAsJsonAsync(new { error = "Aktuelles Passwort ist falsch" }); + return; + } + + var newHash = auth.HashPassword(newPassword); + await db.UpdateUserPassword(userId, newHash); + + await ctx.Response.WriteAsJsonAsync(new { ok = true }); + } +} diff --git a/Endpoints/SessionEndpoint.cs b/Endpoints/SessionEndpoint.cs index 307b023..aa091dc 100644 --- a/Endpoints/SessionEndpoint.cs +++ b/Endpoints/SessionEndpoint.cs @@ -139,9 +139,9 @@ public static class SessionEndpoint var markedMessages = await db.GetMarkedMessages(sessionId); var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId); - var dataDir = memoryService.DataDir; var agentSkills = await db.GetAgentSkills(agent.AgentId); - var systemPrompt = SystemPromptBuilder.Build(agent, session, markedMessages, memoryService, agentMemoryDir, dataDir, agentSkills, skillService); + var user = await db.GetUser(userId); + var systemPrompt = SystemPromptBuilder.Build(agent, session, markedMessages, memoryService, agentMemoryDir, agentSkills, skillService, userPersona: user?.GetEffectivePersona()); ctx.Response.ContentType = "text/plain; charset=utf-8"; await ctx.Response.WriteAsync(systemPrompt); @@ -173,9 +173,9 @@ public static class SessionEndpoint var markedMessages = await db.GetMarkedMessages(sessionId); var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId); - var dataDir = memoryService.DataDir; var agentSkills = await db.GetAgentSkills(agent.AgentId); - var blocks = SystemPromptBuilder.BuildBlocks(agent, session, markedMessages, memoryService, agentMemoryDir, dataDir, agentSkills, skillService); + var user = await db.GetUser(userId); + var blocks = SystemPromptBuilder.BuildBlocks(agent, session, markedMessages, memoryService, agentMemoryDir, agentSkills, skillService, userPersona: user?.GetEffectivePersona()); await ctx.Response.WriteAsJsonAsync(blocks.Select(b => new { b.Icon, b.Label, b.Content })); } diff --git a/Models/UserRecord.cs b/Models/UserRecord.cs index a127ff7..39a56c3 100644 --- a/Models/UserRecord.cs +++ b/Models/UserRecord.cs @@ -10,4 +10,14 @@ public class UserRecord public string CreatedAt { get; set; } public string UpdatedAt { get; set; } public bool IsAdmin { get; set; } + public string Persona { get; set; } + + public string GetEffectivePersona() + { + if (!string.IsNullOrWhiteSpace(Persona)) + return Persona; + + var name = !string.IsNullOrWhiteSpace(DisplayName) ? DisplayName : Username; + return $"Mein Name ist **{name}**."; + } } diff --git a/Program.cs b/Program.cs index 3ede814..2602445 100644 --- a/Program.cs +++ b/Program.cs @@ -37,5 +37,6 @@ app.MapWorkspaceEndpoint(); app.MapInfoEndpoint(); app.MapBackupEndpoint(); app.MapSkillEndpoints(); +app.MapProfileEndpoints(); app.Run("http://0.0.0.0:19000"); diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index a1de799..edeca08 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -159,6 +159,7 @@ public class DatabaseService TryAddColumn(conn, "sessions", "user_id", "TEXT"); TryAddColumn(conn, "agents", "user_id", "TEXT"); TryAddColumn(conn, "users", "is_admin", "INTEGER NOT NULL DEFAULT 0"); + TryAddColumn(conn, "users", "persona", "TEXT"); conn.Execute(@" CREATE TABLE IF NOT EXISTS agent_skills ( @@ -733,4 +734,12 @@ public class DatabaseService "UPDATE users SET password_hash = @passwordHash, updated_at = @now WHERE user_id = @userId", new { userId, passwordHash, now = DateTime.UtcNow.ToString("o") }); } + + public async Task UpdateUserProfile(string userId, string displayName, string persona) + { + using var conn = CreateConnection(); + await conn.ExecuteAsync( + "UPDATE users SET display_name = @displayName, persona = @persona, updated_at = @now WHERE user_id = @userId", + new { userId, displayName, persona, now = DateTime.UtcNow.ToString("o") }); + } } diff --git a/Services/SystemPromptBuilder.cs b/Services/SystemPromptBuilder.cs index 8c7c73c..01a3caa 100644 --- a/Services/SystemPromptBuilder.cs +++ b/Services/SystemPromptBuilder.cs @@ -13,7 +13,7 @@ public class PromptBlock public static class SystemPromptBuilder { - public static List BuildBlocks(AgentRecord agent, SessionRecord session, IEnumerable markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, string dataDir = null, IEnumerable skillSlugs = null, SkillService skillService = null) + public static List BuildBlocks(AgentRecord agent, SessionRecord session, IEnumerable markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, IEnumerable skillSlugs = null, SkillService skillService = null, string userPersona = null) { var blocks = new List(); @@ -39,19 +39,10 @@ public static class SystemPromptBuilder blocks.Add(new PromptBlock { Icon = "fa-heart", Label = "Agent · SOUL.md (UseSoul)", Content = soul }); } - // USER.md - if (agent.UseUserInfo && !string.IsNullOrWhiteSpace(dataDir)) + // User-Info: Persona aus dem Profil des Users der gerade schreibt + if (agent.UseUserInfo && !string.IsNullOrWhiteSpace(userPersona)) { - var userFile = System.IO.Path.Combine(dataDir, "USER.md"); - if (System.IO.File.Exists(userFile)) - { - var userContent = System.IO.File.ReadAllText(userFile); - if (!string.IsNullOrWhiteSpace(userContent)) - { - var absolutePath = System.IO.Path.GetFullPath(userFile); - blocks.Add(new PromptBlock { Icon = "fa-user", Label = "Agent · USER.md (UseUserInfo)", Content = $"{userContent}\n\n_Pfad: `{absolutePath}`_" }); - } - } + blocks.Add(new PromptBlock { Icon = "fa-user", Label = "User · Persona (UseUserInfo)", Content = $"## Dein Gegenüber\nDie folgende Beschreibung stammt von der Person, die dir gerade schreibt:\n\n{userPersona}" }); } bool toolAny = string.Equals(agent.ToolChoice, "any", System.StringComparison.OrdinalIgnoreCase); @@ -233,9 +224,9 @@ Bei Zeilenreferenzen (z.B. `[Datei: foo.txt:11]`) IMMER `cat -n` verwenden um di return blocks; } - public static string Build(AgentRecord agent, SessionRecord session, IEnumerable markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, string dataDir = null, IEnumerable skillSlugs = null, SkillService skillService = null) + public static string Build(AgentRecord agent, SessionRecord session, IEnumerable markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, IEnumerable skillSlugs = null, SkillService skillService = null, string userPersona = null) { - var blocks = BuildBlocks(agent, session, markedMessages, memoryService, agentMemoryDir, dataDir, skillSlugs, skillService); + var blocks = BuildBlocks(agent, session, markedMessages, memoryService, agentMemoryDir, skillSlugs, skillService, userPersona); return string.Join("\n\n", blocks.Select(b => b.Content)); } } diff --git a/wwwroot/css/form.css b/wwwroot/css/form.css index 11c4140..e736b40 100644 --- a/wwwroot/css/form.css +++ b/wwwroot/css/form.css @@ -195,6 +195,30 @@ label.skill-item { margin-left: auto; } +/* ── Profile ── */ + +.profile-message { + padding: var(--badge-spacing) var(--box-spacing); + border-radius: var(--border-radius); + margin-top: var(--badge-spacing); +} + +.profile-message-success { + background: rgba(0, 180, 0, 0.15); + color: #0a0; +} + +.profile-message-error { + background: rgba(220, 0, 0, 0.15); + color: var(--dangerous); +} + +.profile-divider { + border: none; + border-top: var(--border-width) solid var(--f2-border); + margin: var(--box-spacing) 0; +} + .danger-btn { color: var(--dangerous); border-color: var(--dangerous); diff --git a/wwwroot/index.html b/wwwroot/index.html index 338b840..adaed68 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -50,7 +50,7 @@

// gott

@@ -288,6 +288,44 @@
+ + @@ -315,6 +353,7 @@ + diff --git a/wwwroot/js/api.js b/wwwroot/js/api.js index 5504b02..73340dc 100644 --- a/wwwroot/js/api.js +++ b/wwwroot/js/api.js @@ -221,6 +221,40 @@ 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(); +} + async function apiBackupDb() { const resp = await fetch('/api/backup', { method: 'POST' }); if (!resp.ok) throw new Error('Backup fehlgeschlagen'); diff --git a/wwwroot/js/pages/dashboard.js b/wwwroot/js/pages/dashboard.js index 044c29a..f6637bc 100644 --- a/wwwroot/js/pages/dashboard.js +++ b/wwwroot/js/pages/dashboard.js @@ -8,11 +8,10 @@ registerPage('dashboard', { navigateTo('settings'); }); - // Logout-Link - document.getElementById('dashboardLogoutLink').addEventListener('click', async (e) => { + // Profil-Link + document.getElementById('dashboardProfileLink').addEventListener('click', (e) => { e.preventDefault(); - await apiLogout(); - navigateTo('login'); + navigateTo('profile'); }); // Globaler Click-Handler zum Schließen aller Card-Menüs diff --git a/wwwroot/js/pages/profile-page.js b/wwwroot/js/pages/profile-page.js new file mode 100644 index 0000000..4491bbf --- /dev/null +++ b/wwwroot/js/pages/profile-page.js @@ -0,0 +1,112 @@ +// ── Profile Page ── + +registerPage('profile', { + async init() { + var profile = await apiGetProfile(); + if (!profile) return; + + var usernameInput = document.getElementById('profileUsername'); + var displayNameInput = document.getElementById('profileDisplayName'); + var personaInput = document.getElementById('profilePersona'); + var saveBtn = document.getElementById('profileSaveBtn'); + var messageDiv = document.getElementById('profileMessage'); + + var currentPwInput = document.getElementById('profileCurrentPassword'); + var newPwInput = document.getElementById('profileNewPassword'); + var confirmPwInput = document.getElementById('profileConfirmPassword'); + var passwordBtn = document.getElementById('profilePasswordBtn'); + var passwordMessageDiv = document.getElementById('profilePasswordMessage'); + + var logoutBtn = document.getElementById('profileLogoutBtn'); + + // Felder befüllen + usernameInput.value = profile.username || ''; + displayNameInput.value = profile.displayName || ''; + personaInput.value = profile.persona || ''; + + // Profil speichern + saveBtn.addEventListener('click', async function() { + saveBtn.disabled = true; + saveBtn.textContent = 'Speichern...'; + messageDiv.hidden = true; + + try { + var updated = await apiUpdateProfile({ + displayName: displayNameInput.value.trim(), + persona: personaInput.value.trim() + }); + + // currentUser aktualisieren + if (currentUser) { + currentUser.displayName = updated.displayName; + } + + messageDiv.textContent = 'Profil gespeichert'; + messageDiv.className = 'profile-message profile-message-success'; + messageDiv.hidden = false; + setTimeout(function() { messageDiv.hidden = true; }, 3000); + } catch (err) { + messageDiv.textContent = err.message; + messageDiv.className = 'profile-message profile-message-error'; + messageDiv.hidden = false; + } finally { + saveBtn.disabled = false; + saveBtn.textContent = 'Speichern'; + } + }); + + // Passwort ändern + passwordBtn.addEventListener('click', async function() { + passwordMessageDiv.hidden = true; + + var currentPw = currentPwInput.value; + var newPw = newPwInput.value; + var confirmPw = confirmPwInput.value; + + if (!currentPw || !newPw) { + passwordMessageDiv.textContent = 'Alle Felder ausfüllen'; + passwordMessageDiv.className = 'profile-message profile-message-error'; + passwordMessageDiv.hidden = false; + return; + } + + if (newPw !== confirmPw) { + passwordMessageDiv.textContent = 'Neue Passwörter stimmen nicht überein'; + passwordMessageDiv.className = 'profile-message profile-message-error'; + passwordMessageDiv.hidden = false; + return; + } + + passwordBtn.disabled = true; + passwordBtn.textContent = 'Ändern...'; + + try { + await apiChangePassword(currentPw, newPw); + + currentPwInput.value = ''; + newPwInput.value = ''; + confirmPwInput.value = ''; + + passwordMessageDiv.textContent = 'Passwort geändert'; + passwordMessageDiv.className = 'profile-message profile-message-success'; + passwordMessageDiv.hidden = false; + setTimeout(function() { passwordMessageDiv.hidden = true; }, 3000); + } catch (err) { + passwordMessageDiv.textContent = err.message; + passwordMessageDiv.className = 'profile-message profile-message-error'; + passwordMessageDiv.hidden = false; + } finally { + passwordBtn.disabled = false; + passwordBtn.textContent = 'Passwort ändern'; + } + }); + + // Logout + logoutBtn.addEventListener('click', async function() { + await apiLogout(); + currentUser = null; + navigateTo('login'); + }); + }, + destroy() {} +});