using System.Text; using System.Text.Json; using IBKRTrader.Core.Analytics; using IBKRTrader.Core.Logging; using IBKRTrader.Modules.Supervisor.Agent; using IBKRTrader.Modules.Supervisor.Persistence; using IBKRTrader.Modules.Supervisor.Services; namespace IBKRTrader.UI.Views.Modules; /// /// Fenster des Supervisor-Moduls: Analyse (Chat mit dem Agenten, Tool-Aufrufe sichtbar), Dossier-Browser, /// Berichte, Settings. Read-only. DB-/Agent-Zugriffe laufen NUR auf Nutzer-Interaktion (Smoke-UI-sicher). /// public sealed class SupervisorMainForm : Form { private readonly SupervisorAgent _agent; private readonly DossierService _dossiers; private readonly ISupervisorReportRepository _reports; private readonly LoggingService _logger; private readonly ComboBox _profile = new() { DropDownStyle = ComboBoxStyle.DropDownList, Width = 160 }; private readonly TextBox _question = new() { Dock = DockStyle.Fill, Multiline = true, Height = 60 }; private readonly RichTextBox _answer = new() { Dock = DockStyle.Fill, ReadOnly = true, Font = new Font("Consolas", 9f) }; private readonly Button _ask = new() { Text = "Fragen", Width = 100 }; private readonly DataGridView _signals = new() { Dock = DockStyle.Left, Width = 360, ReadOnly = true, AllowUserToAddRows = false, RowHeadersVisible = false, AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill }; private readonly RichTextBox _dossier = new() { Dock = DockStyle.Fill, ReadOnly = true, Font = new Font("Consolas", 9f) }; private readonly DataGridView _reportsGrid = new() { Dock = DockStyle.Fill, ReadOnly = true, AllowUserToAddRows = false, RowHeadersVisible = false, AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill }; public SupervisorMainForm( SupervisorAgent agent, DossierService dossiers, ISupervisorReportRepository reports, LoggingService logger) { _agent = agent; _dossiers = dossiers; _reports = reports; _logger = logger; Text = "Supervisor"; Width = 1080; Height = 720; StartPosition = FormStartPosition.CenterScreen; MinimumSize = new Size(800, 520); foreach (var p in SupervisorProfiles.All) _profile.Items.Add(p.Name); _profile.SelectedIndex = 0; BuildLayout(); } private void BuildLayout() { var tabs = new TabControl { Dock = DockStyle.Fill }; // ── Tab: Analyse ── var tabChat = new TabPage("Analyse"); var top = new FlowLayoutPanel { Dock = DockStyle.Top, Height = 36, Padding = new Padding(8, 6, 8, 0) }; top.Controls.Add(new Label { Text = "Profil", AutoSize = true, Margin = new Padding(0, 8, 4, 0) }); top.Controls.Add(_profile); _ask.Click += async (_, _) => await AskAsync(); var qPanel = new Panel { Dock = DockStyle.Top, Height = 70, Padding = new Padding(8, 2, 8, 4) }; qPanel.Controls.Add(_question); var askPanel = new Panel { Dock = DockStyle.Top, Height = 34, Padding = new Padding(8, 0, 8, 0) }; askPanel.Controls.Add(_ask); var answerPanel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(8) }; answerPanel.Controls.Add(_answer); tabChat.Controls.Add(answerPanel); tabChat.Controls.Add(askPanel); tabChat.Controls.Add(qPanel); tabChat.Controls.Add(top); // ── Tab: Dossier-Browser ── var tabDossier = new TabPage("Dossier-Browser"); _signals.SelectionChanged += (_, _) => ShowSelectedDossier(); var refreshSignals = new Button { Text = "Signale laden", Dock = DockStyle.Top, Height = 28 }; refreshSignals.Click += (_, _) => LoadSignals(); var left = new Panel { Dock = DockStyle.Left, Width = 360 }; left.Controls.Add(_signals); left.Controls.Add(refreshSignals); var dossierPanel = new Panel { Dock = DockStyle.Fill, Padding = new Padding(8) }; dossierPanel.Controls.Add(_dossier); tabDossier.Controls.Add(dossierPanel); tabDossier.Controls.Add(left); // ── Tab: Berichte ── var tabReports = new TabPage("Berichte"); var refreshReports = new Button { Text = "Berichte laden", Dock = DockStyle.Top, Height = 28 }; refreshReports.Click += (_, _) => LoadReports(); tabReports.Controls.Add(_reportsGrid); tabReports.Controls.Add(refreshReports); // ── Tab: Settings (Info) ── var tabSettings = new TabPage("Settings"); tabSettings.Controls.Add(new Label { Dock = DockStyle.Fill, Padding = new Padding(16), Text = "Supervisor – read-only Analyse/Forensik über alle Module.\n\n" + "OpenRouter-Key: env IBKRTRADER_OPENROUTER_KEY oder Datei 'openrouter.key' (gitignored).\n" + $" Status: {(string.IsNullOrEmpty(OpenRouterClient.DefaultApiKeyProvider()) ? "NICHT gesetzt – Chat nicht verfügbar" : "gesetzt")}\n\n" + "Tagesbericht (opt-in): env IBKRTRADER_SUPERVISOR_DAILY = Stunde 0–23.\n" + "MCP-Light (opt-in): env IBKRTRADER_MCP_PORT = Port (bindet nur 127.0.0.1).\n\n" + "Sicherheit: OpenRouter ist ein bewusst freigegebener externer Datenempfänger. Es werden nur\n" + "Analyse-Daten der Tools gesendet, niemals Secrets. Kein Tool kann handeln oder schreiben." }); tabs.TabPages.AddRange(new[] { tabChat, tabDossier, tabReports, tabSettings }); Controls.Add(tabs); } // ── Analyse ── private async Task AskAsync() { var question = _question.Text.Trim(); if (string.IsNullOrEmpty(question)) return; _ask.Enabled = false; _answer.Clear(); var profile = SupervisorProfiles.ByName(_profile.Text); var progress = new Progress(s => AppendLine(s)); try { var result = await _agent.AskAsync(question, profile: profile, progress: progress); AppendLine(""); AppendLine("─── Antwort ───"); AppendLine(result.Answer); _reports.Insert(new SupervisorReport { Profile = profile.Name, Model = SupervisorAgent.DefaultModel, Question = question, Answer = result.Answer, ToolCallsJson = JsonSerializer.Serialize(result.ToolInvocations.Select(i => new { i.Tool, i.Arguments })), ToolCallCount = result.ToolInvocations.Count, PromptTokens = result.PromptTokens, CompletionTokens = result.CompletionTokens }); } catch (Exception ex) { AppendLine(""); AppendLine($"FEHLER: {ex.Message}"); _logger.Warn("Supervisor", $"Analyse fehlgeschlagen: {ex.Message}"); } finally { _ask.Enabled = true; } } private void AppendLine(string text) { if (_answer.InvokeRequired) { _answer.BeginInvoke(() => AppendLine(text)); return; } _answer.AppendText(text + "\n"); _answer.ScrollToCaret(); } // ── Dossier ── private void LoadSignals() { try { _signals.DataSource = _dossiers.RecentSignals(200); } catch (Exception ex) { _logger.Warn("Supervisor", $"Signale laden fehlgeschlagen: {ex.Message}"); } } private void ShowSelectedDossier() { if (_signals.CurrentRow?.DataBoundItem is not SignalSummary s) return; try { _dossier.Text = DossierBuilder.ToMarkdown(_dossiers.BuildForSignal(s.SignalId)); } catch (Exception ex) { _dossier.Text = $"FEHLER: {ex.Message}"; } } // ── Berichte ── private void LoadReports() { try { _reportsGrid.DataSource = _reports.GetRecent(100) .Select(r => new { r.CreatedAt, r.Profile, r.Model, r.Question, r.ToolCallCount, r.PromptTokens, r.CompletionTokens }) .ToList(); } catch (Exception ex) { _logger.Warn("Supervisor", $"Berichte laden fehlgeschlagen: {ex.Message}"); } } }