diff --git a/App.axaml b/app.axaml similarity index 100% rename from App.axaml rename to app.axaml diff --git a/App.axaml.cs b/app.axaml.cs similarity index 100% rename from App.axaml.cs rename to app.axaml.cs diff --git a/Assets/avalonia-logo.ico b/assets/avalonia-logo.ico similarity index 100% rename from Assets/avalonia-logo.ico rename to assets/avalonia-logo.ico diff --git a/finder2e_foundry_converter.csproj b/finder2e_foundry_converter.csproj index 7dbfc62..7edd38b 100644 --- a/finder2e_foundry_converter.csproj +++ b/finder2e_foundry_converter.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/LICENSE b/license similarity index 100% rename from LICENSE rename to license diff --git a/Program.cs b/main.cs similarity index 100% rename from Program.cs rename to main.cs diff --git a/Models/ImageItem.cs b/models/imageitem.cs similarity index 100% rename from Models/ImageItem.cs rename to models/imageitem.cs diff --git a/README.md b/readme.md similarity index 100% rename from README.md rename to readme.md diff --git a/services/illmservice.cs b/services/illmservice.cs new file mode 100644 index 0000000..9373fd4 --- /dev/null +++ b/services/illmservice.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace finder2e_foundry_converter.Services +{ + public interface ILlmService + { + Task> GetModelsAsync(string baseUrl); + Task GenerateResponseAsync(string baseUrl, string model, string text, List imagePaths); + } +} diff --git a/Services/LlmService.cs b/services/lmstudioservice.cs similarity index 98% rename from Services/LlmService.cs rename to services/lmstudioservice.cs index 71231c8..ef7ad3d 100644 --- a/Services/LlmService.cs +++ b/services/lmstudioservice.cs @@ -8,7 +8,7 @@ using System.Text.Json.Serialization; namespace finder2e_foundry_converter.Services { - public class LlmService + public class LmStudioService : ILlmService { private readonly HttpClient _httpClient = new HttpClient(); diff --git a/services/ollamaservice.cs b/services/ollamaservice.cs new file mode 100644 index 0000000..684d19a --- /dev/null +++ b/services/ollamaservice.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using System.Threading.Tasks; +using System.Text.Json.Serialization; +using System.Linq; + +namespace finder2e_foundry_converter.Services +{ + public class OllamaService : ILlmService + { + private readonly HttpClient _httpClient = new HttpClient(); + + public async Task> GetModelsAsync(string baseUrl) + { + try + { + var response = await _httpClient.GetFromJsonAsync($"{baseUrl.TrimEnd('/')}/api/tags"); + if (response?.Models == null) return new List(); + + return response.Models.Select(m => m.Name).ToList(); + } + catch + { + return new List { "Error: Ollama not found" }; + } + } + + public async Task GenerateResponseAsync(string baseUrl, string model, string text, List imagePaths) + { + try + { + var images = new List(); + foreach (var path in imagePaths) + { + if (string.IsNullOrWhiteSpace(path)) continue; + var bytes = await System.IO.File.ReadAllBytesAsync(path); + images.Add(Convert.ToBase64String(bytes)); + } + + var requestBody = new + { + model = model, + messages = new[] + { + new { role = "user", content = text, images = images.Count > 0 ? images.ToArray() : null } + }, + stream = false + }; + + var response = await _httpClient.PostAsJsonAsync($"{baseUrl.TrimEnd('/')}/api/chat", requestBody); + var body = await response.Content.ReadAsStringAsync(); + + using var doc = JsonDocument.Parse(body); + if (doc.RootElement.TryGetProperty("error", out var error)) + { + return $"Error: {error.GetString()}"; + } + + if (doc.RootElement.TryGetProperty("message", out var message)) + { + return message.GetProperty("content").GetString() ?? "No content returned"; + } + + return "No response returned."; + } + catch (Exception ex) + { + return $"Error: {ex.Message}"; + } + } + + private class OllamaModelsResponse + { + [JsonPropertyName("models")] + public List? Models { get; set; } + } + + private class OllamaModelData + { + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + } + } +} diff --git a/ViewLocator.cs b/viewlocator.cs similarity index 100% rename from ViewLocator.cs rename to viewlocator.cs diff --git a/ViewModels/MainWindowViewModel.cs b/viewmodels/mainwindowviewmodel.cs similarity index 70% rename from ViewModels/MainWindowViewModel.cs rename to viewmodels/mainwindowviewmodel.cs index 8cb72fa..6fb022b 100644 --- a/ViewModels/MainWindowViewModel.cs +++ b/viewmodels/mainwindowviewmodel.cs @@ -14,18 +14,38 @@ namespace finder2e_foundry_converter.ViewModels { public partial class MainWindowViewModel : ViewModelBase { - private readonly LlmService _llmService = new LlmService(); + private readonly LmStudioService _lmStudioService = new(); + private readonly OllamaService _ollamaService = new(); + + private ILlmService CurrentService => SelectedProvider == "Ollama" ? (ILlmService)_ollamaService : _lmStudioService; [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CurrentAddress))] private string _lmStudioAddress = "http://localhost:1234"; + [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)); + } + } + [ObservableProperty] private bool _isAddressVisible = false; [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CurrentAddress))] private string _selectedProvider = "LM Studio"; - public ObservableCollection Providers { get; } = new() { "LM Studio" }; + public ObservableCollection Providers { get; } = new() { "LM Studio", "Ollama" }; [ObservableProperty] private string? _selectedModel; @@ -52,6 +72,13 @@ namespace finder2e_foundry_converter.ViewModels _ = StartModelRefreshLoop(); } + partial void OnSelectedProviderChanged(string value) + { + Models.Clear(); + SelectedModel = null; + _ = RefreshModels(); + } + [RelayCommand] private void ToggleAddress() { @@ -61,13 +88,7 @@ namespace finder2e_foundry_converter.ViewModels [RelayCommand] private async Task RefreshModels() { - if (SelectedProvider != "LM Studio") - { - IsErrorVisible = false; - return; - } - - var models = await _llmService.GetModelsAsync(LmStudioAddress); + var models = await CurrentService.GetModelsAsync(CurrentAddress); Models.Clear(); foreach (var m in models) @@ -85,7 +106,7 @@ namespace finder2e_foundry_converter.ViewModels } else { - ErrorText = models.FirstOrDefault() ?? "Error connecting to LM Studio"; + ErrorText = models.FirstOrDefault() ?? $"Error connecting to {SelectedProvider}"; IsErrorVisible = true; IsAddressVisible = true; // Auto-show on failure } @@ -101,7 +122,7 @@ namespace finder2e_foundry_converter.ViewModels } StatusMessage = "Generating..."; - var result = await _llmService.GenerateResponseAsync(LmStudioAddress, SelectedModel, Description, ImageItems.Select(i => i.Path).ToList()); + var result = await CurrentService.GenerateResponseAsync(CurrentAddress, SelectedModel, Description, ImageItems.Select(i => i.Path).ToList()); StatusMessage = result; } diff --git a/ViewModels/ViewModelBase.cs b/viewmodels/viewmodelbase.cs similarity index 100% rename from ViewModels/ViewModelBase.cs rename to viewmodels/viewmodelbase.cs diff --git a/Views/MainWindow.axaml b/views/mainwindow.axaml similarity index 92% rename from Views/MainWindow.axaml rename to views/mainwindow.axaml index 8207248..e43b243 100644 --- a/Views/MainWindow.axaml +++ b/views/mainwindow.axaml @@ -6,7 +6,7 @@ mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="600" x:Class="finder2e_foundry_converter.Views.MainWindow" x:DataType="vm:MainWindowViewModel" - Icon="/Assets/avalonia-logo.ico" + Icon="/assets/avalonia-logo.ico" Title="Finder2e Foundry Converter" Width="500" Height="600" DragDrop.AllowDrop="True"> @@ -30,8 +30,8 @@ -