@@ -115,6 +118,17 @@ await LoadProjects(); } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await SettingsService.InitializeAsync(); + var theme = OllamaSettings.Theme; + if (!string.IsNullOrEmpty(theme)) + await ThemeService.ApplyTheme(theme); + } + } + private async Task LoadProjects() { try diff --git a/Components/Pages/Editor.razor b/Components/Pages/Editor.razor index 5fea484..ffc1079 100644 --- a/Components/Pages/Editor.razor +++ b/Components/Pages/Editor.razor @@ -70,7 +70,7 @@ try { - analysisResult = await OllamaService.StreamStyleCheckAsync(editorContent); + //analysisResult = await OllamaService.StreamStyleCheckAsync(editorContent); } catch (Exception ex) { @@ -94,7 +94,7 @@ try { - analysisResult = await OllamaService.QuickSpellCheckAsync(editorContent); + //analysisResult = await OllamaService.QuickSpellCheckAsync(editorContent); } catch (Exception ex) { diff --git a/Components/Pages/Models.razor b/Components/Pages/Models.razor index 1d1b99f..1525164 100644 --- a/Components/Pages/Models.razor +++ b/Components/Pages/Models.razor @@ -1,7 +1,7 @@ @page "/models" @inject IOllamaService OllamaService @inject NavigationManager Navigation -@using Ollama + Modelle - Author Buddy @@ -24,7 +24,7 @@ else @foreach (var model in localModels) {
-
@model.Model1
+
@model.Name
Größe: @FormatSize(model.Size) | Geändert: @model.ModifiedAt?.ToString("g") @@ -64,7 +64,7 @@ else } @code { - private List localModels = new(); + private List localModels = new(); private bool isLoading; private string? errorMessage; private string modelToPull = string.Empty; @@ -111,10 +111,9 @@ else try { - var progress = new Progress(status => + var progress = new Progress(status => { - pullProgress = status.Progress; - pullStatus = status.Status; + pullStatus = status; InvokeAsync(StateHasChanged); }); diff --git a/Components/Pages/Settings.razor b/Components/Pages/Settings.razor index caa152d..3841d13 100644 --- a/Components/Pages/Settings.razor +++ b/Components/Pages/Settings.razor @@ -1,7 +1,7 @@ @page "/settings" -@inject IDbContextFactory ContextFactory +@inject ISettingsService SettingsService +@inject IThemeService ThemeService @inject IOllamaService OllamaService -@inject OllamaSettings OllamaConfig @inject NavigationManager Navigation @inject IJSRuntime JSRuntime @inject IProjectService ProjectService @@ -10,89 +10,107 @@
-
-

Modelle

-
- - +
+ +
+
+

Modelle

+
+ + +
+
+ + +
+
+ + +
+
+ +
+

Ollama Server

+
+ + +
Adresse des Ollama-Servers (z. B. http://192.168.1.100:11434)
+
+
+ +
+

Design

+
+ + +
+
-
- - -
-
- - + +
+
+

Lektor-System-Prompt

+
+ +
+
+ +
+

Fact-Check-Vorlage

+
+ +
+
+ +
+

Rechtschreibprüfungs-Prompt

+
+ +
+
+
-
-

Ollama Server

-
- - -
Adresse des Ollama-Servers (z. B. http://192.168.1.100:11434)
-
+
+ + @if (!string.IsNullOrEmpty(statusMessage)) + { + @statusMessage + }
-
-

Design

-
- - -
-
- -
-

Neues Projekt

-
- - -
Der Projektordner wird in "Dokumente\AuthorBuddy\" erstellt.
-
- -
- -
- - -
- - @if (!string.IsNullOrEmpty(statusMessage)) - { -
@statusMessage
- }
@code { @@ -101,22 +119,27 @@ private string spellingModel = "phi3:mini"; private string selectedTheme = "light"; private string ollamaUrl = "http://localhost:11434"; + private string lektorPrompt = string.Empty; + private string factCheckTemplate = string.Empty; + private string spellingPrompt = string.Empty; private string newProjectName = string.Empty; private string statusMessage = string.Empty; - private List availableModels = new(); + private List availableModels = new(); protected override async Task OnInitializedAsync() { - await using var context = await ContextFactory.CreateDbContextAsync(); - - ragModel = await GetSettingAsync(context, "ollama_rag_model") ?? "llama3.2"; - styleModel = await GetSettingAsync(context, "ollama_style_model") ?? "llama3.2"; - spellingModel = await GetSettingAsync(context, "ollama_spelling_model") ?? "phi3:mini"; - selectedTheme = await GetSettingAsync(context, "theme") ?? "light"; - ollamaUrl = await GetSettingAsync(context, "ollama_url") ?? "http://localhost:11434"; + ragModel = await SettingsService.GetRagModelAsync() ?? "llama3.2"; + styleModel = await SettingsService.GetStyleModelAsync() ?? "llama3.2"; + spellingModel = await SettingsService.GetSpellingModelAsync() ?? "phi3:mini"; + selectedTheme = await SettingsService.GetThemeAsync() ?? "light"; + ollamaUrl = await SettingsService.GetOllamaUrlAsync() ?? "http://localhost:11434"; + lektorPrompt = await SettingsService.GetLektorSystemPromptAsync() ?? string.Empty; + factCheckTemplate = await SettingsService.GetFactCheckTemplateAsync() ?? string.Empty; + spellingPrompt = await SettingsService.GetSpellingPromptAsync() ?? string.Empty; try { + var models = await OllamaService.GetLocalModelsAsync(); availableModels = models.ToList(); } @@ -126,75 +149,22 @@ } } - private static async Task GetSettingAsync(AppDbContext context, string key) - { - var setting = await context.Settings.FindAsync(key); - return setting?.Value; - } - - private static async Task SaveSettingAsync(AppDbContext context, string key, string value) - { - var existing = await context.Settings.FindAsync(key); - if (existing != null) - { - existing.Value = value; - } - else - { - context.Settings.Add(new SettingEntity { Key = key, Value = value }); - } - } - private async Task SaveSettings() { - await using var context = await ContextFactory.CreateDbContextAsync(); - - await SaveSettingAsync(context, "ollama_rag_model", ragModel); - await SaveSettingAsync(context, "ollama_style_model", styleModel); - await SaveSettingAsync(context, "ollama_spelling_model", spellingModel); - await SaveSettingAsync(context, "theme", selectedTheme); - await SaveSettingAsync(context, "ollama_url", ollamaUrl); - - await context.SaveChangesAsync(); - - OllamaConfig.OllamaUrl = ollamaUrl; - OllamaConfig.ChatModel = ragModel; - OllamaConfig.SpellingModel = spellingModel; - await OllamaService.ReconfigureAsync(ollamaUrl); + await SettingsService.SaveOllamaUrlAsync(ollamaUrl); + await SettingsService.SaveRagModelAsync(ragModel); + await SettingsService.SaveStyleModelAsync(styleModel); + await SettingsService.SaveSpellingModelAsync(spellingModel); + await SettingsService.SaveThemeAsync(selectedTheme); + await SettingsService.SaveLektorSystemPromptAsync(lektorPrompt); + await SettingsService.SaveFactCheckTemplateAsync(factCheckTemplate); + await SettingsService.SaveSpellingPromptAsync(spellingPrompt); + OllamaService.Reconfigure(ollamaUrl); statusMessage = "Einstellungen gespeichert."; - await ApplyTheme(selectedTheme); + await ThemeService.ApplyTheme(selectedTheme); } - private async Task CreateProject() - { - if (string.IsNullOrWhiteSpace(newProjectName)) return; + - try - { - string rootPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), - "AuthorBuddy", - newProjectName); - - await ProjectService.CreateNewProjectAsync(newProjectName, rootPath); - statusMessage = $"Projekt '{newProjectName}' erstellt unter: {rootPath}"; - newProjectName = string.Empty; - } - catch (Exception ex) - { - statusMessage = $"Fehler beim Erstellen: {ex.Message}"; - } - } - - private async Task ApplyTheme(string theme) - { - await JSRuntime.InvokeVoidAsync("eval", - $"document.getElementById('theme-css')?.remove(); " + - $"var link = document.createElement('link'); " + - $"link.id = 'theme-css'; " + - $"link.rel = 'stylesheet'; " + - $"link.href = 'css/themes/{theme}.css'; " + - $"document.head.appendChild(link);"); - } } diff --git a/Data/AppDbContext.cs b/Data/AppDbContext.cs index 58109ec..178a3fa 100644 --- a/Data/AppDbContext.cs +++ b/Data/AppDbContext.cs @@ -19,7 +19,7 @@ public class AppDbContext : DbContext entity.HasKey(e => e.Id); entity.Property(e => e.Name).IsRequired(); entity.Property(e => e.RootPath).IsRequired(); - entity.Property(e => e.LastOpened).HasColumnName("last_opened"); + entity.Property(e => e.LastOpened); }); modelBuilder.Entity(entity => @@ -65,8 +65,8 @@ public class AppDbContext : DbContext CREATE TABLE IF NOT EXISTS Projects ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, - root_path TEXT NOT NULL, - last_opened DATETIME + RootPath TEXT NOT NULL, + LastOpened DATETIME ); CREATE TABLE IF NOT EXISTS Settings ( key TEXT NOT NULL UNIQUE, diff --git a/Models/LLM_Model.cs b/Models/LLM_Model.cs new file mode 100644 index 0000000..021c7da --- /dev/null +++ b/Models/LLM_Model.cs @@ -0,0 +1,30 @@ +using System.Text.Json; + +namespace AuthorBuddy.Web.Models +{ + public class LLM_Model + { + + + public LLM_Model(JsonElement model) + { + if (model.TryGetProperty("name", out var name)) + { + Name = name.GetString() ?? "" ; + } + if (model.TryGetProperty("size", out var size)) + { + Size = size.GetInt64(); + } + if (model.TryGetProperty("modified_at", out var modified)) + { + ModifiedAt = modified.GetDateTime(); + } + } + + public string Name { get; set; } = string.Empty; + public long Size { get; set; } + public DateTime? ModifiedAt { get; set; } + + } +} diff --git a/Models/ModelStatus.cs b/Models/ModelStatus.cs index 10e4353..66d1666 100644 --- a/Models/ModelStatus.cs +++ b/Models/ModelStatus.cs @@ -1,18 +1,18 @@ -using Ollama; -namespace AuthorBuddy.Web.Models; -public class ModelStatus -{ - public string Status { get; private set; } = "Unknown"; - public double Progress { get; private set; } +//namespace AuthorBuddy.Web.Models; - public void SetModelStatus(PullModelResponse modelResponse) - { - if (modelResponse.Total > 0 && modelResponse.Completed.HasValue && modelResponse.Completed > 0) - { - Progress = (double)modelResponse.Completed.Value / (double)modelResponse.Total * 100; - Status = modelResponse.Status?.ToString() ?? "Unknown"; - } - } -} +//public class ModelStatus +//{ +// public string Status { get; private set; } = "Unknown"; +// public double Progress { get; private set; } + +// public void SetModelStatus(PullModelResponse modelResponse) +// { +// if (modelResponse.Total > 0 && modelResponse.Completed.HasValue && modelResponse.Completed > 0) +// { +// Progress = (double)modelResponse.Completed.Value / (double)modelResponse.Total * 100; +// Status = modelResponse.Status?.ToString() ?? "Unknown"; +// } +// } +//} diff --git a/Models/OllamaSettings.cs b/Models/OllamaSettings.cs index 16560e1..c88e17b 100644 --- a/Models/OllamaSettings.cs +++ b/Models/OllamaSettings.cs @@ -2,18 +2,18 @@ namespace AuthorBuddy.Web.Models; public class OllamaSettings { - public string ChatModel { get; set; } = "llama3.2"; + public string StyleModel { get; set; } = "llama3.2"; public string EmbedModel { get; set; } = "mxbai-embed-large"; public string SpellingModel { get; set; } = "phi3:mini"; public string OllamaUrl { get; set; } = "http://localhost:11434"; public string Theme { get; set; } = "light"; - public string LektorSystemPrompt { get; private set; } = "Du bist ein erfahrener Lektor. Analysiere den Text des Autors. " + + public string LektorSystemPrompt { get; set; } = "Du bist ein erfahrener Lektor. Analysiere den Text des Autors. " + "Optimiere ihn auf einen lebendigen, anschaulichen und subjektiven Stil. " + "Nutze starke Verben, vermeide unnötige Adjektive und achte auf 'Show, don't tell'. " + "Antworte direkt mit dem verbesserten Text oder konkreten Vorschlägen."; - private string FactCheckTemplate { get; set; } = "FAKTEN AUS DER DATENBANK:\n{0}\n\n" + + public string FactCheckTemplate { get; set; } = "FAKTEN AUS DER DATENBANK:\n{0}\n\n" + "FRAGE / KONTEXT:\n{1}\n\n" + "Basierend auf den Fakten, schreibe eine kurze, lebendige Ergänzung oder Korrektur."; diff --git a/Program.cs b/Program.cs index 6269dd6..d821f15 100644 --- a/Program.cs +++ b/Program.cs @@ -12,6 +12,8 @@ builder.Services.AddRazorComponents() builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddDbContextFactory(options => diff --git a/Services/FileScannerService.cs b/Services/FileScannerService.cs index 52dfd23..42d2d5e 100644 --- a/Services/FileScannerService.cs +++ b/Services/FileScannerService.cs @@ -53,7 +53,7 @@ public class FileScannerService : IFileScannerService { string content = await File.ReadAllTextAsync(path); - var vector = await _ollama.GetEmbeddingAsync(content); + //var vector = await _ollama.GetEmbeddingAsync(content); using var connection = new SqliteConnection(_connectionString); connection.Open(); @@ -73,7 +73,7 @@ public class FileScannerService : IFileScannerService var docId = await cmdMeta.ExecuteScalarAsync(); - byte[] blob = ConvertVectorToBytes(vector); + byte[] blob = [];// ConvertVectorToBytes(vector); var cmdVec = connection.CreateCommand(); cmdVec.CommandText = "INSERT INTO vec_documents(rowid, embedding) VALUES (@id, @vec) " + "ON CONFLICT(rowid) DO UPDATE SET embedding = @vec;"; diff --git a/Services/IOllamaService.cs b/Services/IOllamaService.cs index 6ef4979..6408792 100644 --- a/Services/IOllamaService.cs +++ b/Services/IOllamaService.cs @@ -1,15 +1,17 @@ + + using AuthorBuddy.Web.Models; -using Ollama; namespace AuthorBuddy.Web.Services; public interface IOllamaService { - Task> GetLocalModelsAsync(); - Task PullModelAsync(string modelName, IProgress progress); - Task> GetEmbeddingAsync(string text); - Task StreamStyleCheckAsync(string input); - Task AnswerBasedOnFactsAsync(string question, string facts); - Task QuickSpellCheckAsync(string text); - Task ReconfigureAsync(string ollamaUrl); + Task> GetLocalModelsAsync(); + Task PullModelAsync(string modelName, IProgress progress); + //Task> GetEmbeddingAsync(string text); + //Task StreamStyleCheckAsync(string input); + //Task AnswerBasedOnFactsAsync(string question, string facts); + //Task QuickSpellCheckAsync(string text); + void Reconfigure(string ollamaUrl); + } diff --git a/Services/ISettingsService.cs b/Services/ISettingsService.cs new file mode 100644 index 0000000..d853f81 --- /dev/null +++ b/Services/ISettingsService.cs @@ -0,0 +1,25 @@ +namespace AuthorBuddy.Web.Services; + +public interface ISettingsService +{ + Task GetOllamaUrlAsync(); + Task GetRagModelAsync(); + Task GetStyleModelAsync(); + Task GetSpellingModelAsync(); + Task GetThemeAsync(); + Task SaveOllamaUrlAsync(string url); + Task SaveRagModelAsync(string model); + Task SaveStyleModelAsync(string model); + Task SaveSpellingModelAsync(string model); + Task SaveThemeAsync(string theme); + + Task GetLektorSystemPromptAsync(); + Task GetFactCheckTemplateAsync(); + Task GetSpellingPromptAsync(); + + Task SaveLektorSystemPromptAsync(string prompt); + Task SaveFactCheckTemplateAsync(string prompt); + Task SaveSpellingPromptAsync(string prompt); + + Task InitializeAsync(); +} diff --git a/Services/IThemeService.cs b/Services/IThemeService.cs new file mode 100644 index 0000000..800ff9e --- /dev/null +++ b/Services/IThemeService.cs @@ -0,0 +1,6 @@ +namespace AuthorBuddy.Web.Services; + +public interface IThemeService +{ + Task ApplyTheme(string theme); +} diff --git a/Services/OllamaService.cs b/Services/OllamaService.cs index ccbee31..fa49caf 100644 --- a/Services/OllamaService.cs +++ b/Services/OllamaService.cs @@ -1,84 +1,218 @@ using AuthorBuddy.Web.Models; -using Ollama; +using System.Text.Json; namespace AuthorBuddy.Web.Services; -public class OllamaService : IOllamaService, IDisposable +public class OllamaService : IOllamaService { - private readonly OllamaSettings _settings; - private OllamaApiClient? _client; - private string _currentUrl = "http://localhost:11434"; + private readonly HttpClient _httpClient; + private string _baseUrl = "http://localhost:11434"; - public OllamaService(OllamaSettings settings) + public OllamaService() { - _settings = settings; - _currentUrl = settings.OllamaUrl; - _client = new OllamaApiClient(baseUri: new Uri(_currentUrl)); + _httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(300) }; } - public Task ReconfigureAsync(string ollamaUrl) + public void Reconfigure(string url) { - if (_currentUrl != ollamaUrl) + _baseUrl = url; + } + + public async Task> GetLocalModelsAsync() + { + try { - _currentUrl = ollamaUrl; - _client?.Dispose(); - _client = new OllamaApiClient(baseUri: new Uri(ollamaUrl)); + var response = await _httpClient.GetAsync($"{_baseUrl}/api/tags"); + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(content); + var root = doc.RootElement; + + if (root.TryGetProperty("models", out var models)) + { + var modelList = new List(); + foreach (var model in models.EnumerateArray()) + { + modelList.Add(new LLM_Model(model)); + + + } + return modelList; + } + } + return new List(); } - return Task.CompletedTask; - } - - private OllamaApiClient Client => _client ??= new OllamaApiClient(baseUri: new Uri(_currentUrl)); - - public async Task> GetLocalModelsAsync() - { - var models = await Client.Models.ListModelsAsync(); - return models.Models ?? Enumerable.Empty(); - } - - public async Task PullModelAsync(string modelName, IProgress progress) - { - var p = new ModelStatus(); - await foreach (var response in Client.Models.PullModelAsync(modelName)) + catch (Exception) { - p.SetModelStatus(response); - progress?.Report(p); + return new List(); } } - public async Task> GetEmbeddingAsync(string text) + public async Task Chat(string model, string userMessage, string? systemPrompt = null) { - var response = await Client.Embeddings.GenerateEmbeddingAsync( - model: _settings.EmbedModel, - prompt: text); - return response.Embedding ?? Array.Empty(); + try + { + var request = new + { + model = model, + prompt = userMessage, + stream = false + }; + + var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}/api/generate", request); + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(content); + var root = doc.RootElement; + + if (root.TryGetProperty("response", out var responseText)) + { + return responseText.GetString() ?? ""; + } + } + return "Fehler bei der Verarbeitung."; + } + catch (Exception ex) + { + return $"Error: {ex.Message}"; + } } - public async Task StreamStyleCheckAsync(string input) + public async IAsyncEnumerable ChatStream(string model, string userMessage, string? systemPrompt = null) { - var systemPrompt = _settings.LektorSystemPrompt; - var chat = Client.Chat(_settings.ChatModel, systemPrompt); - var message = await chat.SendAsync(input); - return message.Content; + var request = new + { + model = model, + prompt = userMessage, + stream = true + }; + + HttpResponseMessage? response = null; + try + { + response = await _httpClient.PostAsync( + $"{_baseUrl}/api/generate", + new StringContent(JsonSerializer.Serialize(request), System.Text.Encoding.UTF8, "application/json") + ); + + if (response.IsSuccessStatusCode) + { + using var stream = await response.Content.ReadAsStreamAsync(); + using var reader = new StreamReader(stream); + + string? line; + while ((line = await reader.ReadLineAsync()) != null) + { + var text = ExtractResponseText(line); + if (!string.IsNullOrEmpty(text)) + { + yield return text; + } + } + } + } + finally + { + response?.Dispose(); + } } - public async Task AnswerBasedOnFactsAsync(string question, string facts) + private string ExtractResponseText(string jsonLine) { - var prompt = _settings.GetFactCheckPrompt(question, facts); - var chat = Client.Chat(_settings.ChatModel); - var message = await chat.SendAsync(prompt); - return message.Content; + if (string.IsNullOrWhiteSpace(jsonLine)) + return string.Empty; + + try + { + using var doc = JsonDocument.Parse(jsonLine); + var root = doc.RootElement; + + if (root.TryGetProperty("response", out var responseText)) + { + return responseText.GetString() ?? string.Empty; + } + } + catch { } + + return string.Empty; } - public async Task QuickSpellCheckAsync(string text) + public async Task DeleteModel(string modelName) { - var systemPrompt = _settings.SpellingPrompt; - var chat = Client.Chat(_settings.SpellingModel, systemPrompt); - var message = await chat.SendAsync(text); - return message.Content; + try + { + var request = new HttpRequestMessage(HttpMethod.Delete, $"{_baseUrl}/api/delete"); + request.Content = new StringContent(JsonSerializer.Serialize(new { name = modelName }), + System.Text.Encoding.UTF8, "application/json"); + + var response = await _httpClient.SendAsync(request); + return response.IsSuccessStatusCode; + } + catch (Exception) + { + return false; + } } - public void Dispose() + public async Task PullModelAsync(string modelName, IProgress progress) { - _client?.Dispose(); + try + { + var request = new { name = modelName, stream = true }; + var response = await _httpClient.PostAsync( + $"{_baseUrl}/api/pull", + new StringContent(JsonSerializer.Serialize(request), System.Text.Encoding.UTF8, "application/json") + ); + + if (response.IsSuccessStatusCode) + { + using var stream = await response.Content.ReadAsStreamAsync(); + using var reader = new StreamReader(stream); + + string? line; + while ((line = await reader.ReadLineAsync()) != null) + { + if (string.IsNullOrWhiteSpace(line)) continue; + + try + { + using var doc = JsonDocument.Parse(line); + var root = doc.RootElement; + + if (root.TryGetProperty("status", out var status)) + { + var statusText = status.GetString() ?? ""; + var msg = statusText; + + if (root.TryGetProperty("completed", out var completed) && + root.TryGetProperty("total", out var total)) + { + var comp = completed.GetInt64(); + var tot = total.GetInt64(); + if (tot > 0) + { + var pct = (int)(comp * 100 / tot); + msg = $"{statusText}: {comp}/{tot} ({pct}%)"; + } + } + + progress?.Report(msg); + + if (statusText == "success") + return true; + } + } + catch { } + } + } + + return false; + } + catch (Exception) + { + return false; + } } } diff --git a/Services/SettingsService.cs b/Services/SettingsService.cs new file mode 100644 index 0000000..649b78e --- /dev/null +++ b/Services/SettingsService.cs @@ -0,0 +1,139 @@ +using AuthorBuddy.Web.Data; +using AuthorBuddy.Web.Models; +using Microsoft.EntityFrameworkCore; + +namespace AuthorBuddy.Web.Services; + +public class SettingsService : ISettingsService +{ + private const string KeyOllamaUrl = "ollama_url"; + private const string KeyRagModel = "ollama_rag_model"; + private const string KeyStyleModel = "ollama_style_model"; + private const string KeySpellingModel = "ollama_spelling_model"; + private const string KeyTheme = "theme"; + private const string KeyLektorPrompt = "ollama_lektor_prompt"; + private const string KeyFactCheckTemplate = "ollama_fact_check"; + private const string KeySpellingPrompt = "ollama_spelling_prompt"; + + private readonly IDbContextFactory _contextFactory; + private readonly OllamaSettings _ollamaConfig; + + public SettingsService(IDbContextFactory contextFactory, OllamaSettings ollamaConfig) + { + _contextFactory = contextFactory; + _ollamaConfig = ollamaConfig; + } + + private async Task GetAsync(string key) + { + await using var context = await _contextFactory.CreateDbContextAsync(); + var setting = await context.Settings.FindAsync(key); + return setting?.Value; + } + + public Task GetOllamaUrlAsync() => GetAsync(KeyOllamaUrl); + public Task GetRagModelAsync() => GetAsync(KeyRagModel); + public Task GetStyleModelAsync() => GetAsync(KeyStyleModel); + + public Task GetLektorSystemPromptAsync() => GetAsync(KeyLektorPrompt); + public Task GetFactCheckTemplateAsync() => GetAsync(KeyFactCheckTemplate); + public Task GetSpellingPromptAsync() => GetAsync(KeySpellingPrompt); + + public Task GetSpellingModelAsync() => GetAsync(KeySpellingModel); + public Task GetThemeAsync() => GetAsync(KeyTheme); + + private async Task SetAsync(string key, string value) + { + await using var context = await _contextFactory.CreateDbContextAsync(); + var existing = await context.Settings.FindAsync(key); + if (existing != null) + existing.Value = value; + else + context.Settings.Add(new SettingEntity { Key = key, Value = value }); + await context.SaveChangesAsync(); + } + + public async Task SaveOllamaUrlAsync(string url) + { + await SetAsync(KeyOllamaUrl, url); + _ollamaConfig.OllamaUrl = url; + } + + public async Task SaveRagModelAsync(string model) + { + await SetAsync(KeyRagModel, model); + _ollamaConfig.StyleModel = model; + } + + public async Task SaveStyleModelAsync(string model) + { + await SetAsync(KeyStyleModel, model); + _ollamaConfig.StyleModel = model; + } + + public async Task SaveLektorSystemPromptAsync(string prompt) + { + await SetAsync(KeyLektorPrompt, prompt); + _ollamaConfig.LektorSystemPrompt = prompt; + } + + public async Task SaveFactCheckTemplateAsync(string prompt) + { + await SetAsync(KeyFactCheckTemplate, prompt); + _ollamaConfig.FactCheckTemplate = prompt; + } + public async Task SaveSpellingPromptAsync(string prompt) + { + await SetAsync(KeySpellingPrompt, prompt); + _ollamaConfig.SpellingPrompt = prompt; + } + + public async Task SaveSpellingModelAsync(string model) + { + await SetAsync(KeySpellingModel, model); + _ollamaConfig.SpellingModel = model; + } + + public Task SaveThemeAsync(string theme) + => SetAsync(KeyTheme, theme); + + public async Task InitializeAsync() + { + await using var context = await _contextFactory.CreateDbContextAsync(); + + var entries = new (string Key, string Default)[] + { + (KeyOllamaUrl, _ollamaConfig.OllamaUrl), + (KeyRagModel, _ollamaConfig.StyleModel), + (KeyStyleModel, _ollamaConfig.StyleModel), + (KeySpellingModel, _ollamaConfig.SpellingModel), + (KeyTheme, _ollamaConfig.Theme), + (KeyLektorPrompt, _ollamaConfig.LektorSystemPrompt), + (KeyFactCheckTemplate, _ollamaConfig.FactCheckTemplate), + (KeySpellingPrompt, _ollamaConfig.SpellingPrompt), + }; + + foreach (var entry in entries) + { + var existing = await context.Settings.FindAsync(entry.Key); + if (existing != null) + { + var val = existing.Value; + if (entry.Key == KeyOllamaUrl) _ollamaConfig.OllamaUrl = val; + else if (entry.Key == KeyRagModel) _ollamaConfig.StyleModel = val; + else if (entry.Key == KeyStyleModel) _ollamaConfig.StyleModel = val; + else if (entry.Key == KeySpellingModel) _ollamaConfig.SpellingModel = val; + else if (entry.Key == KeyTheme) _ollamaConfig.Theme = val; + else if (entry.Key == KeyLektorPrompt) _ollamaConfig.LektorSystemPrompt = val; + else if (entry.Key == KeyFactCheckTemplate) _ollamaConfig.FactCheckTemplate = val; + else if (entry.Key == KeySpellingPrompt) _ollamaConfig.SpellingPrompt = val; + } + else + { + context.Settings.Add(new SettingEntity { Key = entry.Key, Value = entry.Default }); + } + } + + await context.SaveChangesAsync(); + } +} diff --git a/Services/ThemeService.cs b/Services/ThemeService.cs new file mode 100644 index 0000000..273c038 --- /dev/null +++ b/Services/ThemeService.cs @@ -0,0 +1,24 @@ +using Microsoft.JSInterop; + +namespace AuthorBuddy.Web.Services; + +public class ThemeService : IThemeService +{ + private readonly IJSRuntime _js; + + public ThemeService(IJSRuntime js) + { + _js = js; + } + + public async Task ApplyTheme(string theme) + { + await _js.InvokeVoidAsync("eval", + $"document.getElementById('theme-css')?.remove(); " + + $"var link = document.createElement('link'); " + + $"link.id = 'theme-css'; " + + $"link.rel = 'stylesheet'; " + + $"link.href = 'css/themes/{theme}.css'; " + + $"document.head.appendChild(link);"); + } +} diff --git a/author_brain.db b/author_brain.db index b78f910..6c9e6fb 100644 Binary files a/author_brain.db and b/author_brain.db differ