2026-06-08 10:39:11 -05:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.ObjectModel;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using System.Windows.Input;
|
|
|
|
|
using Avalonia.Media.Imaging;
|
|
|
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
|
|
|
using CommunityToolkit.Mvvm.Input;
|
2026-06-08 15:18:19 -05:00
|
|
|
using finder2e_foundry_converter.Models;
|
|
|
|
|
using finder2e_foundry_converter.Services;
|
2026-06-08 10:39:11 -05:00
|
|
|
|
2026-06-08 15:18:19 -05:00
|
|
|
namespace finder2e_foundry_converter.ViewModels
|
2026-06-08 10:39:11 -05:00
|
|
|
{
|
|
|
|
|
public partial class MainWindowViewModel : ViewModelBase
|
|
|
|
|
{
|
2026-06-08 15:53:40 -05:00
|
|
|
private readonly LmStudioService _lmStudioService = new();
|
|
|
|
|
private readonly OllamaService _ollamaService = new();
|
|
|
|
|
|
|
|
|
|
private ILlmService CurrentService => SelectedProvider == "Ollama" ? (ILlmService)_ollamaService : _lmStudioService;
|
2026-06-08 10:39:11 -05:00
|
|
|
|
|
|
|
|
[ObservableProperty]
|
2026-06-08 15:53:40 -05:00
|
|
|
[NotifyPropertyChangedFor(nameof(CurrentAddress))]
|
2026-06-08 10:39:11 -05:00
|
|
|
private string _lmStudioAddress = "http://localhost:1234";
|
|
|
|
|
|
2026-06-08 15:53:40 -05:00
|
|
|
[ObservableProperty]
|
|
|
|
|
[NotifyPropertyChangedFor(nameof(CurrentAddress))]
|
|
|
|
|
private string _ollamaAddress = "http://localhost:11434";
|
|
|
|
|
|
|
|
|
|
public string CurrentAddress
|
|
|
|
|
{
|
|
|
|
|
get => SelectedProvider == "Ollama" ? OllamaAddress : LmStudioAddress;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (SelectedProvider == "Ollama") OllamaAddress = value;
|
|
|
|
|
else LmStudioAddress = value;
|
|
|
|
|
OnPropertyChanged(nameof(CurrentAddress));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 10:39:11 -05:00
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool _isAddressVisible = false;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
2026-06-08 15:53:40 -05:00
|
|
|
[NotifyPropertyChangedFor(nameof(CurrentAddress))]
|
2026-06-08 10:39:11 -05:00
|
|
|
private string _selectedProvider = "LM Studio";
|
|
|
|
|
|
2026-06-08 15:53:40 -05:00
|
|
|
public ObservableCollection<string> Providers { get; } = new() { "LM Studio", "Ollama" };
|
2026-06-08 10:39:11 -05:00
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string? _selectedModel;
|
|
|
|
|
|
|
|
|
|
public ObservableCollection<string> Models { get; } = new();
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string _description = string.Empty;
|
|
|
|
|
|
|
|
|
|
public ObservableCollection<ImageItem> ImageItems { get; } = new();
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string _statusMessage = string.Empty;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private bool _isErrorVisible = false;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
private string _errorText = string.Empty;
|
|
|
|
|
|
|
|
|
|
public MainWindowViewModel()
|
|
|
|
|
{
|
|
|
|
|
// Load saved preferences would go here if we had a settings service
|
|
|
|
|
_ = StartModelRefreshLoop();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 15:53:40 -05:00
|
|
|
partial void OnSelectedProviderChanged(string value)
|
|
|
|
|
{
|
|
|
|
|
Models.Clear();
|
|
|
|
|
SelectedModel = null;
|
|
|
|
|
_ = RefreshModels();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 10:39:11 -05:00
|
|
|
[RelayCommand]
|
|
|
|
|
private void ToggleAddress()
|
|
|
|
|
{
|
|
|
|
|
IsAddressVisible = !IsAddressVisible;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task RefreshModels()
|
|
|
|
|
{
|
2026-06-08 15:53:40 -05:00
|
|
|
var models = await CurrentService.GetModelsAsync(CurrentAddress);
|
2026-06-08 10:39:11 -05:00
|
|
|
|
|
|
|
|
Models.Clear();
|
|
|
|
|
foreach (var m in models)
|
|
|
|
|
{
|
|
|
|
|
Models.Add(m);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (models.Count > 0 && !models[0].StartsWith("Error"))
|
|
|
|
|
{
|
|
|
|
|
IsErrorVisible = false;
|
|
|
|
|
if (SelectedModel == null || !Models.Contains(SelectedModel))
|
|
|
|
|
{
|
|
|
|
|
SelectedModel = Models.FirstOrDefault();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2026-06-08 15:53:40 -05:00
|
|
|
ErrorText = models.FirstOrDefault() ?? $"Error connecting to {SelectedProvider}";
|
2026-06-08 10:39:11 -05:00
|
|
|
IsErrorVisible = true;
|
|
|
|
|
IsAddressVisible = true; // Auto-show on failure
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private async Task Generate()
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(SelectedModel))
|
|
|
|
|
{
|
|
|
|
|
StatusMessage = "Please select a model first.";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
StatusMessage = "Generating...";
|
2026-06-08 15:53:40 -05:00
|
|
|
var result = await CurrentService.GenerateResponseAsync(CurrentAddress, SelectedModel, Description, ImageItems.Select(i => i.Path).ToList());
|
2026-06-08 10:39:11 -05:00
|
|
|
StatusMessage = result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void AddImagePath(string path)
|
|
|
|
|
{
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
|
|
|
|
|
{
|
|
|
|
|
if (ImageItems.Any(i => i.Path == path)) return;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// Load thumbnail
|
|
|
|
|
using var stream = File.OpenRead(path);
|
|
|
|
|
var bitmap = new Bitmap(stream);
|
|
|
|
|
// We could resize it here for efficiency if needed, but for now let's just use it
|
|
|
|
|
ImageItems.Add(new ImageItem { Path = path, Thumbnail = bitmap });
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
StatusMessage = $"Error loading image: {ex.Message}";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
private void RemoveImage(ImageItem item)
|
|
|
|
|
{
|
|
|
|
|
ImageItems.Remove(item);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task StartModelRefreshLoop()
|
|
|
|
|
{
|
|
|
|
|
while (true)
|
|
|
|
|
{
|
|
|
|
|
await RefreshModels();
|
|
|
|
|
await Task.Delay(IsErrorVisible ? 3000 : 10000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|