feat: Thumbnail-Varianten (256px) für Agent-Avatare und Session-Images

- ImageService: ResizeDataUrlSmall() für 256px WebP-Thumbnails (Quality 70)
- Neue DB-Spalten: agents.avatar_small, sessions.image_small
- Migration generiert Thumbnails für bestehende Bilder automatisch
- Listen-Endpoints (/api/agents, /api/sessions) liefern kleine Variante
- Detail-Endpoints liefern weiterhin das volle 512px-Bild
- Frontend unverändert (Cards nutzen automatisch die kleineren Bilder)
This commit is contained in:
Meleki committed 2026-03-07 20:09:01 +01:00
1 parent 8efce5f2d5
commit 2a2e98ca68
6 files changed
+116 -15

No files matched your search

+7 -2
View File
@@ -42,7 +42,7 @@ public static class AgentEndpoint
{
a.AgentId,
a.Name,
a.Avatar,
Avatar = a.AvatarSmall ?? a.Avatar,
a.Description,
a.Model,
a.ToolChoice,
@@ -96,6 +96,7 @@ public static class AgentEndpoint
UserId = userId,
Name = body.TryGetProperty("name", out var n) ? n.GetString() ?? "Neuer Agent" : "Neuer Agent",
Avatar = imageService.ResizeDataUrl(avatarRaw),
AvatarSmall = imageService.ResizeDataUrlSmall(avatarRaw),
Description = body.TryGetProperty("description", out var d) ? d.GetString() : null,
Model = body.TryGetProperty("model", out var m) ? m.GetString() ?? "sonnet" : "sonnet",
ToolChoice = body.TryGetProperty("toolChoice", out var tc) ? tc.GetString() ?? "any" : "any",
@@ -131,7 +132,11 @@ public static class AgentEndpoint
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
if (body.TryGetProperty("name", out var n)) agent.Name = n.GetString();
if (body.TryGetProperty("avatar", out var av)) agent.Avatar = imageService.ResizeDataUrl(av.GetString());
if (body.TryGetProperty("avatar", out var av))
{
agent.Avatar = imageService.ResizeDataUrl(av.GetString());
agent.AvatarSmall = imageService.ResizeDataUrlSmall(av.GetString());
}
if (body.TryGetProperty("description", out var d)) agent.Description = d.GetString();
if (body.TryGetProperty("model", out var m)) agent.Model = m.GetString();
if (body.TryGetProperty("toolChoice", out var tc)) agent.ToolChoice = tc.GetString();
+3 -2
View File
@@ -66,7 +66,7 @@ public static class SessionEndpoint
s.SessionId,
s.Name,
s.Description,
s.Image,
Image = s.ImageSmall ?? s.Image,
s.AgentId,
s.Workspace,
s.Context,
@@ -120,6 +120,7 @@ public static class SessionEndpoint
var description = body.TryGetProperty("description", out var d) ? d.GetString() : null;
var imageRaw = body.TryGetProperty("image", out var img) ? img.GetString() : null;
var image = imageService.ResizeDataUrl(imageRaw);
var imageSmall = imageService.ResizeDataUrlSmall(imageRaw);
var agentId = body.TryGetProperty("agentId", out var a) ? a.GetString() : null;
var workspaceRaw = body.TryGetProperty("workspace", out var w) ? w.GetString() : null;
// Relativen Workspace validieren (Path-Traversal verhindern)
@@ -142,7 +143,7 @@ public static class SessionEndpoint
var finalizerAgentId = body.TryGetProperty("finalizerAgentId", out var fa) ? fa.GetString() : null;
var preparerAgentId = body.TryGetProperty("preparerAgentId", out var pra) ? pra.GetString() : null;
var session = await db.CreateSession(userId, name, description, image, agentId, workspace, context, customCommands, avatarScale, avatarOffsetX, avatarOffsetY, reviewAgentId, maxReviewIterations, finalizerAgentId, preparerAgentId);
var session = await db.CreateSession(userId, name, description, image, imageSmall, agentId, workspace, context, customCommands, avatarScale, avatarOffsetX, avatarOffsetY, reviewAgentId, maxReviewIterations, finalizerAgentId, preparerAgentId);
await ctx.Response.WriteAsJsonAsync(session);
}
+2 -1
View File
@@ -5,7 +5,8 @@ public class AgentRecord
public long Id { get; set; }
public string AgentId { get; set; }
public string Name { get; set; }
public string Avatar { get; set; } // base64 data url
public string Avatar { get; set; } // base64 data url (512px)
public string AvatarSmall { get; set; } // base64 data url (256px, für Listen)
public string Description { get; set; }
public string Model { get; set; } // sonnet, sonnet-4-5, sonnet-4-6, opus, opus-4-5, opus-4-6, haiku, haiku-4-5
public string ToolChoice { get; set; } // "any" oder "auto"
+1
View File
@@ -7,6 +7,7 @@ public class SessionRecord
public string Name { get; set; }
public string Description { get; set; }
public string Image { get; set; }
public string ImageSmall { get; set; } // 256px Thumbnail für Listen
public string AgentId { get; set; }
public string Workspace { get; set; }
public string Context { get; set; }
+84 -7
View File
@@ -8,6 +8,7 @@ using Gott.Models;
using Microsoft.Data.Sqlite;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Processing;
namespace Gott.Services;
@@ -262,8 +263,14 @@ public class DatabaseService
CREATE INDEX IF NOT EXISTS idx_shared_messages_message ON shared_messages(message_id);
");
TryAddColumn(conn, "agents", "avatar_small", "TEXT");
TryAddColumn(conn, "sessions", "image_small", "TEXT");
// Migration: Bestehende PNG/JPEG-Avatare/Images zu WebP konvertieren
MigrateImagesToWebP(conn);
// Migration: Small-Thumbnails für bestehende Bilder generieren
MigrateImageThumbnails(conn);
}
private void MigrateImagesToWebP(SqliteConnection conn)
@@ -298,6 +305,70 @@ public class DatabaseService
Console.WriteLine($"[Migration] Converted {agentImages.Count} agent avatars and {sessionImages.Count} session images to WebP");
}
private void MigrateImageThumbnails(SqliteConnection conn)
{
const int smallSize = 256;
const int smallQuality = 70;
// Agent Avatare ohne Thumbnail
var agentsWithoutThumb = conn.Query<(string AgentId, string Avatar)>(
"SELECT agent_id, avatar FROM agents WHERE avatar IS NOT NULL AND avatar != '' AND (avatar_small IS NULL OR avatar_small = '')")
.ToList();
foreach (var (agentId, avatar) in agentsWithoutThumb)
{
var small = ResizeDataUrlToWebP(avatar, smallSize, smallQuality);
if (small != null)
conn.Execute("UPDATE agents SET avatar_small = @small WHERE agent_id = @agentId", new { small, agentId });
}
// Session Images ohne Thumbnail
var sessionsWithoutThumb = conn.Query<(string SessionId, string Image)>(
"SELECT session_id, image FROM sessions WHERE image IS NOT NULL AND image != '' AND (image_small IS NULL OR image_small = '')")
.ToList();
foreach (var (sessionId, image) in sessionsWithoutThumb)
{
var small = ResizeDataUrlToWebP(image, smallSize, smallQuality);
if (small != null)
conn.Execute("UPDATE sessions SET image_small = @small WHERE session_id = @sessionId", new { small, sessionId });
}
if (agentsWithoutThumb.Count > 0 || sessionsWithoutThumb.Count > 0)
Console.WriteLine($"[Migration] Generated {agentsWithoutThumb.Count} agent thumbnails and {sessionsWithoutThumb.Count} session thumbnails");
}
private static string ResizeDataUrlToWebP(string dataUrl, int maxSide, int quality)
{
try
{
var commaIndex = dataUrl.IndexOf(',');
if (commaIndex < 0) return null;
var base64 = dataUrl.Substring(commaIndex + 1);
var bytes = Convert.FromBase64String(base64);
using var image = Image.Load(bytes);
if (image.Width > maxSide || image.Height > maxSide)
{
image.Mutate(x => x.Resize(new ResizeOptions
{
Size = new Size(maxSide, maxSide),
Mode = ResizeMode.Max
}));
}
using var ms = new MemoryStream();
image.Save(ms, new WebpEncoder { Quality = quality });
return "data:image/webp;base64," + Convert.ToBase64String(ms.ToArray());
}
catch
{
return null;
}
}
private static string ConvertDataUrlToWebP(string dataUrl, int quality)
{
try
@@ -395,8 +466,8 @@ public class DatabaseService
agent.CreatedAt = now;
agent.UpdatedAt = now;
await conn.ExecuteAsync(@"
INSERT INTO agents (agent_id, user_id, name, avatar, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, use_web_search, web_search_version, allow_user_choice, task_instruction, avatar_scale, avatar_offset_x, avatar_offset_y, is_deleted, created_at, updated_at)
VALUES (@AgentId, @UserId, @Name, @Avatar, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, @UseWebSearch, @WebSearchVersion, @AllowUserChoice, @TaskInstruction, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, 0, @CreatedAt, @UpdatedAt)
INSERT INTO agents (agent_id, user_id, name, avatar, avatar_small, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, use_web_search, web_search_version, allow_user_choice, task_instruction, avatar_scale, avatar_offset_x, avatar_offset_y, is_deleted, created_at, updated_at)
VALUES (@AgentId, @UserId, @Name, @Avatar, @AvatarSmall, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, @UseWebSearch, @WebSearchVersion, @AllowUserChoice, @TaskInstruction, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, 0, @CreatedAt, @UpdatedAt)
", agent);
return agent;
}
@@ -420,7 +491,7 @@ public class DatabaseService
using var conn = CreateConnection();
agent.UpdatedAt = DateTime.UtcNow.ToString("o");
await conn.ExecuteAsync(@"
UPDATE agents SET name = @Name, avatar = @Avatar, description = @Description,
UPDATE agents SET name = @Name, avatar = @Avatar, avatar_small = @AvatarSmall, description = @Description,
model = @Model, tool_choice = @ToolChoice,
custom_commands = @CustomCommands, auto_commit = @AutoCommit,
use_soul = @UseSoul, use_identity = @UseIdentity, use_user_info = @UseUserInfo, use_memory = @UseMemory, use_web_search = @UseWebSearch, web_search_version = @WebSearchVersion,
@@ -440,7 +511,7 @@ public class DatabaseService
// ── Sessions ──
public async Task<SessionRecord> CreateSession(string userId, string name = null, string description = null, string image = null, string agentId = null, string workspace = null, string context = null, string customCommands = null, double avatarScale = 1.0, double avatarOffsetX = 0, double avatarOffsetY = 0, string reviewAgentId = null, int maxReviewIterations = 3, string finalizerAgentId = null, string preparerAgentId = null)
public async Task<SessionRecord> CreateSession(string userId, string name = null, string description = null, string image = null, string imageSmall = null, string agentId = null, string workspace = null, string context = null, string customCommands = null, double avatarScale = 1.0, double avatarOffsetX = 0, double avatarOffsetY = 0, string reviewAgentId = null, int maxReviewIterations = 3, string finalizerAgentId = null, string preparerAgentId = null)
{
using var conn = CreateConnection();
var now = DateTime.UtcNow.ToString("o");
@@ -451,6 +522,7 @@ public class DatabaseService
Name = name,
Description = description,
Image = image,
ImageSmall = imageSmall,
AgentId = agentId,
Workspace = workspace,
Context = context,
@@ -466,8 +538,8 @@ public class DatabaseService
UpdatedAt = now
};
await conn.ExecuteAsync(@"
INSERT INTO sessions (session_id, user_id, name, description, image, agent_id, workspace, context, custom_commands, avatar_scale, avatar_offset_x, avatar_offset_y, review_agent_id, max_review_iterations, finalizer_agent_id, preparer_agent_id, created_at, updated_at)
VALUES (@SessionId, @UserId, @Name, @Description, @Image, @AgentId, @Workspace, @Context, @CustomCommands, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, @ReviewAgentId, @MaxReviewIterations, @FinalizerAgentId, @PreparerAgentId, @CreatedAt, @UpdatedAt)
INSERT INTO sessions (session_id, user_id, name, description, image, image_small, agent_id, workspace, context, custom_commands, avatar_scale, avatar_offset_x, avatar_offset_y, review_agent_id, max_review_iterations, finalizer_agent_id, preparer_agent_id, created_at, updated_at)
VALUES (@SessionId, @UserId, @Name, @Description, @Image, @ImageSmall, @AgentId, @Workspace, @Context, @CustomCommands, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, @ReviewAgentId, @MaxReviewIterations, @FinalizerAgentId, @PreparerAgentId, @CreatedAt, @UpdatedAt)
", session);
return session;
}
@@ -541,7 +613,12 @@ public class DatabaseService
if (body.TryGetProperty("image", out var img))
{
var imageData = img.GetString();
if (imageService != null) imageData = imageService.ResizeDataUrl(imageData);
if (imageService != null)
{
sets.Add("image_small = @imageSmall");
p.Add("imageSmall", imageService.ResizeDataUrlSmall(imageData));
imageData = imageService.ResizeDataUrl(imageData);
}
sets.Add("image = @image");
p.Add("image", imageData);
}
+19 -3
View File
@@ -9,7 +9,9 @@ namespace Gott.Services;
public class ImageService
{
private const int MaxSideLength = 512;
private const int SmallSideLength = 256;
private const int WebPQuality = 80;
private const int WebPQualitySmall = 70;
/// <summary>
/// Nimmt eine Base64-Data-URL (z.B. "data:image/png;base64,..."), skaliert das Bild
@@ -17,6 +19,20 @@ public class ImageService
/// Gibt null zurück wenn input null/leer ist.
/// </summary>
public string ResizeDataUrl(string dataUrl)
{
return ResizeToWebP(dataUrl, MaxSideLength, WebPQuality);
}
/// <summary>
/// Erzeugt eine kleine Thumbnail-Variante (max 256px, Quality 70) für Dashboard-Listen.
/// Gibt null zurück wenn input null/leer ist.
/// </summary>
public string ResizeDataUrlSmall(string dataUrl)
{
return ResizeToWebP(dataUrl, SmallSideLength, WebPQualitySmall);
}
private static string ResizeToWebP(string dataUrl, int maxSide, int quality)
{
if (string.IsNullOrEmpty(dataUrl)) return null;
@@ -30,18 +46,18 @@ public class ImageService
using var image = Image.Load(bytes);
// Skalieren wenn nötig
if (image.Width > MaxSideLength || image.Height > MaxSideLength)
if (image.Width > maxSide || image.Height > maxSide)
{
image.Mutate(x => x.Resize(new ResizeOptions
{
Size = new Size(MaxSideLength, MaxSideLength),
Size = new Size(maxSide, maxSide),
Mode = ResizeMode.Max
}));
}
// Immer als WebP encodieren
using var ms = new MemoryStream();
image.Save(ms, new WebpEncoder { Quality = WebPQuality });
image.Save(ms, new WebpEncoder { Quality = quality });
var webpBase64 = Convert.ToBase64String(ms.ToArray());
return "data:image/webp;base64," + webpBase64;