Dashboard: - Neue Section 'Geteilte Links' zeigt alle aktiven Shares - Pro Eintrag: Role-Icon, Text-Vorschau, Session-Name, Modus, Datum - Buttons: Link kopieren, öffnen, löschen (mit sofortigem UI-Update) - Section nur sichtbar wenn Shares vorhanden Backend: - SharedEndpoint HandleListShares erweitert: Nachricht-Preview, Role, Session-Name pro Share mitgeliefert Frontend: - apiListShares() Funktion hinzugefügt - Bugfix: Share-URL '/#/shared/' → '/#shared/' (Extra-Slash verursachte 404 weil parseHash leeren Page-Namen ergab)
212 lines
6.7 KiB
C#
212 lines
6.7 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Text.Json;
|
||
using System.Threading.Tasks;
|
||
using Gott.Extensions;
|
||
using Gott.Models;
|
||
using Gott.Services;
|
||
using Microsoft.AspNetCore.Builder;
|
||
using Microsoft.AspNetCore.Http;
|
||
using Microsoft.AspNetCore.Routing;
|
||
|
||
namespace Gott.Endpoints;
|
||
|
||
public static class SharedEndpoint
|
||
{
|
||
public static void MapSharedEndpoints(this IEndpointRouteBuilder app)
|
||
{
|
||
// Authentifizierte Endpunkte
|
||
app.MapPost("/api/messages/{messageId}/share", HandleShareMessage);
|
||
app.MapGet("/api/shared", HandleListShares);
|
||
app.MapDelete("/api/shared/{token}", HandleDeleteShare);
|
||
|
||
// Öffentlicher Endpunkt (Auth-Ausnahme in Middleware)
|
||
app.MapGet("/api/shared/{token}", HandleGetShared);
|
||
}
|
||
|
||
private static async Task HandleShareMessage(HttpContext ctx, DatabaseService db)
|
||
{
|
||
var userId = ctx.GetUserId();
|
||
var messageIdStr = ctx.Request.RouteValues["messageId"]?.ToString();
|
||
if (!long.TryParse(messageIdStr, out var messageId))
|
||
{
|
||
ctx.Response.StatusCode = 400;
|
||
await ctx.Response.WriteAsJsonAsync(new { error = "Invalid messageId" });
|
||
return;
|
||
}
|
||
|
||
// Body lesen (optional: includeContext)
|
||
bool includeContext = false;
|
||
try
|
||
{
|
||
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
||
if (body.TryGetProperty("includeContext", out var ic)) includeContext = ic.GetBoolean();
|
||
}
|
||
catch { /* kein Body oder ungültig – defaults verwenden */ }
|
||
|
||
// Nachricht existiert und gehört dem User?
|
||
var message = await db.GetMessage(messageId);
|
||
if (message == null)
|
||
{
|
||
ctx.Response.StatusCode = 404;
|
||
await ctx.Response.WriteAsJsonAsync(new { error = "Message not found" });
|
||
return;
|
||
}
|
||
|
||
// Chain holen → Session prüfen
|
||
var chain = await db.GetChain(message.ChainId);
|
||
if (chain == null)
|
||
{
|
||
ctx.Response.StatusCode = 404;
|
||
await ctx.Response.WriteAsJsonAsync(new { error = "Chain not found" });
|
||
return;
|
||
}
|
||
|
||
var session = await db.GetSession(chain.SessionId);
|
||
if (session == null || session.UserId != userId)
|
||
{
|
||
ctx.Response.StatusCode = 403;
|
||
await ctx.Response.WriteAsJsonAsync(new { error = "Not authorized" });
|
||
return;
|
||
}
|
||
|
||
var shared = await db.CreateSharedMessage(messageId, userId, includeContext);
|
||
await ctx.Response.WriteAsJsonAsync(new { token = shared.ShareToken });
|
||
}
|
||
|
||
private static async Task HandleGetShared(HttpContext ctx, DatabaseService db)
|
||
{
|
||
var token = ctx.Request.RouteValues["token"]?.ToString();
|
||
if (string.IsNullOrEmpty(token))
|
||
{
|
||
ctx.Response.StatusCode = 400;
|
||
return;
|
||
}
|
||
|
||
var shared = await db.GetSharedMessage(token);
|
||
if (shared == null)
|
||
{
|
||
ctx.Response.StatusCode = 404;
|
||
await ctx.Response.WriteAsJsonAsync(new { error = "Share not found" });
|
||
return;
|
||
}
|
||
|
||
// Ablaufdatum prüfen
|
||
if (!string.IsNullOrEmpty(shared.ExpiresAt) &&
|
||
DateTime.TryParse(shared.ExpiresAt, out var expires) &&
|
||
expires < DateTime.UtcNow)
|
||
{
|
||
ctx.Response.StatusCode = 410;
|
||
await ctx.Response.WriteAsJsonAsync(new { error = "Share expired" });
|
||
return;
|
||
}
|
||
|
||
var message = await db.GetMessage(shared.MessageId);
|
||
if (message == null)
|
||
{
|
||
ctx.Response.StatusCode = 404;
|
||
await ctx.Response.WriteAsJsonAsync(new { error = "Message not found" });
|
||
return;
|
||
}
|
||
|
||
// Chain-Info holen
|
||
var chain = await db.GetChain(message.ChainId);
|
||
|
||
if (shared.IncludeContext)
|
||
{
|
||
// Alle Messages der Chain bis zu dieser Nachricht laden
|
||
var contextMessages = await db.GetChainMessagesUpTo(message.ChainId, shared.MessageId);
|
||
|
||
// Nur text-Messages zurückgeben (keine tool_use/tool_result)
|
||
var textMessages = contextMessages
|
||
.Where(m => m.Type == "text" && m.DeletedOn == null)
|
||
.Select(m => new
|
||
{
|
||
m.Id,
|
||
m.Role,
|
||
m.Content,
|
||
m.CreatedAt
|
||
})
|
||
.ToList();
|
||
|
||
await ctx.Response.WriteAsJsonAsync(new
|
||
{
|
||
mode = "context",
|
||
messages = textMessages,
|
||
model = chain?.Model,
|
||
createdAt = shared.CreatedAt
|
||
});
|
||
}
|
||
else
|
||
{
|
||
// Einzelne Nachricht
|
||
await ctx.Response.WriteAsJsonAsync(new
|
||
{
|
||
mode = "single",
|
||
message = new
|
||
{
|
||
message.Id,
|
||
message.Role,
|
||
message.Content,
|
||
message.CreatedAt
|
||
},
|
||
model = chain?.Model,
|
||
createdAt = shared.CreatedAt
|
||
});
|
||
}
|
||
}
|
||
|
||
private static async Task HandleListShares(HttpContext ctx, DatabaseService db)
|
||
{
|
||
var userId = ctx.GetUserId();
|
||
var shares = await db.GetSharedMessagesByUser(userId);
|
||
|
||
var result = new List<object>();
|
||
foreach (var s in shares)
|
||
{
|
||
var message = await db.GetMessage(s.MessageId);
|
||
string sessionName = null;
|
||
if (message != null)
|
||
{
|
||
var chain = await db.GetChain(message.ChainId);
|
||
if (chain != null)
|
||
{
|
||
var session = await db.GetSession(chain.SessionId);
|
||
sessionName = session?.Name;
|
||
}
|
||
}
|
||
|
||
var preview = message?.Content ?? "";
|
||
if (preview.Length > 150) preview = preview.Substring(0, 150) + "…";
|
||
|
||
result.Add(new
|
||
{
|
||
s.ShareToken,
|
||
s.MessageId,
|
||
s.IncludeContext,
|
||
s.CreatedAt,
|
||
role = message?.Role,
|
||
preview,
|
||
sessionName
|
||
});
|
||
}
|
||
|
||
await ctx.Response.WriteAsJsonAsync(result);
|
||
}
|
||
|
||
private static async Task HandleDeleteShare(HttpContext ctx, DatabaseService db)
|
||
{
|
||
var userId = ctx.GetUserId();
|
||
var token = ctx.Request.RouteValues["token"]?.ToString();
|
||
if (string.IsNullOrEmpty(token))
|
||
{
|
||
ctx.Response.StatusCode = 400;
|
||
return;
|
||
}
|
||
|
||
await db.DeleteSharedMessage(token, userId);
|
||
ctx.Response.StatusCode = 204;
|
||
}
|
||
}
|