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:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user