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}"); }
}
}