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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Dione/Views/DataDesignerView.xaml.cs b/Dione/Views/DataDesignerView.xaml.cs
new file mode 100644
index 0000000..79706f8
--- /dev/null
+++ b/Dione/Views/DataDesignerView.xaml.cs
@@ -0,0 +1,12 @@
+using System.Windows.Controls;
+
+namespace Dione.Views
+{
+ public partial class DataDesignerView : UserControl
+ {
+ public DataDesignerView()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/Dione/Views/LiveGenerationView.xaml b/Dione/Views/LiveGenerationView.xaml
new file mode 100644
index 0000000..8961c67
--- /dev/null
+++ b/Dione/Views/LiveGenerationView.xaml
@@ -0,0 +1,643 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Dione/Views/TelemetryHistoryView.xaml.cs b/Dione/Views/TelemetryHistoryView.xaml.cs
new file mode 100644
index 0000000..f4c46cc
--- /dev/null
+++ b/Dione/Views/TelemetryHistoryView.xaml.cs
@@ -0,0 +1,12 @@
+using System.Windows.Controls;
+
+namespace Dione.Views
+{
+ public partial class TelemetryHistoryView : UserControl
+ {
+ public TelemetryHistoryView()
+ {
+ InitializeComponent();
+ }
+ }
+}