Creazione progetto SynthData Pro: struttura WPF completa

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).
This commit is contained in:
2026-04-21 23:19:50 +02:00
parent c4f39e7e14
commit a768fb8e04
42 changed files with 4027 additions and 0 deletions
+199
View File
@@ -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<double>() }
};
TokensPerMinuteLabels = Array.Empty<string>();
SuccessErrorSeries = new SeriesCollection
{
new PieSeries { Title = "Success", Values = new ChartValues<double> { 0 }, DataLabels = true },
new PieSeries { Title = "API Error", Values = new ChartValues<double> { 0 }, DataLabels = true },
new PieSeries { Title = "Parse Fail", Values = new ChartValues<double> { 0 }, DataLabels = true }
};
LatencySeries = new SeriesCollection
{
new ColumnSeries { Title = "Latency (ms)", Values = new ChartValues<double>() }
};
LatencyLabels = Array.Empty<string>();
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<double>();
var tpmLabels = new List<string>();
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<double> { successCount }, DataLabels = true },
new PieSeries { Title = "API Error", Values = new ChartValues<double> { apiErrors }, DataLabels = true },
new PieSeries { Title = "Parse Fail", Values = new ChartValues<double> { parseErrors }, DataLabels = true }
};
// --- Latency bar chart (last 20 requests) ---
var recent = logs.OrderByDescending(l => l.Timestamp).Take(20).Reverse().ToList();
var latValues = new ChartValues<double>(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
}
}
}
}
+586
View File
@@ -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<LogEntry> LogEntries { get; } = new ObservableCollection<LogEntry>();
// ── 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<int>() ?? tokensPrompt;
tokensComp = usageNode["completion_tokens"]?.Value<int>() ?? tokensComp;
}
var delta = chunk["choices"]?[0]?["delta"]?["content"]?.Value<string>();
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 <think> e log se presenti ──
var strippedText = StripThinkingBlocks(generatedText);
if (strippedText.Length != generatedText.Length)
Log("INFO", $"Rimossi blocchi <think>: {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.");
/// <summary>Attende ms millisecondi; non lancia eccezione se il token viene cancellato.</summary>
private static async Task DelayAsync(int ms, CancellationToken token)
{
try { await Task.Delay(ms, token); }
catch (OperationCanceledException) { /* attesa interrotta: normale al Stop */ }
}
/// <summary>
/// Estrae il primo array JSON dalla risposta (gestisce anche ```json ... ``` markdown).
/// </summary>
private static string StripThinkingBlocks(string text)
{
if (string.IsNullOrWhiteSpace(text)) return text;
// Tag di ragionamento usati da vari modelli
var tagPairs = new[] { ("<think>", "</think>"), ("<thought>", "</thought>") };
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 <think>...</think>
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);
}
}
}
+79
View File
@@ -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<string> 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<string>(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;
}
}
}
+303
View File
@@ -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://<resource>.openai.azure.com/openai/deployments/<deployment>/chat/completions?api-version=2024-02-15-preview";
ModelName = "gpt-4";
StatusMessage = "Sostituisci <resource> e <deployment>.";
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;
}
}
}
}
@@ -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<TelemetryLog> _logs = new ObservableCollection<TelemetryLog>();
public ObservableCollection<TelemetryLog> 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<TelemetryLog> 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<TelemetryLog>(result);
StatusMessage = $"Showing {FilteredCount} of {TotalCount} records.";
}
private void ClearFilter()
{
FilterBatchId = "";
FilterModel = "";
SelectedSuccessOption = "All";
Refresh();
}
}
}