Fix: Chat scrollt nach Font-Loading nochmal nach unten

scrollDown() wartet jetzt auf document.fonts.ready und scrollt erneut.
chat-page.js wartet nach loadHistory() zusätzlich per requestAnimationFrame
auf den Repaint nach Font-Swap.
This commit is contained in:
Meleki committed 2026-02-28 15:54:47 +01:00
1 parent 3cb468f4e8
commit 76341dc42b
14 files changed
+349 -121

No files matched your search

+144 -104
View File
@@ -5,6 +5,7 @@ using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Gott.Models;
using Microsoft.AspNetCore.Http;
@@ -34,7 +35,8 @@ public class AnthropicService
string chainId,
string workspace,
string sessionId = null,
int maxIterations = 25)
int maxIterations = 25,
CancellationToken cancellationToken = default)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var usage = new TokenUsage();
@@ -48,128 +50,152 @@ public class AnthropicService
Model = modelId
};
while (iterations < maxIterations)
try
{
iterations++;
var payload = new Dictionary<string, object>
while (iterations < maxIterations)
{
["model"] = modelId,
["max_tokens"] = 4096,
["system"] = systemPrompt,
["messages"] = messages,
["stream"] = true
};
iterations++;
if (!string.IsNullOrEmpty(workspace))
payload["tools"] = _toolService.GetToolDefinitions();
var client = CreateClient();
var json = JsonSerializer.Serialize(payload);
var httpContent = new StringContent(json, Encoding.UTF8, "application/json");
var httpResp = await client.SendAsync(
new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages") { Content = httpContent },
HttpCompletionOption.ResponseHeadersRead
);
if (!httpResp.IsSuccessStatusCode)
{
var errBody = await httpResp.Content.ReadAsStringAsync();
var errMsg = $"API {(int)httpResp.StatusCode}: {errBody}";
await _db.SaveMessage(new MessageRecord
var payload = new Dictionary<string, object>
{
ChainId = chainId, Iteration = iterations,
Role = "system", Type = "error",
Content = errMsg, IsError = true
});
await WriteSseEvent(response, "error", new { error = errMsg });
await FinalizeChain(chain, usage, iterations, "error", sw);
await WriteSseEvent(response, "done", new { });
return;
}
["model"] = modelId,
["max_tokens"] = 16384,
["system"] = systemPrompt,
["messages"] = messages,
["stream"] = true
};
var (stopReason, assistantContent, executedTools) = await ProcessStream(response, httpResp, usage, workspace);
lastStopReason = stopReason;
if (!string.IsNullOrEmpty(workspace))
payload["tools"] = _toolService.GetToolDefinitions();
messages.Add(new { role = "assistant", content = assistantContent });
var client = CreateClient();
var json = JsonSerializer.Serialize(payload);
var httpContent = new StringContent(json, Encoding.UTF8, "application/json");
// Assistant-Blöcke persistieren
foreach (var block in assistantContent)
{
var blockJson = JsonSerializer.Serialize(block);
var el = JsonSerializer.Deserialize<JsonElement>(blockJson);
var blockType = el.GetProperty("type").GetString() ?? "";
var httpResp = await client.SendAsync(
new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages") { Content = httpContent },
HttpCompletionOption.ResponseHeadersRead,
cancellationToken
);
if (blockType == "text")
if (!httpResp.IsSuccessStatusCode)
{
var errBody = await httpResp.Content.ReadAsStringAsync(cancellationToken);
var errMsg = $"API {(int)httpResp.StatusCode}: {errBody}";
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId, Iteration = iterations,
Role = "system", Type = "error",
Content = errMsg, IsError = true
});
await TryWriteSse(response, "error", new { error = errMsg }, cancellationToken);
await FinalizeChain(chain, usage, iterations, "error", sw);
await TryWriteSse(response, "done", new { }, cancellationToken);
return;
}
var (stopReason, assistantContent, executedTools) = await ProcessStream(response, httpResp, usage, workspace, cancellationToken);
lastStopReason = stopReason;
messages.Add(new { role = "assistant", content = assistantContent });
// Assistant-Blöcke persistieren
foreach (var block in assistantContent)
{
var blockJson = JsonSerializer.Serialize(block);
var el = JsonSerializer.Deserialize<JsonElement>(blockJson);
var blockType = el.GetProperty("type").GetString() ?? "";
if (blockType == "text")
{
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId, Iteration = iterations,
Role = "assistant", Type = "text",
Content = el.GetProperty("text").GetString()
});
}
else if (blockType == "tool_use")
{
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId, Iteration = iterations,
Role = "assistant", Type = "tool_use",
ToolName = el.GetProperty("name").GetString(),
ToolCallId = el.GetProperty("id").GetString(),
Content = el.GetProperty("input").ToString()
});
}
}
// Tool Results persistieren
foreach (var tr in executedTools)
{
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId, Iteration = iterations,
Role = "assistant", Type = "text",
Content = el.GetProperty("text").GetString()
Role = "user", Type = "tool_result",
ToolCallId = tr.Id,
Content = tr.Result,
IsError = tr.IsError
});
}
else if (blockType == "tool_use")
if (stopReason != "tool_use")
{
await _db.SaveMessage(new MessageRecord
var finalStopReason = string.IsNullOrEmpty(stopReason) ? "unexpected_end" : stopReason;
await FinalizeChain(chain, usage, iterations, finalStopReason, sw);
await TryWriteSse(response, "done", new
{
ChainId = chainId, Iteration = iterations,
Role = "assistant", Type = "tool_use",
ToolName = el.GetProperty("name").GetString(),
ToolCallId = el.GetProperty("id").GetString(),
Content = el.GetProperty("input").ToString()
model = modelId,
inputTokens = usage.InputTokens,
outputTokens = usage.OutputTokens,
iterations,
chainId,
durationMs = sw.ElapsedMilliseconds,
stopReason = finalStopReason
}, cancellationToken);
return;
}
var toolResults = new List<object>();
foreach (var tr in executedTools)
{
toolResults.Add(new
{
type = "tool_result",
tool_use_id = tr.Id,
content = tr.Result,
is_error = tr.IsError
});
}
messages.Add(new { role = "user", content = toolResults });
}
// Tool Results persistieren
foreach (var tr in executedTools)
await FinalizeChain(chain, usage, iterations, "max_iterations", sw);
await TryWriteSse(response, "error", new { error = "Max iterations erreicht" }, cancellationToken);
await TryWriteSse(response, "done", new { }, cancellationToken);
}
catch (OperationCanceledException)
{
// Client hat die Verbindung getrennt (z.B. Browser-Refresh)
await FinalizeChain(chain, usage, iterations, "client_disconnected", sw);
}
catch (Exception ex)
{
// Unerwarteter Fehler – in DB speichern und versuchen an Client zu senden
var errMsg = $"Stream-Fehler: {ex.Message}";
await _db.SaveMessage(new MessageRecord
{
await _db.SaveMessage(new MessageRecord
{
ChainId = chainId, Iteration = iterations,
Role = "user", Type = "tool_result",
ToolCallId = tr.Id,
Content = tr.Result,
IsError = tr.IsError
});
}
if (stopReason != "tool_use")
{
await FinalizeChain(chain, usage, iterations, stopReason, sw);
await WriteSseEvent(response, "done", new
{
model = modelId,
inputTokens = usage.InputTokens,
outputTokens = usage.OutputTokens,
iterations,
chainId,
durationMs = sw.ElapsedMilliseconds,
stopReason
});
return;
}
var toolResults = new List<object>();
foreach (var tr in executedTools)
{
toolResults.Add(new
{
type = "tool_result",
tool_use_id = tr.Id,
content = tr.Result,
is_error = tr.IsError
});
}
messages.Add(new { role = "user", content = toolResults });
ChainId = chainId, Iteration = iterations,
Role = "system", Type = "error",
Content = errMsg, IsError = true
});
await FinalizeChain(chain, usage, iterations, "error", sw);
await TryWriteSse(response, "error", new { error = errMsg }, cancellationToken);
await TryWriteSse(response, "done", new { }, cancellationToken);
}
await FinalizeChain(chain, usage, iterations, "max_iterations", sw);
await WriteSseEvent(response, "error", new { error = "Max iterations erreicht" });
await WriteSseEvent(response, "done", new { });
}
private async Task FinalizeChain(ChainRecord chain, TokenUsage usage, int iterations, string stopReason, System.Diagnostics.Stopwatch sw)
@@ -186,9 +212,10 @@ public class AnthropicService
HttpResponse response,
HttpResponseMessage httpResp,
TokenUsage usage,
string workspace)
string workspace,
CancellationToken cancellationToken = default)
{
var stream = await httpResp.Content.ReadAsStreamAsync();
var stream = await httpResp.Content.ReadAsStreamAsync(cancellationToken);
using var reader = new StreamReader(stream);
var stopReason = "";
@@ -202,7 +229,7 @@ public class AnthropicService
var executedTools = new List<ToolResult>();
string line;
while ((line = await reader.ReadLineAsync()) != null)
while ((line = await reader.ReadLineAsync(cancellationToken)) != null)
{
if (!line.StartsWith("data: ")) continue;
var data = line["data: ".Length..];
@@ -357,6 +384,19 @@ public class AnthropicService
await response.WriteAsync($"event: {eventType}\ndata: {json}\n\n");
await response.Body.FlushAsync();
}
private static async Task TryWriteSse(HttpResponse response, string eventType, object data, CancellationToken ct)
{
try
{
if (!ct.IsCancellationRequested)
await WriteSseEvent(response, eventType, data);
}
catch
{
// Client bereits disconnected – ignorieren
}
}
}
public record ToolResult(string Id, string Result, bool IsError);
+16
View File
@@ -0,0 +1,16 @@
using System.Collections.Concurrent;
namespace Gott.Services;
public class StreamingTracker
{
private readonly ConcurrentDictionary<string, string> _streaming = new ConcurrentDictionary<string, string>();
public void Start(string sessionId, string chainId) => _streaming[sessionId] = chainId;
public void Stop(string sessionId) => _streaming.TryRemove(sessionId, out _);
public bool IsStreaming(string sessionId) => _streaming.ContainsKey(sessionId);
public string GetChainId(string sessionId) => _streaming.TryGetValue(sessionId, out var chainId) ? chainId : null;
}