Krea inline thumbnails with cached WebP generation and gallery layout
- Add /api/skills/{slug}/output/{filename}/thumb endpoint with ImageSharp
- Generate 300px WebP thumbnails, cached in data/cache/skills/{slug}/
- Replace text badge with inline thumbnail preview in chat
- Click opens full-size image modal with backdrop blur
- wrapKreaGalleries() groups consecutive krea-refs into flex container
- Thumbnails lazy-loaded, cache invalidated when original is newer
This commit is contained in:
1 parent
9e4ed6dada
commit
8d86406037
3 files changed
+174
-33
No files matched your search
@@ -8,6 +8,9 @@ using Gott.Services;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using SixLabors.ImageSharp.Formats.Webp;
|
||||
|
||||
namespace Gott.Endpoints;
|
||||
|
||||
@@ -31,6 +34,9 @@ public static class SkillEndpoint
|
||||
// Skill-Output-Dateien (Bilder etc.)
|
||||
app.MapGet("/api/skills/{slug}/output/{filename}", HandleSkillOutput);
|
||||
|
||||
// Thumbnail (gecached)
|
||||
app.MapGet("/api/skills/{slug}/output/{filename}/thumb", HandleSkillOutputThumb);
|
||||
|
||||
// Agent ↔ Skill Zuordnung
|
||||
app.MapGet("/api/agents/{agentId}/skills", HandleGetAgentSkills);
|
||||
app.MapPut("/api/agents/{agentId}/skills", HandleSetAgentSkills);
|
||||
@@ -135,4 +141,81 @@ public static class SkillEndpoint
|
||||
ctx.Response.Headers["Cache-Control"] = "private, max-age=3600";
|
||||
await ctx.Response.SendFileAsync(filePath);
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> ThumbExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"
|
||||
};
|
||||
|
||||
private static async Task HandleSkillOutputThumb(HttpContext ctx, SkillService skillService)
|
||||
{
|
||||
var slug = ctx.Request.RouteValues["slug"]?.ToString();
|
||||
var filename = ctx.Request.RouteValues["filename"]?.ToString();
|
||||
|
||||
if (string.IsNullOrEmpty(slug) || string.IsNullOrEmpty(filename))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
if (filename.Contains("..") || filename.Contains('/') || filename.Contains('\\'))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
var ext = Path.GetExtension(filename);
|
||||
if (!ThumbExtensions.Contains(ext))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsync("Thumbnails nur für Bilder verfügbar");
|
||||
return;
|
||||
}
|
||||
|
||||
var skill = skillService.GetSkill(slug);
|
||||
if (skill == null) { ctx.Response.StatusCode = 404; return; }
|
||||
|
||||
var originalPath = Path.Combine(skill.Path, "output", filename);
|
||||
if (!File.Exists(originalPath)) { ctx.Response.StatusCode = 404; return; }
|
||||
|
||||
// Cache-Verzeichnis: data/cache/skills/{slug}/
|
||||
var dataDir = Path.GetDirectoryName(skillService.SkillsDir);
|
||||
var cacheDir = Path.Combine(dataDir, "cache", "skills", slug);
|
||||
Directory.CreateDirectory(cacheDir);
|
||||
|
||||
var thumbFilename = Path.GetFileNameWithoutExtension(filename) + ".webp";
|
||||
var thumbPath = Path.Combine(cacheDir, thumbFilename);
|
||||
|
||||
// Thumbnail generieren wenn nicht gecacht oder Original neuer
|
||||
if (!File.Exists(thumbPath) || File.GetLastWriteTimeUtc(originalPath) > File.GetLastWriteTimeUtc(thumbPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var image = await Image.LoadAsync(originalPath);
|
||||
|
||||
// Max 300px auf der längsten Seite
|
||||
var maxSize = 300;
|
||||
image.Mutate(x => x.Resize(new ResizeOptions
|
||||
{
|
||||
Size = new Size(maxSize, maxSize),
|
||||
Mode = ResizeMode.Max
|
||||
}));
|
||||
|
||||
var encoder = new WebpEncoder { Quality = 75 };
|
||||
await image.SaveAsync(thumbPath, encoder);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback: Original ausliefern wenn Thumbnail-Generierung fehlschlägt
|
||||
ctx.Response.ContentType = MimeTypes.GetValueOrDefault(ext, "application/octet-stream");
|
||||
ctx.Response.Headers["Cache-Control"] = "private, max-age=3600";
|
||||
await ctx.Response.SendFileAsync(originalPath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Response.ContentType = "image/webp";
|
||||
ctx.Response.Headers["Cache-Control"] = "private, max-age=86400";
|
||||
await ctx.Response.SendFileAsync(thumbPath);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user