@page "/chat" @attribute [Authorize(Roles = "Administrator,Chat")] @inject IChatService ChatService @inject IOllamaService OllamaService @inject OllamaSettings Settings @inject ISettingsService SettingsService @inject IAuthService AuthService @inject NavigationManager Navigation @inject IJSRuntime JS @inject IFactCheckService FactCheckService @rendermode InteractiveServer Chat

Chat

@if (_showSettings) {
präzise (0) kreativ (2)
}
@if (_messages.Count == 0) {
Noch keine Nachrichten. Stelle eine Frage.
} @for (var i = 0; i < _messages.Count; i++) { var msg = _messages[i]; var checks = _factChecks.Count > i ? _factChecks[i] : null;
U
@msg.UserMessage
A
@RenderAssistantMessage(msg.AssistantResponse) @if (checks != null && checks.Count > 0) {
Faktenprüfung
@foreach (var check in checks) { @RenderFactCheck(check) }
}
} @if (_isBusy) {
A
@RenderAssistantMessage(_streamingResponse, true)
}
@if (!string.IsNullOrEmpty(_error)) {
@_error
}
@code { private List _messages = new(); private string _input = string.Empty; private string _error = string.Empty; private bool _isBusy; private int _userId; private bool _showSettings; private bool _sendDisabled => _isBusy || string.IsNullOrWhiteSpace(_input); private string _systemPrompt = string.Empty; private double _temperature = 0.7; private bool _needsScroll; private string _streamingResponse = string.Empty; private bool _factCheckEnabled; private List?> _factChecks = new(); protected override async Task OnInitializedAsync() { var user = await AuthService.GetCurrentUserAsync(); if (user == null || (!user.HasRole(UserRole.Administrator) && !user.HasRole(UserRole.Chat))) { Navigation.NavigateTo("/"); return; } _userId = user.Id; _messages = await ChatService.GetHistoryAsync(_userId); _factChecks = _messages.Select(_ => (List?)null).ToList(); _systemPrompt = await SettingsService.GetChatPromptAsync() ?? Settings.GetChatDefaultPrompt(Settings.Language); _temperature = await SettingsService.GetChatTempAsync(); _factCheckEnabled = await SettingsService.GetChatFactCheckEnabledAsync(); _needsScroll = true; } protected override async Task OnAfterRenderAsync(bool firstRender) { if (_needsScroll) { _needsScroll = false; await ScrollToBottom(); } } private void ToggleSettings() { _showSettings = !_showSettings; } private async Task SaveSettings() { await SettingsService.SaveChatPromptAsync(_systemPrompt); await SettingsService.SaveChatTempAsync(_temperature); await SettingsService.SaveChatFactCheckEnabledAsync(_factCheckEnabled); } private async Task ResetToDefault() { _systemPrompt = Settings.GetChatDefaultPrompt(Settings.Language); _temperature = Settings.ChatTemp; await SaveSettings(); } private async Task HandleKeyDown(KeyboardEventArgs e) { if (e.Key == "Enter" && !e.ShiftKey) { await Send(); } } private async Task Send() { if (string.IsNullOrWhiteSpace(_input) || _isBusy) return; var text = _input.Trim(); _input = string.Empty; _error = string.Empty; _isBusy = true; _streamingResponse = string.Empty; _needsScroll = true; try { var model = await SettingsService.GetStyleModelAsync() ?? "llama3.2"; await OllamaService.ChatStream(model, text, async chunk => { _streamingResponse += chunk; _needsScroll = true; await InvokeAsync(StateHasChanged); }, _systemPrompt, _temperature, Settings.StyleBackend); var msg = new ChatEntity { UserId = _userId, UserMessage = text, AssistantResponse = _streamingResponse, Model = model, Timestamp = DateTime.UtcNow }; await ChatService.AddMessageAsync(msg); _messages.Add(msg); if (_factCheckEnabled && !string.IsNullOrWhiteSpace(_streamingResponse)) { _needsScroll = true; _factChecks.Add(await FactCheckService.VerifyAsync(_streamingResponse, Settings.StyleBackend)); } else { _factChecks.Add(null); } _needsScroll = true; } catch (Exception ex) { _error = $"Fehler: {ex.Message}"; } finally { _isBusy = false; _needsScroll = true; } } private async Task ClearHistory() { await ChatService.ClearHistoryAsync(_userId); _messages.Clear(); } private async Task ScrollToBottom() { await JS.InvokeVoidAsync("eval", "document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight"); } private static (string thinking, string answer) ParseResponse(string text) { if (string.IsNullOrEmpty(text)) return ("", ""); var closeTag = ""; var closeIdx = text.IndexOf(closeTag, StringComparison.Ordinal); if (closeIdx >= 0) { var openTag = ""; var openIdx = text.IndexOf(openTag, StringComparison.Ordinal); if (openIdx >= 0 && openIdx < closeIdx) { var thinking = text[(openIdx + openTag.Length)..closeIdx].Trim(); var answer = text[(closeIdx + closeTag.Length)..].Trim(); return (thinking, answer); } } var lastOpenIdx = text.LastIndexOf("", StringComparison.Ordinal); if (lastOpenIdx >= 0) return (text[(lastOpenIdx + "".Length)..], ""); return ("", text); } private MarkupString RenderAssistantMessage(string text, bool isStreaming = false) { if (string.IsNullOrEmpty(text)) return new MarkupString(isStreaming ? "…" : string.Empty); var (thinking, answer) = ParseResponse(text); var html = new System.Text.StringBuilder(); if (!string.IsNullOrEmpty(thinking)) { html.Append("
Gedankengang
"); html.Append(System.Web.HttpUtility.HtmlEncode(thinking)); html.Append("
"); } if (!string.IsNullOrEmpty(answer)) { var encoded = System.Web.HttpUtility.HtmlEncode(answer); encoded = System.Text.RegularExpressions.Regex.Replace(encoded, @"\*\*(.+?)\*\*", "$1"); encoded = System.Text.RegularExpressions.Regex.Replace(encoded, @"\*(.+?)\*", "$1"); encoded = encoded.Replace("\n", "
"); html.Append(encoded); } else if (string.IsNullOrEmpty(thinking)) { html.Append("…"); } return new MarkupString(html.ToString()); } private MarkupString RenderFactCheck(FactCheckResult check) { var (icon, label, cssClass) = check.Verdict switch { FactCheckVerdict.Confirmed => ("✓", "Bestätigt", "verdict-confirmed"), FactCheckVerdict.Contradicted => ("✗", "Widersprochen", "verdict-contradicted"), FactCheckVerdict.Partial => ("⚠", "Teilweise bestätigt", "verdict-partial"), _ => ("?", "Keine Information", "verdict-none") }; var html = new System.Text.StringBuilder(); html.Append("
"); html.Append($"
{icon} {label}
"); html.Append("
"); html.Append(System.Web.HttpUtility.HtmlEncode(check.Claim)); html.Append("
"); if (!string.IsNullOrEmpty(check.Reasoning)) { html.Append("
"); html.Append(System.Web.HttpUtility.HtmlEncode(check.Reasoning)); html.Append("
"); } if (check.Sources.Count > 0) { html.Append("
"); foreach (var source in check.Sources) { html.Append("
"); html.Append(System.Web.HttpUtility.HtmlEncode(source)); html.Append("
"); } html.Append("
"); } html.Append("
"); return new MarkupString(html.ToString()); } }