From a768fb8e047e0e487dee39ff099e2f943837916b Mon Sep 17 00:00:00 2001 From: Alberto Balbo Date: Tue, 21 Apr 2026 23:19:50 +0200 Subject: [PATCH] Creazione progetto SynthData Pro: struttura WPF completa MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aggiunti tutti i file sorgente per la nuova applicazione desktop WPF "SynthData Pro" (namespace Dione) per la generazione dati tramite LLM locale/remoto. Inclusi: - Progetto .csproj, configurazione .NET 4.8.1, risorse e file di soluzione. - UI moderna con Material Design, sidebar, title bar custom, e navigazione tra Dashboard, Generazione Live, Impostazioni e Telemetria. - Modelli dati (AppSettings, DataProject, SchemaColumn, TelemetryLog) e layer dati SQLite con migrazione automatica. - ViewModel principali per dashboard KPI/grafici, generazione streaming, impostazioni, telemetria. - Tutte le View XAML e relativi code-behind. - Localizzazione italiana e attenzione all'usabilità. - Pronto per estensioni future (Data Designer, moduli placeholder). --- Dione/App.config | 6 + Dione/App.xaml | 22 + Dione/App.xaml.cs | 14 + Dione/Converters/InverseBoolConverter.cs | 23 + Dione/Data/SynthDataDbContext.cs | 294 ++++++++ Dione/Dione.csproj | 187 +++++ Dione/Dione.slnx | 3 + Dione/Dione/Dione_new.csproj | 0 Dione/Dione/Models/DataProject.cs | 0 Dione/Dione/Models/SchemaColumn.cs | 0 Dione/Dione/Models/TelemetryLog.cs | 0 Dione/Dione/ViewModels/MainWindowViewModel.cs | 0 Dione/Dione/Views/DashboardView.xaml.cs | 0 Dione/Dione/Views/PlaceholderView.xaml.cs | 0 Dione/Directory.Build.targets | 17 + Dione/MainWindow.xaml | 182 +++++ Dione/MainWindow.xaml.cs | 91 +++ Dione/Models/AppSettings.cs | 46 ++ Dione/Models/DataProject.cs | 110 +++ Dione/Models/SchemaColumn.cs | 25 + Dione/Models/TelemetryLog.cs | 39 ++ Dione/Properties/Resources.Designer.cs | 71 ++ Dione/Properties/Resources.resx | 117 ++++ Dione/Properties/Settings.Designer.cs | 30 + Dione/Properties/Settings.settings | 7 + Dione/ViewModels/DashboardViewModel.cs | 199 ++++++ Dione/ViewModels/LiveGenerationViewModel.cs | 586 ++++++++++++++++ Dione/ViewModels/MainWindowViewModel.cs | 79 +++ Dione/ViewModels/SettingsViewModel.cs | 303 +++++++++ Dione/ViewModels/TelemetryHistoryViewModel.cs | 124 ++++ Dione/Views/DashboardView.xaml | 161 +++++ Dione/Views/DashboardView.xaml.cs | 12 + Dione/Views/DataDesignerView.xaml | 115 ++++ Dione/Views/DataDesignerView.xaml.cs | 12 + Dione/Views/LiveGenerationView.xaml | 643 ++++++++++++++++++ Dione/Views/LiveGenerationView.xaml.cs | 38 ++ Dione/Views/PlaceholderView.xaml | 14 + Dione/Views/PlaceholderView.xaml.cs | 17 + Dione/Views/SettingsView.xaml | 292 ++++++++ Dione/Views/SettingsView.xaml.cs | 12 + Dione/Views/TelemetryHistoryView.xaml | 124 ++++ Dione/Views/TelemetryHistoryView.xaml.cs | 12 + 42 files changed, 4027 insertions(+) create mode 100644 Dione/App.config create mode 100644 Dione/App.xaml create mode 100644 Dione/App.xaml.cs create mode 100644 Dione/Converters/InverseBoolConverter.cs create mode 100644 Dione/Data/SynthDataDbContext.cs create mode 100644 Dione/Dione.csproj create mode 100644 Dione/Dione.slnx create mode 100644 Dione/Dione/Dione_new.csproj create mode 100644 Dione/Dione/Models/DataProject.cs create mode 100644 Dione/Dione/Models/SchemaColumn.cs create mode 100644 Dione/Dione/Models/TelemetryLog.cs create mode 100644 Dione/Dione/ViewModels/MainWindowViewModel.cs create mode 100644 Dione/Dione/Views/DashboardView.xaml.cs create mode 100644 Dione/Dione/Views/PlaceholderView.xaml.cs create mode 100644 Dione/Directory.Build.targets create mode 100644 Dione/MainWindow.xaml create mode 100644 Dione/MainWindow.xaml.cs create mode 100644 Dione/Models/AppSettings.cs create mode 100644 Dione/Models/DataProject.cs create mode 100644 Dione/Models/SchemaColumn.cs create mode 100644 Dione/Models/TelemetryLog.cs create mode 100644 Dione/Properties/Resources.Designer.cs create mode 100644 Dione/Properties/Resources.resx create mode 100644 Dione/Properties/Settings.Designer.cs create mode 100644 Dione/Properties/Settings.settings create mode 100644 Dione/ViewModels/DashboardViewModel.cs create mode 100644 Dione/ViewModels/LiveGenerationViewModel.cs create mode 100644 Dione/ViewModels/MainWindowViewModel.cs create mode 100644 Dione/ViewModels/SettingsViewModel.cs create mode 100644 Dione/ViewModels/TelemetryHistoryViewModel.cs create mode 100644 Dione/Views/DashboardView.xaml create mode 100644 Dione/Views/DashboardView.xaml.cs create mode 100644 Dione/Views/DataDesignerView.xaml create mode 100644 Dione/Views/DataDesignerView.xaml.cs create mode 100644 Dione/Views/LiveGenerationView.xaml create mode 100644 Dione/Views/LiveGenerationView.xaml.cs create mode 100644 Dione/Views/PlaceholderView.xaml create mode 100644 Dione/Views/PlaceholderView.xaml.cs create mode 100644 Dione/Views/SettingsView.xaml create mode 100644 Dione/Views/SettingsView.xaml.cs create mode 100644 Dione/Views/TelemetryHistoryView.xaml create mode 100644 Dione/Views/TelemetryHistoryView.xaml.cs diff --git a/Dione/App.config b/Dione/App.config new file mode 100644 index 0000000..aee9adf --- /dev/null +++ b/Dione/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Dione/App.xaml b/Dione/App.xaml new file mode 100644 index 0000000..3d2aa2c --- /dev/null +++ b/Dione/App.xaml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/Dione/App.xaml.cs b/Dione/App.xaml.cs new file mode 100644 index 0000000..1c7a383 --- /dev/null +++ b/Dione/App.xaml.cs @@ -0,0 +1,14 @@ +using System.Windows; +using Dione.Data; + +namespace Dione +{ + public partial class App : Application + { + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + SynthDataDbContext.EnsureCreated(); + } + } +} diff --git a/Dione/Converters/InverseBoolConverter.cs b/Dione/Converters/InverseBoolConverter.cs new file mode 100644 index 0000000..bc3c154 --- /dev/null +++ b/Dione/Converters/InverseBoolConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace Dione.Converters +{ + public class InverseBoolConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool b) + return !b; + return false; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool b) + return !b; + return false; + } + } +} diff --git a/Dione/Data/SynthDataDbContext.cs b/Dione/Data/SynthDataDbContext.cs new file mode 100644 index 0000000..a57792d --- /dev/null +++ b/Dione/Data/SynthDataDbContext.cs @@ -0,0 +1,294 @@ +using System; +using System.Collections.Generic; +using System.Data.SQLite; +using Dione.Models; + +namespace Dione.Data +{ + public static class SynthDataDbContext + { + private static readonly string DbPath = System.IO.Path.Combine( + System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData), + "SynthDataPro", "synthdata.db"); + + private static string ConnStr => $"Data Source={DbPath};Version=3;"; + + // ── Schema ─────────────────────────────────────────────────────────────── + + public static void EnsureCreated() + { + var dir = System.IO.Path.GetDirectoryName(DbPath); + if (!System.IO.Directory.Exists(dir)) + System.IO.Directory.CreateDirectory(dir); + if (!System.IO.File.Exists(DbPath)) + SQLiteConnection.CreateFile(DbPath); + + using (var conn = new SQLiteConnection(ConnStr)) + { + conn.Open(); + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = "CREATE TABLE IF NOT EXISTS AppSettings (" + + "Id INTEGER PRIMARY KEY DEFAULT 1," + + "ApiEndpoint TEXT DEFAULT 'http://127.0.0.1:1234/v1/chat/completions'," + + "ModelName TEXT DEFAULT ''," + + "ApiKey TEXT DEFAULT ''," + + "Temperature REAL NOT NULL DEFAULT 0.7," + + "MaxTokens INTEGER NOT NULL DEFAULT 2048," + + "SystemPrompt TEXT DEFAULT ''," + + "UserPrompt TEXT DEFAULT ''," + + "OutputDirectory TEXT DEFAULT ''," + + "OutputFilePrefix TEXT DEFAULT 'batch'," + + "MaxFileSizeMb INTEGER NOT NULL DEFAULT 250," + + "ApiTimeoutSeconds INTEGER NOT NULL DEFAULT 120," + + "TimeoutPerTokenRatio REAL NOT NULL DEFAULT 0.5," + + "EnableQualityVerification INTEGER NOT NULL DEFAULT 0," + + "UseSameModelForVerification INTEGER NOT NULL DEFAULT 1," + + "VerificationApiEndpoint TEXT DEFAULT ''," + + "VerificationModelName TEXT DEFAULT ''," + + "VerificationApiKey TEXT DEFAULT ''," + + "RevenuePerHighQualityRecord REAL NOT NULL DEFAULT 0.005," + + "ElectricityCostPerKwh REAL NOT NULL DEFAULT 0.25," + + "SystemPowerWatt REAL NOT NULL DEFAULT 350," + + "ApiCostType TEXT DEFAULT 'Free'," + + "ApiCostPerCall REAL NOT NULL DEFAULT 0," + + "ApiCostPerBlock REAL NOT NULL DEFAULT 0," + + "ApiBlockSize INTEGER NOT NULL DEFAULT 1000" + + ");"; + cmd.ExecuteNonQuery(); + } + + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = "CREATE TABLE IF NOT EXISTS TelemetryLogs (" + + "Id INTEGER PRIMARY KEY AUTOINCREMENT," + + "Timestamp TEXT NOT NULL," + + "BatchId TEXT," + + "ModelUsed TEXT," + + "TokensPrompt INTEGER NOT NULL DEFAULT 0," + + "TokensCompletion INTEGER NOT NULL DEFAULT 0," + + "ExecutionTimeMs INTEGER NOT NULL DEFAULT 0," + + "OutputPreview TEXT," + + "ErrorMessage TEXT," + + "IsSuccess INTEGER NOT NULL DEFAULT 0," + + "QualityScore REAL NOT NULL DEFAULT 0," + + "Revenue REAL NOT NULL DEFAULT 0," + + "RecordsInBatch INTEGER NOT NULL DEFAULT 0" + + ");"; + cmd.ExecuteNonQuery(); + } + + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = "INSERT OR IGNORE INTO AppSettings (Id) VALUES (1);"; + cmd.ExecuteNonQuery(); + } + + // ── Migrate TelemetryLogs: add columns if missing ── + using (var cmd = conn.CreateCommand()) + { + var existingCols = new System.Collections.Generic.HashSet(); + cmd.CommandText = "PRAGMA table_info(TelemetryLogs)"; + using (var r = cmd.ExecuteReader()) + while (r.Read()) + existingCols.Add(r.GetString(1)); + + if (!existingCols.Contains("QualityScore")) + { cmd.CommandText = "ALTER TABLE TelemetryLogs ADD COLUMN QualityScore REAL NOT NULL DEFAULT 0"; cmd.ExecuteNonQuery(); } + if (!existingCols.Contains("Revenue")) + { cmd.CommandText = "ALTER TABLE TelemetryLogs ADD COLUMN Revenue REAL NOT NULL DEFAULT 0"; cmd.ExecuteNonQuery(); } + if (!existingCols.Contains("RecordsInBatch")) + { cmd.CommandText = "ALTER TABLE TelemetryLogs ADD COLUMN RecordsInBatch INTEGER NOT NULL DEFAULT 0"; cmd.ExecuteNonQuery(); } + } + } + } + + // ── AppSettings CRUD ───────────────────────────────────────────────────── + + public static AppSettings LoadSettings() + { + EnsureCreated(); + using (var conn = new SQLiteConnection(ConnStr)) + { + conn.Open(); + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = + "SELECT ApiEndpoint,ModelName,ApiKey,Temperature,MaxTokens," + + "SystemPrompt,UserPrompt,OutputDirectory,OutputFilePrefix,MaxFileSizeMb," + + "ApiTimeoutSeconds,TimeoutPerTokenRatio," + + "EnableQualityVerification,UseSameModelForVerification," + + "VerificationApiEndpoint,VerificationModelName,VerificationApiKey," + + "RevenuePerHighQualityRecord,ElectricityCostPerKwh,SystemPowerWatt," + + "ApiCostType,ApiCostPerCall,ApiCostPerBlock,ApiBlockSize " + + "FROM AppSettings WHERE Id=1"; + + using (var r = cmd.ExecuteReader()) + { + if (r.Read()) + return new AppSettings + { + ApiEndpoint = r.GetValue(0)?.ToString() ?? "", + ModelName = r.GetValue(1)?.ToString() ?? "", + ApiKey = r.GetValue(2)?.ToString() ?? "", + Temperature = r.IsDBNull(3) ? 0.7 : r.GetDouble(3), + MaxTokens = r.IsDBNull(4) ? 2048 : r.GetInt32(4), + SystemPrompt = r.GetValue(5)?.ToString() ?? "", + UserPrompt = r.GetValue(6)?.ToString() ?? "", + OutputDirectory = r.GetValue(7)?.ToString() ?? "", + OutputFilePrefix = r.GetValue(8)?.ToString() ?? "batch", + MaxFileSizeMb = r.IsDBNull(9) ? 250 : r.GetInt32(9), + ApiTimeoutSeconds = r.IsDBNull(10) ? 120 : r.GetInt32(10), + TimeoutPerTokenRatio = r.IsDBNull(11) ? 0.5 : r.GetDouble(11), + EnableQualityVerification = !r.IsDBNull(12) && r.GetInt32(12) != 0, + UseSameModelForVerification = r.IsDBNull(13) || r.GetInt32(13) != 0, + VerificationApiEndpoint = r.GetValue(14)?.ToString() ?? "", + VerificationModelName = r.GetValue(15)?.ToString() ?? "", + VerificationApiKey = r.GetValue(16)?.ToString() ?? "", + RevenuePerHighQualityRecord = r.IsDBNull(17) ? 0.005 : r.GetDouble(17), + ElectricityCostPerKwh = r.IsDBNull(18) ? 0.25 : r.GetDouble(18), + SystemPowerWatt = r.IsDBNull(19) ? 350 : r.GetDouble(19), + ApiCostType = r.GetValue(20)?.ToString() ?? "Free", + ApiCostPerCall = r.IsDBNull(21) ? 0 : r.GetDouble(21), + ApiCostPerBlock = r.IsDBNull(22) ? 0 : r.GetDouble(22), + ApiBlockSize = r.IsDBNull(23) ? 1000 : r.GetInt32(23), + }; + } + } + } + return new AppSettings(); + } + + public static void SaveSettings(AppSettings s) + { + EnsureCreated(); + using (var conn = new SQLiteConnection(ConnStr)) + { + conn.Open(); + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = + "UPDATE AppSettings SET " + + "ApiEndpoint=@api,ModelName=@model,ApiKey=@apikey," + + "Temperature=@temp,MaxTokens=@mt," + + "SystemPrompt=@sp,UserPrompt=@up," + + "OutputDirectory=@odir,OutputFilePrefix=@opfx,MaxFileSizeMb=@mfsz," + + "ApiTimeoutSeconds=@timeout,TimeoutPerTokenRatio=@tokratio," + + "EnableQualityVerification=@eqv,UseSameModelForVerification=@usesame," + + "VerificationApiEndpoint=@vapi,VerificationModelName=@vmodel,VerificationApiKey=@vapikey," + + "RevenuePerHighQualityRecord=@rev," + + "ElectricityCostPerKwh=@ecost,SystemPowerWatt=@spow," + + "ApiCostType=@actype,ApiCostPerCall=@acall,ApiCostPerBlock=@ablock,ApiBlockSize=@ablk " + + "WHERE Id=1"; + cmd.Parameters.AddWithValue("@api", s.ApiEndpoint); + cmd.Parameters.AddWithValue("@model", s.ModelName); + cmd.Parameters.AddWithValue("@apikey", s.ApiKey); + cmd.Parameters.AddWithValue("@temp", s.Temperature); + cmd.Parameters.AddWithValue("@mt", s.MaxTokens); + cmd.Parameters.AddWithValue("@sp", s.SystemPrompt); + cmd.Parameters.AddWithValue("@up", s.UserPrompt); + cmd.Parameters.AddWithValue("@odir", s.OutputDirectory); + cmd.Parameters.AddWithValue("@opfx", s.OutputFilePrefix); + cmd.Parameters.AddWithValue("@mfsz", s.MaxFileSizeMb); + cmd.Parameters.AddWithValue("@timeout", s.ApiTimeoutSeconds); + cmd.Parameters.AddWithValue("@tokratio", s.TimeoutPerTokenRatio); + cmd.Parameters.AddWithValue("@eqv", s.EnableQualityVerification ? 1 : 0); + cmd.Parameters.AddWithValue("@usesame", s.UseSameModelForVerification ? 1 : 0); + cmd.Parameters.AddWithValue("@vapi", s.VerificationApiEndpoint); + cmd.Parameters.AddWithValue("@vmodel", s.VerificationModelName); + cmd.Parameters.AddWithValue("@vapikey", s.VerificationApiKey); + cmd.Parameters.AddWithValue("@rev", s.RevenuePerHighQualityRecord); + cmd.Parameters.AddWithValue("@ecost", s.ElectricityCostPerKwh); + cmd.Parameters.AddWithValue("@spow", s.SystemPowerWatt); + cmd.Parameters.AddWithValue("@actype", s.ApiCostType); + cmd.Parameters.AddWithValue("@acall", s.ApiCostPerCall); + cmd.Parameters.AddWithValue("@ablock", s.ApiCostPerBlock); + cmd.Parameters.AddWithValue("@ablk", s.ApiBlockSize); + cmd.ExecuteNonQuery(); + } + } + } + + // ── Telemetry ──────────────────────────────────────────────────────────── + + public static List QueryAll() + { + var results = new List(); + if (!System.IO.File.Exists(DbPath)) return results; + using (var conn = new SQLiteConnection(ConnStr)) + { + conn.Open(); + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = + "SELECT Id,Timestamp,BatchId,ModelUsed,TokensPrompt,TokensCompletion," + + "ExecutionTimeMs,OutputPreview,ErrorMessage,IsSuccess " + + "FROM TelemetryLogs ORDER BY Id DESC LIMIT 1000"; + using (var r = cmd.ExecuteReader()) + { + while (r.Read()) + results.Add(new TelemetryLog + { + Id = r.GetInt32(0), + Timestamp = DateTime.TryParse(r.GetValue(1)?.ToString(), out var ts) ? ts : default, + BatchId = r.GetValue(2)?.ToString(), + ModelUsed = r.GetValue(3)?.ToString(), + TokensPrompt = r.IsDBNull(4) ? 0 : r.GetInt32(4), + TokensCompletion = r.IsDBNull(5) ? 0 : r.GetInt32(5), + ExecutionTimeMs = r.IsDBNull(6) ? 0 : r.GetInt64(6), + OutputPreview = r.GetValue(7)?.ToString(), + ErrorMessage = r.GetValue(8)?.ToString(), + IsSuccess = !r.IsDBNull(9) && r.GetInt32(9) != 0, + }); + } + } + } + return results; + } + + public static void InsertLog(TelemetryLog log) + { + EnsureCreated(); + using (var conn = new SQLiteConnection(ConnStr)) + { + conn.Open(); + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = + "INSERT INTO TelemetryLogs " + + "(Timestamp,BatchId,ModelUsed,TokensPrompt,TokensCompletion," + + "ExecutionTimeMs,OutputPreview,ErrorMessage,IsSuccess,QualityScore,Revenue,RecordsInBatch) " + + "VALUES (@ts,@bid,@model,@tp,@tc,@et,@op,@err,@ok,0,0,0)"; + cmd.Parameters.AddWithValue("@ts", log.Timestamp.ToString("o")); + cmd.Parameters.AddWithValue("@bid", (object)log.BatchId ?? DBNull.Value); + cmd.Parameters.AddWithValue("@model", (object)log.ModelUsed ?? DBNull.Value); + cmd.Parameters.AddWithValue("@tp", log.TokensPrompt); + cmd.Parameters.AddWithValue("@tc", log.TokensCompletion); + cmd.Parameters.AddWithValue("@et", log.ExecutionTimeMs); + cmd.Parameters.AddWithValue("@op", (object)log.OutputPreview ?? DBNull.Value); + cmd.Parameters.AddWithValue("@err", (object)log.ErrorMessage ?? DBNull.Value); + cmd.Parameters.AddWithValue("@ok", log.IsSuccess ? 1 : 0); + cmd.ExecuteNonQuery(); + } + } + } + + // ── Reset ──────────────────────────────────────────────────────────────── + + public static void DeleteAllTelemetry() + { + if (!System.IO.File.Exists(DbPath)) return; + using (var conn = new SQLiteConnection(ConnStr)) + { + conn.Open(); + using (var cmd = conn.CreateCommand()) { cmd.CommandText = "DELETE FROM TelemetryLogs;"; cmd.ExecuteNonQuery(); } + } + } + + public static void ResetDatabase() + { + if (System.IO.File.Exists(DbPath)) System.IO.File.Delete(DbPath); + EnsureCreated(); + } + } +} \ No newline at end of file diff --git a/Dione/Dione.csproj b/Dione/Dione.csproj new file mode 100644 index 0000000..e246203 --- /dev/null +++ b/Dione/Dione.csproj @@ -0,0 +1,187 @@ + + + + + Debug + AnyCPU + {C455F7DB-C47E-4D71-87D6-9C122F18F986} + WinExe + Dione + Dione + v4.8.1 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + true + true + PackageReference + latest + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + 4.0 + + + + + + C:\Users\alber\.nuget\packages\stub.system.data.sqlite.core.netframework\1.0.119\lib\net46\System.Data.SQLite.dll + + + C:\Users\alber\.nuget\packages\system.data.sqlite.ef6\1.0.119\lib\net46\System.Data.SQLite.EF6.dll + + + C:\Users\alber\.nuget\packages\communitytoolkit.mvvm\8.4.0\lib\netstandard2.0\CommunityToolkit.Mvvm.dll + + + C:\Users\alber\.nuget\packages\entityframework\6.5.1\lib\net45\EntityFramework.dll + + + C:\Users\alber\.nuget\packages\newtonsoft.json\13.0.3\lib\net45\Newtonsoft.Json.dll + + + C:\Users\alber\.nuget\packages\livecharts\0.9.7\lib\net45\LiveCharts.dll + + + C:\Users\alber\.nuget\packages\livecharts.wpf\0.9.7\lib\net45\LiveCharts.Wpf.dll + + + + + + + + + + + + + + MSBuild:Compile + Designer + + + + + + + DataDesignerView.xaml + + + LiveGenerationView.xaml + + + SettingsView.xaml + + + TelemetryHistoryView.xaml + + + MSBuild:Compile + Designer + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + App.xaml + Code + + + + + + + + + + + + + + + + DashboardView.xaml + Code + + + PlaceholderView.xaml + Code + + + MainWindow.xaml + Code + + + MSBuild:Compile + + + MSBuild:Compile + + + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + \ No newline at end of file diff --git a/Dione/Dione.slnx b/Dione/Dione.slnx new file mode 100644 index 0000000..f2f6868 --- /dev/null +++ b/Dione/Dione.slnx @@ -0,0 +1,3 @@ + + + diff --git a/Dione/Dione/Dione_new.csproj b/Dione/Dione/Dione_new.csproj new file mode 100644 index 0000000..e69de29 diff --git a/Dione/Dione/Models/DataProject.cs b/Dione/Dione/Models/DataProject.cs new file mode 100644 index 0000000..e69de29 diff --git a/Dione/Dione/Models/SchemaColumn.cs b/Dione/Dione/Models/SchemaColumn.cs new file mode 100644 index 0000000..e69de29 diff --git a/Dione/Dione/Models/TelemetryLog.cs b/Dione/Dione/Models/TelemetryLog.cs new file mode 100644 index 0000000..e69de29 diff --git a/Dione/Dione/ViewModels/MainWindowViewModel.cs b/Dione/Dione/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..e69de29 diff --git a/Dione/Dione/Views/DashboardView.xaml.cs b/Dione/Dione/Views/DashboardView.xaml.cs new file mode 100644 index 0000000..e69de29 diff --git a/Dione/Dione/Views/PlaceholderView.xaml.cs b/Dione/Dione/Views/PlaceholderView.xaml.cs new file mode 100644 index 0000000..e69de29 diff --git a/Dione/Directory.Build.targets b/Dione/Directory.Build.targets new file mode 100644 index 0000000..96ec96d --- /dev/null +++ b/Dione/Directory.Build.targets @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + diff --git a/Dione/MainWindow.xaml b/Dione/MainWindow.xaml new file mode 100644 index 0000000..8d18d62 --- /dev/null +++ b/Dione/MainWindow.xaml @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Dione/MainWindow.xaml.cs b/Dione/MainWindow.xaml.cs new file mode 100644 index 0000000..ee497ef --- /dev/null +++ b/Dione/MainWindow.xaml.cs @@ -0,0 +1,91 @@ +using System; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Input; +using System.Windows.Interop; + +namespace Dione +{ + public partial class MainWindow : Window + { + public MainWindow() + { + InitializeComponent(); + SourceInitialized += OnSourceInitialized; + } + + private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + if (e.ClickCount == 2) + WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; + else + DragMove(); + } + + private void OnSourceInitialized(object sender, EventArgs e) + { + var handle = new WindowInteropHelper(this).Handle; + HwndSource.FromHwnd(handle)?.AddHook(WndProc); + } + + private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) + { + // WM_GETMINMAXINFO + if (msg == 0x0024) + { + var mmi = Marshal.PtrToStructure(lParam); + + var monitor = MonitorFromWindow(hwnd, 0x00000002 /* MONITOR_DEFAULTTONEAREST */); + if (monitor != IntPtr.Zero) + { + var monitorInfo = new MONITORINFO { cbSize = Marshal.SizeOf() }; + if (GetMonitorInfo(monitor, ref monitorInfo)) + { + var work = monitorInfo.rcWork; + var area = monitorInfo.rcMonitor; + + mmi.ptMaxPosition.x = work.left - area.left; + mmi.ptMaxPosition.y = work.top - area.top; + mmi.ptMaxSize.x = work.right - work.left; + mmi.ptMaxSize.y = work.bottom - work.top; + } + } + + Marshal.StructureToPtr(mmi, lParam, true); + handled = true; + } + return IntPtr.Zero; + } + + [DllImport("user32.dll")] + private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags); + + [DllImport("user32.dll")] + private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi); + + [StructLayout(LayoutKind.Sequential)] + private struct POINT { public int x, y; } + + [StructLayout(LayoutKind.Sequential)] + private struct RECT { public int left, top, right, bottom; } + + [StructLayout(LayoutKind.Sequential)] + private struct MINMAXINFO + { + public POINT ptReserved; + public POINT ptMaxSize; + public POINT ptMaxPosition; + public POINT ptMaxTrackSize; + public POINT ptMinTrackSize; + } + + [StructLayout(LayoutKind.Sequential)] + private struct MONITORINFO + { + public int cbSize; + public RECT rcMonitor; + public RECT rcWork; + public uint dwFlags; + } + } +} diff --git a/Dione/Models/AppSettings.cs b/Dione/Models/AppSettings.cs new file mode 100644 index 0000000..1119a00 --- /dev/null +++ b/Dione/Models/AppSettings.cs @@ -0,0 +1,46 @@ +namespace Dione.Models +{ + /// + /// Configurazione globale dell'applicazione sempre un solo record (Id = 1). + /// + public class AppSettings + { + public int Id { get; set; } = 1; + + // ?? API principale ?? + public string ApiEndpoint { get; set; } = "http://127.0.0.1:1234/v1/chat/completions"; + public string ModelName { get; set; } = ""; + public string ApiKey { get; set; } = ""; + public double Temperature { get; set; } = 0.7; + public int MaxTokens { get; set; } = 2048; + + // ?? Prompt ?? + public string SystemPrompt { get; set; } = ""; + public string UserPrompt { get; set; } = ""; + + // ?? Output ?? + public string OutputDirectory { get; set; } = ""; + public string OutputFilePrefix { get; set; } = "batch"; + public int MaxFileSizeMb { get; set; } = 250; + + // ?? Timeout ?? + public int ApiTimeoutSeconds { get; set; } = 120; + public double TimeoutPerTokenRatio { get; set; } = 0.5; + + // ?? Verifica qualit ?? + public bool EnableQualityVerification { get; set; } = false; + public bool UseSameModelForVerification { get; set; } = true; + public string VerificationApiEndpoint { get; set; } = ""; + public string VerificationModelName { get; set; } = ""; + public string VerificationApiKey { get; set; } = ""; + public double RevenuePerHighQualityRecord { get; set; } = 0.005; + + // ?? Costi ?? + public double ElectricityCostPerKwh { get; set; } = 0.25; + public double SystemPowerWatt { get; set; } = 350; + public string ApiCostType { get; set; } = "Free"; + public double ApiCostPerCall { get; set; } = 0.0; + public double ApiCostPerBlock { get; set; } = 0.0; + public int ApiBlockSize { get; set; } = 1000; + } +} diff --git a/Dione/Models/DataProject.cs b/Dione/Models/DataProject.cs new file mode 100644 index 0000000..9e9a86e --- /dev/null +++ b/Dione/Models/DataProject.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Dione.Models +{ + [Table("DataProjects")] + public class DataProject + { + [Key] + public int Id { get; set; } + + [Required, MaxLength(200)] + public string Name { get; set; } + + [MaxLength(512)] + public string Description { get; set; } + + [MaxLength(512)] + public string ApiEndpoint { get; set; } = "http://127.0.0.1:1234/v1/"; + + [MaxLength(128)] + public string ModelName { get; set; } + + /// API Key per servizi remoti (OpenAI, Anthropic, Google AI, etc.) + [MaxLength(512)] + public string ApiKey { get; set; } = ""; + + public double Temperature { get; set; } = 0.7; + + public int MaxTokens { get; set; } = 2048; + + [MaxLength(16)] + public string OutputFormat { get; set; } = "JSON"; + + public string SystemPrompt { get; set; } + + public string FewShotExamples { get; set; } + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public DateTime? LastRunAt { get; set; } + + public int RecordsPerFile { get; set; } = 100; + + [MaxLength(512)] + public string OutputDirectory { get; set; } = ""; + + [MaxLength(256)] + public string OutputFileName { get; set; } = "fin_ticks"; + + public bool ContinuousMode { get; set; } = true; + + public int MinFieldCount { get; set; } = 120; + + [MaxLength(16)] + public string FieldSeparator { get; set; } = "|"; + + // ?? Cost Management ?? + /// Costo elettricit in /kWh + public double ElectricityCostPerKwh { get; set; } = 0.25; + + /// Potenza stimata del sistema in Watt durante generazione + public double SystemPowerWatt { get; set; } = 350; + + /// Costo API AI per chiamata () + public double ApiCostPerCall { get; set; } = 0.0; + + /// Costo API AI per blocco di N chiamate () + public double ApiCostPerBlock { get; set; } = 0.0; + + /// Numero chiamate per blocco tariffario + public int ApiBlockSize { get; set; } = 1000; + + /// Tipo tariffazione: PerCall, PerBlock, Free + [MaxLength(16)] + public string ApiCostType { get; set; } = "Free"; + + // ?? Revenue / Verification ?? + /// Endpoint AI di verifica (pu essere lo stesso o diverso) + [MaxLength(512)] + public string VerificationApiEndpoint { get; set; } = ""; + + /// Modello AI per verifica qualit + [MaxLength(128)] + public string VerificationModelName { get; set; } = ""; + + /// API Key per servizio verifica qualit + [MaxLength(512)] + public string VerificationApiKey { get; set; } = ""; + + /// Se true, usa stesso endpoint/modello/key della generazione per la verifica + public bool UseSameModelForVerification { get; set; } = true; + + /// Abilita verifica qualit automatica + public bool EnableQualityVerification { get; set; } = false; + + /// Valore base per record di alta qualit () + public double RevenuePerHighQualityRecord { get; set; } = 0.005; + + /// Timeout base per chiamata API in secondi + public int ApiTimeoutSeconds { get; set; } = 120; + + /// Timeout aggiuntivo per token (secondi per ogni 100 token) + public double TimeoutPerTokenRatio { get; set; } = 0.5; + + public virtual ICollection SchemaColumns { get; set; } = new List(); + } +} diff --git a/Dione/Models/SchemaColumn.cs b/Dione/Models/SchemaColumn.cs new file mode 100644 index 0000000..ef63e49 --- /dev/null +++ b/Dione/Models/SchemaColumn.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Dione.Models +{ + [Table("SchemaColumns")] + public class SchemaColumn + { + [Key] + public int Id { get; set; } + + public int DataProjectId { get; set; } + + [Required, MaxLength(128)] + public string ColumnName { get; set; } + + [Required, MaxLength(64)] + public string DataType { get; set; } + + public int SortOrder { get; set; } + + [ForeignKey(nameof(DataProjectId))] + public virtual DataProject DataProject { get; set; } + } +} diff --git a/Dione/Models/TelemetryLog.cs b/Dione/Models/TelemetryLog.cs new file mode 100644 index 0000000..cacd3c7 --- /dev/null +++ b/Dione/Models/TelemetryLog.cs @@ -0,0 +1,39 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Dione.Models +{ + [Table("TelemetryLogs")] + public class TelemetryLog + { + [Key] + public int Id { get; set; } + + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + + [MaxLength(64)] + public string BatchId { get; set; } + + public int Seed { get; set; } + + public string Prompt { get; set; } + + [MaxLength(128)] + public string ModelUsed { get; set; } + + public int TokensPrompt { get; set; } + + public int TokensCompletion { get; set; } + + public long ExecutionTimeMs { get; set; } + + [MaxLength(1000)] + public string OutputPreview { get; set; } + + [MaxLength(2000)] + public string ErrorMessage { get; set; } + + public bool IsSuccess { get; set; } + } +} diff --git a/Dione/Properties/Resources.Designer.cs b/Dione/Properties/Resources.Designer.cs new file mode 100644 index 0000000..5f6bb3f --- /dev/null +++ b/Dione/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// Codice generato da uno strumento. +// Versione runtime:4.0.30319.42000 +// +// Le modifiche apportate a questo file possono causare un comportamento non corretto e andranno perse se +// il codice viene rigenerato. +// +//------------------------------------------------------------------------------ + +namespace Dione.Properties +{ + + + /// + /// Classe di risorse fortemente tipizzata per la ricerca di stringhe localizzate e così via. + /// + // Questa classe è stata generata automaticamente dalla classe StronglyTypedResourceBuilder + // tramite uno strumento quale ResGen o Visual Studio. + // Per aggiungere o rimuovere un membro, modificare il file .ResX, quindi eseguire di nuovo ResGen + // con l'opzione /str oppure ricompilare il progetto VS. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Restituisce l'istanza di ResourceManager memorizzata nella cache e usata da questa classe. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Dione.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Esegue l'override della proprietà CurrentUICulture del thread corrente per tutte + /// le ricerche di risorse che utilizzano questa classe di risorse fortemente tipizzata. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/Dione/Properties/Resources.resx b/Dione/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/Dione/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Dione/Properties/Settings.Designer.cs b/Dione/Properties/Settings.Designer.cs new file mode 100644 index 0000000..830de68 --- /dev/null +++ b/Dione/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Dione.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/Dione/Properties/Settings.settings b/Dione/Properties/Settings.settings new file mode 100644 index 0000000..033d7a5 --- /dev/null +++ b/Dione/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Dione/ViewModels/DashboardViewModel.cs b/Dione/ViewModels/DashboardViewModel.cs new file mode 100644 index 0000000..ba5f8cc --- /dev/null +++ b/Dione/ViewModels/DashboardViewModel.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using LiveCharts; +using LiveCharts.Wpf; +using Dione.Data; + +namespace Dione.ViewModels +{ + public class DashboardViewModel : ObservableObject + { + private long _totalTokensSpent; + public long TotalTokensSpent + { + get => _totalTokensSpent; + set => SetProperty(ref _totalTokensSpent, value); + } + + private int _validRecords; + public int ValidRecords + { + get => _validRecords; + set => SetProperty(ref _validRecords, value); + } + + private double _estimatedCostEur; + public double EstimatedCostEur + { + get => _estimatedCostEur; + set => SetProperty(ref _estimatedCostEur, value); + } + + private double _qualityScore; + public double QualityScore + { + get => _qualityScore; + set => SetProperty(ref _qualityScore, value); + } + + private SeriesCollection _tokensPerMinuteSeries; + public SeriesCollection TokensPerMinuteSeries + { + get => _tokensPerMinuteSeries; + set => SetProperty(ref _tokensPerMinuteSeries, value); + } + + private string[] _tokensPerMinuteLabels; + public string[] TokensPerMinuteLabels + { + get => _tokensPerMinuteLabels; + set => SetProperty(ref _tokensPerMinuteLabels, value); + } + + private SeriesCollection _successErrorSeries; + public SeriesCollection SuccessErrorSeries + { + get => _successErrorSeries; + set => SetProperty(ref _successErrorSeries, value); + } + + private SeriesCollection _latencySeries; + public SeriesCollection LatencySeries + { + get => _latencySeries; + set => SetProperty(ref _latencySeries, value); + } + + private string[] _latencyLabels; + public string[] LatencyLabels + { + get => _latencyLabels; + set => SetProperty(ref _latencyLabels, value); + } + + public RelayCommand RefreshCommand { get; } + + public DashboardViewModel() + { + RefreshCommand = new RelayCommand(RefreshFromDb); + + // Initialize with empty series + TokensPerMinuteSeries = new SeriesCollection + { + new LineSeries { Title = "Tokens/min", Values = new ChartValues() } + }; + TokensPerMinuteLabels = Array.Empty(); + + SuccessErrorSeries = new SeriesCollection + { + new PieSeries { Title = "Success", Values = new ChartValues { 0 }, DataLabels = true }, + new PieSeries { Title = "API Error", Values = new ChartValues { 0 }, DataLabels = true }, + new PieSeries { Title = "Parse Fail", Values = new ChartValues { 0 }, DataLabels = true } + }; + + LatencySeries = new SeriesCollection + { + new ColumnSeries { Title = "Latency (ms)", Values = new ChartValues() } + }; + LatencyLabels = Array.Empty(); + + RefreshFromDb(); + } + + public void RefreshFromDb() + { + try + { + var logs = SynthDataDbContext.QueryAll(); + + if (logs.Count == 0) + { + TotalTokensSpent = 0; + ValidRecords = 0; + EstimatedCostEur = 0; + QualityScore = 0; + return; + } + + // --- KPI Cards --- + long totalPrompt = logs.Sum(l => l.TokensPrompt); + long totalCompletion = logs.Sum(l => l.TokensCompletion); + TotalTokensSpent = totalPrompt + totalCompletion; + + int successCount = logs.Count(l => l.IsSuccess); + ValidRecords = successCount; + + // Estimate electric cost: ~0.3 kWh GPU idle, rough 0.0003 EUR/token + double totalSeconds = logs.Sum(l => l.ExecutionTimeMs) / 1000.0; + double kWh = (totalSeconds / 3600.0) * 0.3; + EstimatedCostEur = Math.Round(kWh * 0.25, 4); + + int totalCount = logs.Count; + QualityScore = totalCount > 0 + ? Math.Round(100.0 * successCount / totalCount, 1) + : 0; + + // --- Tokens per minute (group by minute) --- + var byMinute = logs + .Where(l => l.Timestamp != default) + .GroupBy(l => new DateTime(l.Timestamp.Year, l.Timestamp.Month, l.Timestamp.Day, + l.Timestamp.Hour, l.Timestamp.Minute, 0)) + .OrderBy(g => g.Key) + .ToList(); + + var tpmValues = new ChartValues(); + var tpmLabels = new List(); + foreach (var g in byMinute) + { + tpmValues.Add(g.Sum(x => x.TokensPrompt + x.TokensCompletion)); + tpmLabels.Add(g.Key.ToString("HH:mm")); + } + + TokensPerMinuteSeries = new SeriesCollection + { + new LineSeries + { + Title = "Tokens/min", + Values = tpmValues, + PointGeometry = null + } + }; + TokensPerMinuteLabels = tpmLabels.ToArray(); + + // --- Success vs Error pie --- + int apiErrors = logs.Count(l => !l.IsSuccess && !string.IsNullOrEmpty(l.ErrorMessage) + && l.ErrorMessage.IndexOf("parse", StringComparison.OrdinalIgnoreCase) < 0); + int parseErrors = logs.Count(l => !l.IsSuccess && !string.IsNullOrEmpty(l.ErrorMessage) + && l.ErrorMessage.IndexOf("parse", StringComparison.OrdinalIgnoreCase) >= 0); + int otherErrors = totalCount - successCount - apiErrors - parseErrors; + apiErrors += otherErrors; + + SuccessErrorSeries = new SeriesCollection + { + new PieSeries { Title = "Success", Values = new ChartValues { successCount }, DataLabels = true }, + new PieSeries { Title = "API Error", Values = new ChartValues { apiErrors }, DataLabels = true }, + new PieSeries { Title = "Parse Fail", Values = new ChartValues { parseErrors }, DataLabels = true } + }; + + // --- Latency bar chart (last 20 requests) --- + var recent = logs.OrderByDescending(l => l.Timestamp).Take(20).Reverse().ToList(); + var latValues = new ChartValues(recent.Select(l => (double)l.ExecutionTimeMs)); + var latLabels = recent.Select((l, i) => $"#{i + 1}").ToArray(); + + LatencySeries = new SeriesCollection + { + new ColumnSeries { Title = "Latency (ms)", Values = latValues } + }; + LatencyLabels = latLabels; + } + catch + { + // DB not yet initialized or empty - leave zeros + } + } + } +} diff --git a/Dione/ViewModels/LiveGenerationViewModel.cs b/Dione/ViewModels/LiveGenerationViewModel.cs new file mode 100644 index 0000000..6e16a6a --- /dev/null +++ b/Dione/ViewModels/LiveGenerationViewModel.cs @@ -0,0 +1,586 @@ +using System; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Threading; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Dione.Data; +using Dione.Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Dione.ViewModels +{ + public class LogEntry + { + public string Level { get; set; } + public string Message { get; set; } + public DateTime Timestamp { get; set; } + public string TimeLabel => Timestamp.ToString("HH:mm:ss"); + } + + public class LiveGenerationViewModel : ObservableObject + { + // HttpClient senza timeout fisso: lo gestiamo per-request con CancellationToken + private static readonly HttpClient Http = new HttpClient { Timeout = Timeout.InfiniteTimeSpan }; + + // ── Observable properties ───────────────────────────────────────────── + + private bool _isRunning; + public bool IsRunning { get => _isRunning; set { SetProperty(ref _isRunning, value); StartCommand.NotifyCanExecuteChanged(); StopCommand.NotifyCanExecuteChanged(); } } + + private string _statusText = "Pronto"; + public string StatusText { get => _statusText; set => SetProperty(ref _statusText, value); } + + private string _elapsedTime = "00:00:00"; + public string ElapsedTime { get => _elapsedTime; set => SetProperty(ref _elapsedTime, value); } + + private int _completedBatches; + public int CompletedBatches { get => _completedBatches; set => SetProperty(ref _completedBatches, value); } + + private int _successCount; + public int SuccessCount { get => _successCount; set => SetProperty(ref _successCount, value); } + + private int _errorCount; + public int ErrorCount { get => _errorCount; set => SetProperty(ref _errorCount, value); } + + private long _totalRecordsWritten; + public long TotalRecordsWritten { get => _totalRecordsWritten; set => SetProperty(ref _totalRecordsWritten, value); } + + private int _filesCreated; + public int FilesCreated { get => _filesCreated; set => SetProperty(ref _filesCreated, value); } + + private string _currentFileName = ""; + public string CurrentFileName { get => _currentFileName; set => SetProperty(ref _currentFileName, value); } + + private long _currentFileSizeBytes; + public long CurrentFileSizeBytes { get => _currentFileSizeBytes; set => SetProperty(ref _currentFileSizeBytes, value); } + + private double _currentFileSizeMb; + public double CurrentFileSizeMb { get => _currentFileSizeMb; set => SetProperty(ref _currentFileSizeMb, value); } + + private string _lastPreview = ""; + public string LastPreview { get => _lastPreview; set => SetProperty(ref _lastPreview, value); } + + private double _totalCostElectricity; + public double TotalCostElectricity { get => _totalCostElectricity; set => SetProperty(ref _totalCostElectricity, value); } + + private double _totalCostApi; + public double TotalCostApi { get => _totalCostApi; set => SetProperty(ref _totalCostApi, value); } + + private double _totalRevenue; + public double TotalRevenue { get => _totalRevenue; set => SetProperty(ref _totalRevenue, value); } + + private double _netProfit; + public double NetProfit { get => _netProfit; set => SetProperty(ref _netProfit, value); } + + private double _avgBatchTimeMs; + public double AvgBatchTimeMs { get => _avgBatchTimeMs; set => SetProperty(ref _avgBatchTimeMs, value); } + + private int _tokensTotal; + public int TokensTotal { get => _tokensTotal; set => SetProperty(ref _tokensTotal, value); } + + // Usato come Maximum dalla ProgressBar del file corrente + private double _maxFileSizeMbDouble = 250; + public double MaxFileSizeMbDouble { get => _maxFileSizeMbDouble; set => SetProperty(ref _maxFileSizeMbDouble, value); } + + // ── Streaming ───────────────────────────────────────────────────────── + private string _streamingText = ""; + public string StreamingText { get => _streamingText; set => SetProperty(ref _streamingText, value); } + + private bool _isStreaming; + public bool IsStreaming { get => _isStreaming; set => SetProperty(ref _isStreaming, value); } + + private int _streamingChars; + public int StreamingChars { get => _streamingChars; set => SetProperty(ref _streamingChars, value); } + + private int _streamingTokensLive; + public int StreamingTokensLive { get => _streamingTokensLive; set => SetProperty(ref _streamingTokensLive, value); } + + public ObservableCollection LogEntries { get; } = new ObservableCollection(); + + // ── Commands ────────────────────────────────────────────────────────── + + public RelayCommand StartCommand { get; } + public RelayCommand StopCommand { get; } + public RelayCommand ClearLogCommand { get; } + public RelayCommand CopyLogsCommand { get; } + + // ── Private fields ──────────────────────────────────────────────────── + + private CancellationTokenSource _cts; + private Stopwatch _sessionSw; + private DispatcherTimer _elapsedTimer; + + // ── Constructor ─────────────────────────────────────────────────────── + + public LiveGenerationViewModel() + { + StartCommand = new RelayCommand(StartGeneration, () => !IsRunning); + StopCommand = new RelayCommand(StopGeneration, () => IsRunning); + ClearLogCommand = new RelayCommand(() => LogEntries.Clear()); + CopyLogsCommand = new RelayCommand(CopyLogs, () => LogEntries.Count > 0); + + _elapsedTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; + _elapsedTimer.Tick += (_, __) => + { + if (_sessionSw == null) return; + var e = _sessionSw.Elapsed; + ElapsedTime = $"{(int)e.TotalHours:D2}:{e.Minutes:D2}:{e.Seconds:D2}"; + }; + } + + // ── Generation ──────────────────────────────────────────────────────── + + private async void StartGeneration() + { + var settings = SynthDataDbContext.LoadSettings(); + + if (string.IsNullOrWhiteSpace(settings.ApiEndpoint)) { Log("ERR", "API Endpoint non configurato."); return; } + if (string.IsNullOrWhiteSpace(settings.SystemPrompt)) { Log("ERR", "System Prompt vuoto."); return; } + if (string.IsNullOrWhiteSpace(settings.UserPrompt)) { Log("ERR", "User Prompt vuoto."); return; } + if (string.IsNullOrWhiteSpace(settings.OutputDirectory)) + { + Log("ERR", "Cartella output non configurata. Vai nelle Impostazioni."); + return; + } + if (!Directory.Exists(settings.OutputDirectory)) + { + try { Directory.CreateDirectory(settings.OutputDirectory); } + catch { Log("ERR", "Impossibile creare la cartella output."); return; } + } + + // Reset counters + IsRunning = true; + CompletedBatches = 0; + SuccessCount = 0; + ErrorCount = 0; + TotalRecordsWritten = 0; + FilesCreated = 0; + CurrentFileSizeBytes = 0; + CurrentFileSizeMb = 0; + TotalCostElectricity = 0; + TotalCostApi = 0; + TotalRevenue = 0; + NetProfit = 0; + AvgBatchTimeMs = 0; + TokensTotal = 0; + LastPreview = ""; + CurrentFileName = ""; + ElapsedTime = "00:00:00"; + _sessionSw = Stopwatch.StartNew(); + _cts = new CancellationTokenSource(); + var token = _cts.Token; + _elapsedTimer.Start(); + + // Derived settings + var endpoint = settings.ApiEndpoint.Trim(); + var apiKey = settings.ApiKey ?? ""; + bool isLocal = IsLocalEndpoint(endpoint); + long maxBytes = (long)settings.MaxFileSizeMb * 1024 * 1024; + int calcTimeout = (int)(settings.ApiTimeoutSeconds + settings.MaxTokens * settings.TimeoutPerTokenRatio / 100.0); + MaxFileSizeMbDouble = settings.MaxFileSizeMb; + + Log("INFO", $"Endpoint: {endpoint} | Model: {settings.ModelName}"); + Log("INFO", $"Output: {settings.OutputDirectory} | Max file: {settings.MaxFileSizeMb} MB"); + Log("INFO", $"Timeout: {calcTimeout}s | Max tokens: {settings.MaxTokens}"); + if (!string.IsNullOrWhiteSpace(apiKey) && !isLocal) + Log("INFO", "Autenticazione API: attiva"); + else if (!string.IsNullOrWhiteSpace(apiKey) && isLocal) + Log("WARN", "API Key ignorata (endpoint locale)"); + + try + { + await Task.Run(async () => + { + // ── File state ── + string filePath = null; + int fileIndex = NextFileIndex(settings.OutputDirectory, settings.OutputFilePrefix); + + var batchSw = new Stopwatch(); + long totalBatchMs = 0; + + while (!token.IsCancellationRequested) + { + // ── Open new file if needed ── + if (filePath == null || !File.Exists(filePath) || new FileInfo(filePath).Length >= maxBytes) + { + filePath = Path.Combine(settings.OutputDirectory, $"{settings.OutputFilePrefix}_{fileIndex:D3}.jsonl"); + fileIndex++; + UpdateUI(() => { FilesCreated++; CurrentFileName = Path.GetFileName(filePath); CurrentFileSizeBytes = 0; CurrentFileSizeMb = 0; }); + Log("INFO", $"[NUOVO FILE] {Path.GetFileName(filePath)}"); + } + + // ── Build request (streaming) ── + var requestBody = new + { + model = settings.ModelName ?? "", + messages = new[] + { + new { role = "system", content = settings.SystemPrompt }, + new { role = "user", content = settings.UserPrompt } + }, + temperature = settings.Temperature, + max_tokens = settings.MaxTokens, + stream = true, + }; + + var json = JsonConvert.SerializeObject(requestBody); + var telelog = new TelemetryLog + { + Timestamp = DateTime.UtcNow, + BatchId = Guid.NewGuid().ToString("N").Substring(0, 12), + ModelUsed = settings.ModelName, + }; + + // Reset streaming UI state + UpdateUI(() => { StreamingText = ""; StreamingChars = 0; StreamingTokensLive = 0; IsStreaming = false; }); + + batchSw.Restart(); + HttpResponseMessage response = null; + int retries = 0; + + while (retries < 3 && response == null) + { + try + { + using (var content = new StringContent(json, Encoding.UTF8, "application/json")) + using (var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(calcTimeout))) + using (var linked = CancellationTokenSource.CreateLinkedTokenSource(token, timeoutCts.Token)) + { + var req = new HttpRequestMessage(HttpMethod.Post, endpoint) { Content = content }; + if (!string.IsNullOrWhiteSpace(apiKey) && !isLocal) + req.Headers.Add("Authorization", $"Bearer {apiKey}"); + + // ResponseHeadersRead: leggiamo il body in streaming + response = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, linked.Token); + } + } + catch (OperationCanceledException) when (!token.IsCancellationRequested) + { + retries++; + Log("WARN", $"Timeout, tentativo {retries}/3"); + if (retries < 3) await DelayAsync(2000, token); + } + catch (Exception ex) when (!token.IsCancellationRequested) + { + retries++; + var msg = ex.Message + (ex.InnerException != null ? " | " + ex.InnerException.Message : ""); + Log("ERR", $"Errore connessione: {msg}"); + if (retries < 3) await DelayAsync(2000, token); + } + } + + batchSw.Stop(); + + if (token.IsCancellationRequested) break; + + if (response == null) + { + telelog.IsSuccess = false; + telelog.ErrorMessage = "Tutti i retry esauriti."; + SynthDataDbContext.InsertLog(telelog); + UpdateUI(() => ErrorCount++); + await DelayAsync(3000, token); + continue; + } + + telelog.ExecutionTimeMs = batchSw.ElapsedMilliseconds; + + if (!response.IsSuccessStatusCode) + { + string errBody = ""; + try { errBody = await response.Content.ReadAsStringAsync(); } catch { } + Log("ERR", $"HTTP {(int)response.StatusCode} {response.ReasonPhrase}"); + if (!string.IsNullOrWhiteSpace(errBody)) + Log("ERR", $"Body: {errBody.Substring(0, Math.Min(500, errBody.Length))}"); + var err = $"HTTP {(int)response.StatusCode}: {errBody?.Substring(0, Math.Min(200, errBody?.Length ?? 0))}"; + telelog.IsSuccess = false; + telelog.ErrorMessage = err; + SynthDataDbContext.InsertLog(telelog); + UpdateUI(() => ErrorCount++); + await DelayAsync(2000, token); + continue; + } + + // ── Leggi lo stream SSE ── + string generatedText = null; + int tokensPrompt = 0; + int tokensComp = 0; + + try + { + var accumulated = new StringBuilder(); + UpdateUI(() => IsStreaming = true); + + using (var httpStream = await response.Content.ReadAsStreamAsync()) + using (var reader = new StreamReader(httpStream)) + { + string line; + while ((line = await reader.ReadLineAsync()) != null) + { + if (token.IsCancellationRequested) break; + if (string.IsNullOrEmpty(line)) continue; + if (!line.StartsWith("data:")) continue; + + var payload = line.Substring(5).TrimStart(); + if (payload == "[DONE]") break; + + try + { + var chunk = JObject.Parse(payload); + + // Aggiorna usage se presente nel chunk (alcuni provider lo inviano nell'ultimo chunk) + var usageNode = chunk["usage"]; + if (usageNode != null) + { + tokensPrompt = usageNode["prompt_tokens"]?.Value() ?? tokensPrompt; + tokensComp = usageNode["completion_tokens"]?.Value() ?? tokensComp; + } + + var delta = chunk["choices"]?[0]?["delta"]?["content"]?.Value(); + if (!string.IsNullOrEmpty(delta)) + { + accumulated.Append(delta); + tokensComp++; // stima locale se il provider non invia usage inline + var snapshot = accumulated.ToString(); + UpdateUI(() => + { + StreamingText = snapshot; + StreamingChars = snapshot.Length; + StreamingTokensLive = tokensComp; + }); + } + } + catch (Exception chunkEx) + { + Log("WARN", $"Chunk SSE non parsabile: {chunkEx.Message}"); + Log("WARN", $"Payload: {payload.Substring(0, Math.Min(200, payload.Length))}"); + } + } + } + + generatedText = accumulated.ToString(); + UpdateUI(() => { IsStreaming = false; LastPreview = generatedText.Substring(0, Math.Min(400, generatedText.Length)); }); + } + catch (Exception ex) when (!token.IsCancellationRequested) + { + UpdateUI(() => { IsStreaming = false; }); + Log("ERR", "Lettura stream fallita: " + ex.Message); + UpdateUI(() => ErrorCount++); + continue; + } + + if (string.IsNullOrWhiteSpace(generatedText)) + { + Log("WARN", "Stream vuoto: nessun testo ricevuto dall'API."); + if (!string.IsNullOrEmpty(generatedText)) + Log("WARN", $"Contenuto parziale: {generatedText.Substring(0, Math.Min(300, generatedText.Length))}"); + UpdateUI(() => ErrorCount++); + continue; + } + + telelog.TokensPrompt = tokensPrompt; + telelog.TokensCompletion = tokensComp; + + // ── Rimuovi blocchi e log se presenti ── + var strippedText = StripThinkingBlocks(generatedText); + if (strippedText.Length != generatedText.Length) + Log("INFO", $"Rimossi blocchi : {generatedText.Length - strippedText.Length} caratteri di ragionamento ignorati."); + generatedText = strippedText; + + // ── Extract JSON array from text (handles markdown code blocks) ── + var jsonArray = ExtractJsonArray(generatedText); + if (jsonArray == null) + { + Log("WARN", "Nessun array JSON trovato nella risposta."); + Log("WARN", $"Risposta grezza ({generatedText.Length} car): {generatedText.Substring(0, Math.Min(500, generatedText.Length))}"); + if (generatedText.Length > 500) + Log("WARN", $"...fine risposta: {generatedText.Substring(generatedText.Length - Math.Min(200, generatedText.Length))}"); + UpdateUI(() => ErrorCount++); + continue; + } + + // ── Write JSONL ── + int recordsInBatch = 0; + var sb = new StringBuilder(); + foreach (var obj in jsonArray) + { + sb.AppendLine(obj.ToString(Formatting.None)); + recordsInBatch++; + } + + var jsonlChunk = sb.ToString(); + File.AppendAllText(filePath, jsonlChunk, Encoding.UTF8); + + // ── Cost calculation ── + double elecCost = (batchSw.Elapsed.TotalHours * settings.SystemPowerWatt / 1000.0) * settings.ElectricityCostPerKwh; + double apiCost = 0; + if (settings.ApiCostType == "PerCall") apiCost = settings.ApiCostPerCall; + if (settings.ApiCostType == "PerBlock") apiCost = (1.0 / Math.Max(1, settings.ApiBlockSize)) * settings.ApiCostPerBlock; + + // ── Update state ── + totalBatchMs += batchSw.ElapsedMilliseconds; + telelog.IsSuccess = true; + telelog.OutputPreview = jsonlChunk.Substring(0, Math.Min(300, jsonlChunk.Length)); + SynthDataDbContext.InsertLog(telelog); + + long newSize = new FileInfo(filePath).Length; + + UpdateUI(() => + { + CompletedBatches++; + SuccessCount++; + TotalRecordsWritten += recordsInBatch; + TokensTotal += tokensPrompt + tokensComp; + TotalCostElectricity += elecCost; + TotalCostApi += apiCost; + NetProfit = TotalRevenue - TotalCostApi - TotalCostElectricity; + CurrentFileSizeBytes = newSize; + CurrentFileSizeMb = newSize / (1024.0 * 1024.0); + AvgBatchTimeMs = CompletedBatches > 0 ? (double)totalBatchMs / CompletedBatches : 0; + LastPreview = jsonlChunk.Substring(0, Math.Min(400, jsonlChunk.Length)); + }); + + Log("OK", $"Batch #{CompletedBatches}: {recordsInBatch} record | {tokensPrompt + tokensComp} tok | {batchSw.ElapsedMilliseconds}ms"); + } + + Log("INFO", $"Generazione fermata. Batch: {CompletedBatches}, Record: {TotalRecordsWritten}, Errori: {ErrorCount}"); + UpdateUI(() => { IsRunning = false; StatusText = "Fermato"; _elapsedTimer.Stop(); }); + + }, CancellationToken.None); + } + catch (OperationCanceledException) + { + _elapsedTimer.Stop(); + Log("INFO", $"Generazione interrotta. Batch: {CompletedBatches}, Record: {TotalRecordsWritten}"); + UpdateUI(() => { IsRunning = false; StatusText = "Fermato"; }); + } + catch (Exception ex) + { + _elapsedTimer.Stop(); + Log("ERR", $"Errore imprevisto: {ex.Message}"); + UpdateUI(() => { IsRunning = false; StatusText = "Errore"; }); + } + } + + private void StopGeneration() + { + _cts?.Cancel(); + StatusText = "Arresto in corso..."; + Log("WARN", "Stop richiesto..."); + } + + private void CopyLogs() + { + var sb = new StringBuilder(); + for (int i = LogEntries.Count - 1; i >= 0; i--) + { + var e = LogEntries[i]; + sb.AppendLine($"{e.TimeLabel} [{e.Level,-4}] {e.Message}"); + } + System.Windows.Clipboard.SetText(sb.ToString()); + } + + // ── Helpers ─────────────────────────────────────────────────────────── + + private static bool IsLocalEndpoint(string url) + => url.Contains("localhost") || url.Contains("127.0.0.1") + || url.Contains("172.") || url.Contains("192.168.") || url.Contains("10."); + + /// Attende ms millisecondi; non lancia eccezione se il token viene cancellato. + private static async Task DelayAsync(int ms, CancellationToken token) + { + try { await Task.Delay(ms, token); } + catch (OperationCanceledException) { /* attesa interrotta: normale al Stop */ } + } + + /// + /// Estrae il primo array JSON dalla risposta (gestisce anche ```json ... ``` markdown). + /// + private static string StripThinkingBlocks(string text) + { + if (string.IsNullOrWhiteSpace(text)) return text; + // Tag di ragionamento usati da vari modelli + var tagPairs = new[] { ("", ""), ("", "") }; + foreach (var (open, close) in tagPairs) + { + var sb = new StringBuilder(); + int i = 0; + while (i < text.Length) + { + int openIdx = text.IndexOf(open, i, StringComparison.OrdinalIgnoreCase); + if (openIdx < 0) { sb.Append(text, i, text.Length - i); break; } + sb.Append(text, i, openIdx - i); + int closeIdx = text.IndexOf(close, openIdx, StringComparison.OrdinalIgnoreCase); + i = closeIdx < 0 ? text.Length : closeIdx + close.Length; + } + text = sb.ToString(); + } + return text; + } + + private static JArray ExtractJsonArray(string text) + { + if (string.IsNullOrWhiteSpace(text)) return null; + + // Rimuovi blocchi di ragionamento ... + text = StripThinkingBlocks(text); + + // Strip markdown code fences + var cleaned = text.Trim(); + if (cleaned.StartsWith("```")) + { + var firstNewline = cleaned.IndexOf('\n'); + var lastFence = cleaned.LastIndexOf("```"); + if (firstNewline > 0 && lastFence > firstNewline) + cleaned = cleaned.Substring(firstNewline + 1, lastFence - firstNewline - 1).Trim(); + } + + // Find first [ ... ] + var start = cleaned.IndexOf('['); + var end = cleaned.LastIndexOf(']'); + if (start < 0 || end <= start) return null; + + try { return JArray.Parse(cleaned.Substring(start, end - start + 1)); } + catch { return null; } + } + + private static int NextFileIndex(string dir, string prefix) + { + int max = 0; + if (!Directory.Exists(dir)) return 1; + foreach (var f in Directory.GetFiles(dir, $"{prefix}_*.jsonl")) + { + var name = Path.GetFileNameWithoutExtension(f); + var part = name.Replace(prefix + "_", ""); + if (int.TryParse(part, out var n) && n > max) max = n; + } + return max + 1; + } + + private void Log(string level, string msg) + { + UpdateUI(() => + { + LogEntries.Insert(0, new LogEntry { Level = level, Message = msg, Timestamp = DateTime.Now }); + if (LogEntries.Count > 500) LogEntries.RemoveAt(LogEntries.Count - 1); + CopyLogsCommand?.NotifyCanExecuteChanged(); + }); + } + + private void UpdateUI(Action action) + { + if (System.Windows.Application.Current?.Dispatcher == null) { action(); return; } + if (System.Windows.Application.Current.Dispatcher.CheckAccess()) + action(); + else + System.Windows.Application.Current.Dispatcher.Invoke(action); + } + } +} \ No newline at end of file diff --git a/Dione/ViewModels/MainWindowViewModel.cs b/Dione/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..3c17002 --- /dev/null +++ b/Dione/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,79 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace Dione.ViewModels +{ + public class MainWindowViewModel : ObservableObject + { + private object _currentView; + public object CurrentView + { + get => _currentView; + set => SetProperty(ref _currentView, value); + } + + private string _selectedMenu = "Dashboard"; + public string SelectedMenu + { + get => _selectedMenu; + set + { + if (SetProperty(ref _selectedMenu, value)) + NavigateTo(value); + } + } + + public DashboardViewModel DashboardVm { get; } + public SettingsViewModel SettingsVm { get; } + public LiveGenerationViewModel LiveGenerationVm { get; } + public TelemetryHistoryViewModel TelemetryHistoryVm { get; } + + public RelayCommand MinimizeCommand { get; } + public RelayCommand MaximizeCommand { get; } + public RelayCommand CloseCommand { get; } + public RelayCommand NavigateCommand { get; } + + public MainWindowViewModel() + { + DashboardVm = new DashboardViewModel(); + SettingsVm = new SettingsViewModel(); + LiveGenerationVm = new LiveGenerationViewModel(); + TelemetryHistoryVm = new TelemetryHistoryViewModel(); + + CurrentView = DashboardVm; + + MinimizeCommand = new RelayCommand(() => + System.Windows.Application.Current.MainWindow.WindowState = System.Windows.WindowState.Minimized); + MaximizeCommand = new RelayCommand(() => + { + var w = System.Windows.Application.Current.MainWindow; + w.WindowState = w.WindowState == System.Windows.WindowState.Maximized + ? System.Windows.WindowState.Normal + : System.Windows.WindowState.Maximized; + }); + CloseCommand = new RelayCommand(() => System.Windows.Application.Current.Shutdown()); + NavigateCommand = new RelayCommand(NavigateTo); + } + + private void NavigateTo(string view) + { + switch (view) + { + case "Dashboard": + DashboardVm.RefreshFromDb(); + CurrentView = DashboardVm; + break; + case "LiveGeneration": + CurrentView = LiveGenerationVm; + break; + case "Settings": + CurrentView = SettingsVm; + break; + case "Telemetry": + CurrentView = TelemetryHistoryVm; + break; + } + SelectedMenu = view; + } + } +} diff --git a/Dione/ViewModels/SettingsViewModel.cs b/Dione/ViewModels/SettingsViewModel.cs new file mode 100644 index 0000000..8736962 --- /dev/null +++ b/Dione/ViewModels/SettingsViewModel.cs @@ -0,0 +1,303 @@ +using System; +using System.Windows; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Dione.Data; +using Dione.Models; +using Microsoft.Win32; + +namespace Dione.ViewModels +{ + public class SettingsViewModel : ObservableObject + { + // ── API ────────────────────────────────────────────────────────────────── + + private string _selectedEndpointPreset = "Custom"; + public string SelectedEndpointPreset + { + get => _selectedEndpointPreset; + set { if (SetProperty(ref _selectedEndpointPreset, value) && value != "Custom") ApplyPreset(value); } + } + public string[] EndpointPresets { get; } = { "Custom", "OpenAI", "Anthropic", "Google AI", "Azure OpenAI", "LM Studio (Local)", "Ollama (Local)" }; + + private string _apiEndpoint = "http://127.0.0.1:1234/v1/chat/completions"; + public string ApiEndpoint { get => _apiEndpoint; set => SetProperty(ref _apiEndpoint, value); } + + private string _modelName = ""; + public string ModelName { get => _modelName; set => SetProperty(ref _modelName, value); } + + private string _apiKey = ""; + public string ApiKey { get => _apiKey; set => SetProperty(ref _apiKey, value); } + + private double _temperature = 0.7; + public double Temperature { get => _temperature; set => SetProperty(ref _temperature, value); } + + private int _maxTokens = 2048; + public int MaxTokens { get => _maxTokens; set => SetProperty(ref _maxTokens, value); } + + // ── Prompt ─────────────────────────────────────────────────────────────── + + private string _systemPrompt = ""; + public string SystemPrompt { get => _systemPrompt; set => SetProperty(ref _systemPrompt, value); } + + private string _userPrompt = ""; + public string UserPrompt { get => _userPrompt; set => SetProperty(ref _userPrompt, value); } + + // ── Output ─────────────────────────────────────────────────────────────── + + private string _outputDirectory = ""; + public string OutputDirectory { get => _outputDirectory; set => SetProperty(ref _outputDirectory, value); } + + private string _outputFilePrefix = "batch"; + public string OutputFilePrefix { get => _outputFilePrefix; set => SetProperty(ref _outputFilePrefix, value); } + + private int _maxFileSizeMb = 250; + public int MaxFileSizeMb { get => _maxFileSizeMb; set => SetProperty(ref _maxFileSizeMb, value); } + + // ── Timeout ────────────────────────────────────────────────────────────── + + private int _apiTimeoutSeconds = 120; + public int ApiTimeoutSeconds { get => _apiTimeoutSeconds; set => SetProperty(ref _apiTimeoutSeconds, value); } + + private double _timeoutPerTokenRatio = 0.5; + public double TimeoutPerTokenRatio { get => _timeoutPerTokenRatio; set => SetProperty(ref _timeoutPerTokenRatio, value); } + + // ── Verifica qualita ───────────────────────────────────────────────────── + + private bool _enableQualityVerification = false; + public bool EnableQualityVerification { get => _enableQualityVerification; set => SetProperty(ref _enableQualityVerification, value); } + + private bool _useSameModelForVerification = true; + public bool UseSameModelForVerification { get => _useSameModelForVerification; set => SetProperty(ref _useSameModelForVerification, value); } + + private string _verificationApiEndpoint = ""; + public string VerificationApiEndpoint { get => _verificationApiEndpoint; set => SetProperty(ref _verificationApiEndpoint, value); } + + private string _verificationModelName = ""; + public string VerificationModelName { get => _verificationModelName; set => SetProperty(ref _verificationModelName, value); } + + private string _verificationApiKey = ""; + public string VerificationApiKey { get => _verificationApiKey; set => SetProperty(ref _verificationApiKey, value); } + + private double _revenuePerHighQualityRecord = 0.005; + public double RevenuePerHighQualityRecord { get => _revenuePerHighQualityRecord; set => SetProperty(ref _revenuePerHighQualityRecord, value); } + + // ── Costi ──────────────────────────────────────────────────────────────── + + private double _electricityCostPerKwh = 0.25; + public double ElectricityCostPerKwh { get => _electricityCostPerKwh; set => SetProperty(ref _electricityCostPerKwh, value); } + + private double _systemPowerWatt = 350; + public double SystemPowerWatt { get => _systemPowerWatt; set => SetProperty(ref _systemPowerWatt, value); } + + private string _apiCostType = "Free"; + public string ApiCostType { get => _apiCostType; set => SetProperty(ref _apiCostType, value); } + public string[] ApiCostTypes { get; } = { "Free", "PerCall", "PerBlock" }; + + private double _apiCostPerCall = 0; + public double ApiCostPerCall { get => _apiCostPerCall; set => SetProperty(ref _apiCostPerCall, value); } + + private double _apiCostPerBlock = 0; + public double ApiCostPerBlock { get => _apiCostPerBlock; set => SetProperty(ref _apiCostPerBlock, value); } + + private int _apiBlockSize = 1000; + public int ApiBlockSize { get => _apiBlockSize; set => SetProperty(ref _apiBlockSize, value); } + + // ── Status ─────────────────────────────────────────────────────────────── + + private string _statusMessage = ""; + public string StatusMessage { get => _statusMessage; set => SetProperty(ref _statusMessage, value); } + + // ── Commands ───────────────────────────────────────────────────────────── + + public RelayCommand SaveCommand { get; } + public RelayCommand BrowseOutputDirectoryCommand { get; } + public RelayCommand ResetTelemetryCommand { get; } + public RelayCommand ResetDatabaseCommand { get; } + public RelayCommand InsertDefaultBettingPromptCommand { get; } + + // ── Constructor ────────────────────────────────────────────────────────── + + public SettingsViewModel() + { + SaveCommand = new RelayCommand(Save); + BrowseOutputDirectoryCommand = new RelayCommand(BrowseOutputDirectory); + ResetTelemetryCommand = new RelayCommand(ResetTelemetry); + ResetDatabaseCommand = new RelayCommand(ResetDatabase); + InsertDefaultBettingPromptCommand = new RelayCommand(InsertDefaultBettingPrompt); + + Load(); + } + + // ── Private methods ────────────────────────────────────────────────────── + + public void Load() + { + try + { + var s = SynthDataDbContext.LoadSettings(); + ApiEndpoint = s.ApiEndpoint; + ModelName = s.ModelName; + ApiKey = s.ApiKey; + Temperature = s.Temperature; + MaxTokens = s.MaxTokens; + SystemPrompt = s.SystemPrompt; + UserPrompt = s.UserPrompt; + OutputDirectory = s.OutputDirectory; + OutputFilePrefix = s.OutputFilePrefix; + MaxFileSizeMb = s.MaxFileSizeMb; + ApiTimeoutSeconds = s.ApiTimeoutSeconds; + TimeoutPerTokenRatio = s.TimeoutPerTokenRatio; + EnableQualityVerification = s.EnableQualityVerification; + UseSameModelForVerification = s.UseSameModelForVerification; + VerificationApiEndpoint = s.VerificationApiEndpoint; + VerificationModelName = s.VerificationModelName; + VerificationApiKey = s.VerificationApiKey; + RevenuePerHighQualityRecord = s.RevenuePerHighQualityRecord; + ElectricityCostPerKwh = s.ElectricityCostPerKwh; + SystemPowerWatt = s.SystemPowerWatt; + ApiCostType = s.ApiCostType; + ApiCostPerCall = s.ApiCostPerCall; + ApiCostPerBlock = s.ApiCostPerBlock; + ApiBlockSize = s.ApiBlockSize; + + StatusMessage = "Impostazioni caricate."; + } + catch (Exception ex) + { + StatusMessage = "Errore caricamento: " + ex.Message; + } + } + + private void Save() + { + if (string.IsNullOrWhiteSpace(OutputDirectory)) + { + StatusMessage = "Seleziona una cartella di output."; + return; + } + try + { + SynthDataDbContext.SaveSettings(new AppSettings + { + ApiEndpoint = ApiEndpoint, + ModelName = ModelName, + ApiKey = ApiKey, + Temperature = Temperature, + MaxTokens = MaxTokens, + SystemPrompt = SystemPrompt, + UserPrompt = UserPrompt, + OutputDirectory = OutputDirectory, + OutputFilePrefix = OutputFilePrefix, + MaxFileSizeMb = MaxFileSizeMb, + ApiTimeoutSeconds = ApiTimeoutSeconds, + TimeoutPerTokenRatio = TimeoutPerTokenRatio, + EnableQualityVerification = EnableQualityVerification, + UseSameModelForVerification = UseSameModelForVerification, + VerificationApiEndpoint = VerificationApiEndpoint, + VerificationModelName = VerificationModelName, + VerificationApiKey = VerificationApiKey, + RevenuePerHighQualityRecord = RevenuePerHighQualityRecord, + ElectricityCostPerKwh = ElectricityCostPerKwh, + SystemPowerWatt = SystemPowerWatt, + ApiCostType = ApiCostType, + ApiCostPerCall = ApiCostPerCall, + ApiCostPerBlock = ApiCostPerBlock, + ApiBlockSize = ApiBlockSize, + }); + StatusMessage = "Impostazioni salvate."; + } + catch (Exception ex) + { + StatusMessage = "Errore salvataggio: " + ex.Message; + } + } + + private void BrowseOutputDirectory() + { + var dlg = new System.Windows.Forms.FolderBrowserDialog + { + Description = "Seleziona la cartella di output", + SelectedPath = OutputDirectory + }; + if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) + OutputDirectory = dlg.SelectedPath; + } + + private void ResetTelemetry() + { + if (MessageBox.Show("Eliminare TUTTA la telemetria?", "Conferma", MessageBoxButton.YesNo, MessageBoxImage.Warning) != MessageBoxResult.Yes) + return; + try { SynthDataDbContext.DeleteAllTelemetry(); StatusMessage = "Telemetria eliminata."; } + catch (Exception ex) { StatusMessage = "Errore: " + ex.Message; } + } + + private void ResetDatabase() + { + if (MessageBox.Show("Resettare COMPLETAMENTE il database? Tutte le impostazioni e la telemetria verranno cancellate.", + "Conferma Reset", MessageBoxButton.YesNo, MessageBoxImage.Stop) != MessageBoxResult.Yes) + return; + try { SynthDataDbContext.ResetDatabase(); Load(); StatusMessage = "Database resettato."; } + catch (Exception ex) { StatusMessage = "Errore: " + ex.Message; } + } + + private void InsertDefaultBettingPrompt() + { + SystemPrompt = + "Sei il nodo validatore di una blockchain per una piattaforma di scommesse decentralizzata. " + + "Il tuo compito e generare transazioni fittizie ma realistiche in formato JSON puro. " + + "Non includere testo introduttivo o conclusivo, restituisci solo un array di oggetti JSON.\n\n" + + "Regole per la generazione dei dati:\n" + + "- Ogni transazione deve avere un tx_hash esadecimale casuale di 64 caratteri e un timestamp ISO 8601.\n" + + "- bet_type puo essere solo 'singola' o 'multipla'.\n" + + "- Genera un bankroll_at_time casuale tra 100 e 5000.\n" + + "- Logica dello Stake: Se 'singola', stake_amount deve essere intero e multiplo di 5. " + + "Se 'multipla', genera confidence_level tra 0.0 e 1.0. Se confidenza tra 0.9 e 1.0, " + + "stake_amount fino al 10% del bankroll. Altrimenti sotto l'1%.\n" + + "- Includi smart_contract_log che spiega come lo smart contract ha processato i fondi."; + + UserPrompt = "Genera un blocco di 5 nuove transazioni sequenziali rispettando rigorosamente le regole di business."; + + StatusMessage = "Prompt blockchain betting inseriti. Ricorda di salvare."; + } + + private void ApplyPreset(string preset) + { + switch (preset) + { + case "OpenAI": + ApiEndpoint = "https://api.openai.com/v1/chat/completions"; + ModelName = "gpt-4o"; + StatusMessage = "Preset OpenAI applicato. Inserisci la API Key."; + break; + case "Anthropic": + ApiEndpoint = "https://api.anthropic.com/v1/messages"; + ModelName = "claude-3-5-sonnet-20241022"; + StatusMessage = "Preset Anthropic applicato. Inserisci la API Key."; + break; + case "Google AI": + ApiEndpoint = "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions"; + ModelName = "gemini-2.0-flash-exp"; + StatusMessage = "Preset Google AI applicato. Inserisci la API Key."; + break; + case "Azure OpenAI": + ApiEndpoint = "https://.openai.azure.com/openai/deployments//chat/completions?api-version=2024-02-15-preview"; + ModelName = "gpt-4"; + StatusMessage = "Sostituisci e ."; + break; + case "LM Studio (Local)": + ApiEndpoint = "http://127.0.0.1:1234/v1/chat/completions"; + ModelName = ""; + ApiKey = ""; + StatusMessage = "Preset LM Studio applicato."; + break; + case "Ollama (Local)": + ApiEndpoint = "http://127.0.0.1:11434/v1/chat/completions"; + ModelName = "llama3.3"; + ApiKey = ""; + StatusMessage = "Preset Ollama applicato."; + break; + } + } + } +} \ No newline at end of file diff --git a/Dione/ViewModels/TelemetryHistoryViewModel.cs b/Dione/ViewModels/TelemetryHistoryViewModel.cs new file mode 100644 index 0000000..a3a62de --- /dev/null +++ b/Dione/ViewModels/TelemetryHistoryViewModel.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Dione.Data; +using Dione.Models; + +namespace Dione.ViewModels +{ + public class TelemetryHistoryViewModel : ObservableObject + { + private ObservableCollection _logs = new ObservableCollection(); + public ObservableCollection Logs { get => _logs; set => SetProperty(ref _logs, value); } + + // Filters + private string _filterBatchId = ""; + public string FilterBatchId { get => _filterBatchId; set => SetProperty(ref _filterBatchId, value); } + + private string _filterModel = ""; + public string FilterModel { get => _filterModel; set => SetProperty(ref _filterModel, value); } + + private bool? _filterSuccess; + public bool? FilterSuccess { get => _filterSuccess; set => SetProperty(ref _filterSuccess, value); } + + public string[] SuccessOptions { get; } = new[] { "All", "Success", "Errors" }; + + private string _selectedSuccessOption = "All"; + public string SelectedSuccessOption + { + get => _selectedSuccessOption; + set + { + if (SetProperty(ref _selectedSuccessOption, value)) + { + switch (value) + { + case "Success": FilterSuccess = true; break; + case "Errors": FilterSuccess = false; break; + default: FilterSuccess = null; break; + } + } + } + } + + // Stats + private int _totalCount; + public int TotalCount { get => _totalCount; set => SetProperty(ref _totalCount, value); } + + private int _filteredCount; + public int FilteredCount { get => _filteredCount; set => SetProperty(ref _filteredCount, value); } + + private string _statusMessage = ""; + public string StatusMessage { get => _statusMessage; set => SetProperty(ref _statusMessage, value); } + + public RelayCommand RefreshCommand { get; } + public RelayCommand ApplyFilterCommand { get; } + public RelayCommand ClearFilterCommand { get; } + + public TelemetryHistoryViewModel() + { + RefreshCommand = new RelayCommand(Refresh); + ApplyFilterCommand = new RelayCommand(ApplyFilter); + ClearFilterCommand = new RelayCommand(ClearFilter); + Refresh(); + } + + private void Refresh() + { + try + { + var all = SynthDataDbContext.QueryAll(); + TotalCount = all.Count; + ApplyFilterToList(all); + StatusMessage = $"Loaded {TotalCount} total records from database."; + } + catch (Exception ex) + { + StatusMessage = "Error: " + ex.Message; + } + } + + private void ApplyFilter() + { + try + { + var all = SynthDataDbContext.QueryAll(); + TotalCount = all.Count; + ApplyFilterToList(all); + } + catch (Exception ex) + { + StatusMessage = "Error: " + ex.Message; + } + } + + private void ApplyFilterToList(System.Collections.Generic.List all) + { + var filtered = all.AsEnumerable(); + + if (!string.IsNullOrWhiteSpace(FilterBatchId)) + filtered = filtered.Where(l => (l.BatchId ?? "").IndexOf(FilterBatchId, StringComparison.OrdinalIgnoreCase) >= 0); + + if (!string.IsNullOrWhiteSpace(FilterModel)) + filtered = filtered.Where(l => (l.ModelUsed ?? "").IndexOf(FilterModel, StringComparison.OrdinalIgnoreCase) >= 0); + + if (FilterSuccess.HasValue) + filtered = filtered.Where(l => l.IsSuccess == FilterSuccess.Value); + + var result = filtered.OrderByDescending(l => l.Timestamp).ToList(); + FilteredCount = result.Count; + Logs = new ObservableCollection(result); + StatusMessage = $"Showing {FilteredCount} of {TotalCount} records."; + } + + private void ClearFilter() + { + FilterBatchId = ""; + FilterModel = ""; + SelectedSuccessOption = "All"; + Refresh(); + } + } +} diff --git a/Dione/Views/DashboardView.xaml b/Dione/Views/DashboardView.xaml new file mode 100644 index 0000000..c8661ac --- /dev/null +++ b/Dione/Views/DashboardView.xaml @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Per iniziare: vai in Settings per configurare un profilo, poi in Data Designer per generare il prompt, e infine in Live Generation per avviare la generazione dati. I grafici si popoleranno automaticamente. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Dione/Views/DashboardView.xaml.cs b/Dione/Views/DashboardView.xaml.cs new file mode 100644 index 0000000..2362e0c --- /dev/null +++ b/Dione/Views/DashboardView.xaml.cs @@ -0,0 +1,12 @@ +using System.Windows.Controls; + +namespace Dione.Views +{ + public partial class DashboardView : UserControl + { + public DashboardView() + { + InitializeComponent(); + } + } +} diff --git a/Dione/Views/DataDesignerView.xaml b/Dione/Views/DataDesignerView.xaml new file mode 100644 index 0000000..6331a57 --- /dev/null +++ b/Dione/Views/DataDesignerView.xaml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Dione/Views/LiveGenerationView.xaml.cs b/Dione/Views/LiveGenerationView.xaml.cs new file mode 100644 index 0000000..7e8e13d --- /dev/null +++ b/Dione/Views/LiveGenerationView.xaml.cs @@ -0,0 +1,38 @@ +using System.ComponentModel; +using System.Windows.Controls; +using Dione.ViewModels; + +namespace Dione.Views +{ + public partial class LiveGenerationView : UserControl + { + public LiveGenerationView() + { + InitializeComponent(); + DataContextChanged += OnDataContextChanged; + } + + private LiveGenerationViewModel _vm; + + private void OnDataContextChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e) + { + if (_vm != null) + _vm.PropertyChanged -= OnVmPropertyChanged; + + _vm = e.NewValue as LiveGenerationViewModel; + + if (_vm != null) + _vm.PropertyChanged += OnVmPropertyChanged; + } + + private void OnVmPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(LiveGenerationViewModel.StreamingText) && _vm.IsStreaming) + { + var sv = FindName("StreamScrollViewer") as ScrollViewer; + sv?.ScrollToBottom(); + } + } + } +} + diff --git a/Dione/Views/PlaceholderView.xaml b/Dione/Views/PlaceholderView.xaml new file mode 100644 index 0000000..ba7dca6 --- /dev/null +++ b/Dione/Views/PlaceholderView.xaml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/Dione/Views/PlaceholderView.xaml.cs b/Dione/Views/PlaceholderView.xaml.cs new file mode 100644 index 0000000..c67cf6d --- /dev/null +++ b/Dione/Views/PlaceholderView.xaml.cs @@ -0,0 +1,17 @@ +using System.Windows.Controls; + +namespace Dione.Views +{ + public partial class PlaceholderView : UserControl + { + public PlaceholderView() + { + InitializeComponent(); + } + + public PlaceholderView(string title) : this() + { + TitleText.Text = title; + } + } +} diff --git a/Dione/Views/SettingsView.xaml b/Dione/Views/SettingsView.xaml new file mode 100644 index 0000000..59dee10 --- /dev/null +++ b/Dione/Views/SettingsView.xaml @@ -0,0 +1,292 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Dione/Views/SettingsView.xaml.cs b/Dione/Views/SettingsView.xaml.cs new file mode 100644 index 0000000..c45782d --- /dev/null +++ b/Dione/Views/SettingsView.xaml.cs @@ -0,0 +1,12 @@ +using System.Windows.Controls; + +namespace Dione.Views +{ + public partial class SettingsView : UserControl + { + public SettingsView() + { + InitializeComponent(); + } + } +} diff --git a/Dione/Views/TelemetryHistoryView.xaml b/Dione/Views/TelemetryHistoryView.xaml new file mode 100644 index 0000000..ae9b795 --- /dev/null +++ b/Dione/Views/TelemetryHistoryView.xaml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +