Compare commits
No commits in common. "7d1f289b9e31f005cfc841f9f637b5c559619ebf" and "6abb069dd0549c924e95bc0fe2786ea7367335cf" have entirely different histories.
7d1f289b9e
...
6abb069dd0
101 changed files with 594 additions and 11635 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,2 @@
|
|||
/bin/*
|
||||
/obj/*
|
||||
/.vs/*
|
||||
|
|
|
|||
|
|
@ -1,128 +0,0 @@
|
|||
## WORKSPACE EXECUTIVE SUMMARY (YAML)
|
||||
# Generated: 2026-05-23T07:48:43.1838976+02:00
|
||||
# Root: E:\ollama_net\ollama\AuthorBuddy.Web
|
||||
Architecture:
|
||||
Type: Backend (C#/.NET)
|
||||
Scale: 42 relevant source files
|
||||
Components: 0
|
||||
Controllers: 0
|
||||
Models: 8
|
||||
Services: 18
|
||||
---
|
||||
- File: AuthorBuddy.Web.csproj
|
||||
Preview: "<Project Sdk='Microsoft.NET.Sdk.Web'> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>en..."
|
||||
- File: appsettings.Development.json
|
||||
Preview: "{ 'Logging': { 'LogLevel': { 'Default': 'Information', 'Microsoft.AspNetCore': 'Warning' } } } "
|
||||
- File: appsettings.json
|
||||
Preview: "{ 'Logging': { 'LogLevel': { 'Default': 'Information', 'Microsoft.AspNetCore': 'Warning' } }, 'AllowedHosts': '*' } "
|
||||
- File: AuthorBuddy.Web.slnx
|
||||
Preview: "<Solution> <Project Path='AuthorBuddy.Web.csproj' /> </Solution> "
|
||||
- File: Program.cs
|
||||
Preview: "using AuthorBuddy.Web.Components; using AuthorBuddy.Web.Models; using AuthorBuddy.Web.Services; using LiteDB; using Microsoft.AspNetCore.Authenticatio..."
|
||||
- File: Data\DocumentMetaEntity.cs
|
||||
Symbols: [DocumentMetaEntity]
|
||||
Preview: "namespace AuthorBuddy.Web.Data; public class DocumentMetaEntity { public int Id { get; set; } public int? ProjectId { get; set; } public string ..."
|
||||
- File: Data\DocumentVector.cs
|
||||
Symbols: [DocumentVector]
|
||||
Preview: "namespace AuthorBuddy.Web.Data; public class DocumentVector { public int Id { get; set; } public int DocumentMetaId { get; set; } public int Pro..."
|
||||
- File: Data\ProjectEntity.cs
|
||||
Symbols: [ProjectEntity]
|
||||
Preview: "namespace AuthorBuddy.Web.Data; public class ProjectEntity { public int Id { get; set; } public string Name { get; set; } = string.Empty; public..."
|
||||
- File: Data\SettingEntity.cs
|
||||
Symbols: [SettingEntity]
|
||||
Preview: "using LiteDB; namespace AuthorBuddy.Web.Data; public class SettingEntity { public int Id { get; set; } public string Key { get; set; } = string.Em..."
|
||||
- File: Data\UserEntity.cs
|
||||
Symbols: [UserEntity]
|
||||
Preview: "namespace AuthorBuddy.Web.Data; public class UserEntity { public int Id { get; set; } public string Name { get; set; } = string.Empty; public st..."
|
||||
- File: Models\Chapter.cs
|
||||
Symbols: [Chapter]
|
||||
Preview: "namespace AuthorBuddy.Web.Models; public class Chapter { public string Title { get; set; } = string.Empty; public string FilePath { get; set; } = ..."
|
||||
- File: Models\Constants.cs
|
||||
Symbols: [Constants]
|
||||
Preview: "namespace AuthorBuddy.Web.Models; public static class Constants { public static readonly string[] SubFolders = { '00_Drafts', '01_Fakten/Per..."
|
||||
- File: Models\FileTreeItem.cs
|
||||
Symbols: [FileTreeItem]
|
||||
Preview: "namespace AuthorBuddy.Web.Models; public class FileTreeItem { public string Name { get; set; } = string.Empty; public string FullPath { get; set; ..."
|
||||
- File: Models\LLM_Model.cs
|
||||
Symbols: [LLM_Model, LLM_Model]
|
||||
Preview: "using System.Text.Json; namespace AuthorBuddy.Web.Models { public class LLM_Model { public LLM_Model(JsonElement model) { if (mode..."
|
||||
- File: Models\ModelStatus.cs
|
||||
Symbols: [ModelStatus]
|
||||
Preview: " //namespace AuthorBuddy.Web.Models; //public class ModelStatus //{ // public string Status { get; private set; } = 'Unknown'; // public double Prog..."
|
||||
- File: Models\OllamaSettings.cs
|
||||
Symbols: [OllamaSettings]
|
||||
Preview: "namespace AuthorBuddy.Web.Models; public class OllamaSettings { public string StyleModel { get; set; } = 'gemma4:e4b'; public string EmbedModel { ..."
|
||||
- File: Models\Project.cs
|
||||
Symbols: [Project]
|
||||
Preview: "namespace AuthorBuddy.Web.Models; public class Project { public int Id { get; internal set; } public string Name { get; internal set; } = string.E..."
|
||||
- File: Models\WikipediaResult.cs
|
||||
Symbols: [WikipediaResult]
|
||||
Preview: "namespace AuthorBuddy.Web.Models; public class WikipediaResult { public string Title { get; set; } = string.Empty; public string Snippet { get; se..."
|
||||
- File: Properties\launchSettings.json
|
||||
Preview: "{ '$schema': 'https://json.schemastore.org/launchsettings.json', 'profiles': { 'http': { 'commandName': 'Project', 'dotnetRunMessages': ..."
|
||||
- File: Resources\de.json
|
||||
Preview: "{ 'common.status.ready': 'Bereit', 'common.cancel': 'Abbrechen', 'common.error': 'Fehler:', 'common.copy': 'Kopieren', 'common.status.error': 'Fe..."
|
||||
- File: Resources\en.json
|
||||
Preview: "{ 'common.status.ready': 'Ready', 'common.cancel': 'Cancel', 'common.error': 'Error:', 'common.copy': 'Copy', 'common.status.error': 'Error', 'p..."
|
||||
- File: Services\AuthService.cs
|
||||
Symbols: [AuthService]
|
||||
Preview: "using AuthorBuddy.Web.Data; using LiteDB; namespace AuthorBuddy.Web.Services; public class AuthService : IAuthService { private readonly ILiteDataba..."
|
||||
- File: Services\CustomAuthStateProvider.cs
|
||||
Symbols: [CustomAuthStateProvider]
|
||||
Preview: "using System.Security.Claims; using Microsoft.AspNetCore.Components.Authorization; namespace AuthorBuddy.Web.Services; public class CustomAuthStatePro..."
|
||||
- File: Services\FileScannerService.cs
|
||||
Symbols: [FileScannerService]
|
||||
Preview: "using AuthorBuddy.Web.Data; using LiteDB; namespace AuthorBuddy.Web.Services; public class FileScannerService : IFileScannerService { private readon..."
|
||||
- File: Services\IAuthService.cs
|
||||
Symbols: [IAuthService]
|
||||
Preview: "using AuthorBuddy.Web.Data; namespace AuthorBuddy.Web.Services; public interface IAuthService { Task<bool> LoginAsync(string username, string passwo..."
|
||||
- File: Services\IFileScannerService.cs
|
||||
Symbols: [IFileScannerService]
|
||||
Preview: "namespace AuthorBuddy.Web.Services; public interface IFileScannerService { Task ScanProjectFolderAsync(int projectId, string rootPath, IProgress<str..."
|
||||
- File: Services\ILocalizationService.cs
|
||||
Symbols: [ILocalizationService]
|
||||
Preview: "namespace AuthorBuddy.Web.Services; public interface ILocalizationService { string this[string key] { get; } string Get(string key); Task InitAs..."
|
||||
- File: Services\IOllamaService.cs
|
||||
Symbols: [IOllamaService]
|
||||
Preview: " using AuthorBuddy.Web.Models; namespace AuthorBuddy.Web.Services; public interface IOllamaService { Task<IEnumerable<LLM_Model>> GetLocalModelsAsyn..."
|
||||
- File: Services\IProjectService.cs
|
||||
Symbols: [IProjectService]
|
||||
Preview: "using AuthorBuddy.Web.Models; namespace AuthorBuddy.Web.Services; public interface IProjectService { Task<int> CreateNewProjectAsync(string projectN..."
|
||||
- File: Services\IRagService.cs
|
||||
Symbols: [IRagServi, RagSearchResult]
|
||||
Preview: "namespace AuthorBuddy.Web.Services; public class RagSearchResult { public string Text { get; set; } = string.Empty; public string FilePath { get; ..."
|
||||
- File: Services\ISettingsService.cs
|
||||
Symbols: [ISettingsService]
|
||||
Preview: "using AuthorBuddy.Web.Data; namespace AuthorBuddy.Web.Services; public interface ISettingsService { Task<string?> GetOllamaUrlAsync(); Task<string..."
|
||||
- File: Services\IThemeService.cs
|
||||
Symbols: [IThemeService]
|
||||
Preview: "namespace AuthorBuddy.Web.Services; public interface IThemeService { Task ApplyTheme(string theme); string Theme { get; } } "
|
||||
- File: Services\IWikipediaService.cs
|
||||
Symbols: [IWikipediaService]
|
||||
Preview: "using AuthorBuddy.Web.Models; namespace AuthorBuddy.Web.Services; public interface IWikipediaService { Task<List<WikipediaResult>> SearchAsync(strin..."
|
||||
- File: Services\LocalizationService.cs
|
||||
Symbols: [LocalizationService]
|
||||
Preview: "using System.Text.Json; namespace AuthorBuddy.Web.Services; public class LocalizationService : ILocalizationService { private readonly ISettingsServ..."
|
||||
- File: Services\OllamaService.cs
|
||||
Symbols: [OllamaService]
|
||||
Preview: "using AuthorBuddy.Web.Models; using System.Text.Json; using static System.Net.Mime.MediaTypeNames; namespace AuthorBuddy.Web.Services; public class Ol..."
|
||||
- File: Services\ProjectService.cs
|
||||
Symbols: [ProjectService]
|
||||
Preview: "using AuthorBuddy.Web.Data; using AuthorBuddy.Web.Models; using LiteDB; namespace AuthorBuddy.Web.Services; public class ProjectService : IProjectServ..."
|
||||
- File: Services\RagService.cs
|
||||
Symbols: [RagService]
|
||||
Preview: "using AuthorBuddy.Web.Data; using LiteDB; using System.Security.Cryptography; using System.Text; namespace AuthorBuddy.Web.Services; public class RagS..."
|
||||
- File: Services\SettingsService.cs
|
||||
Symbols: [SettingsService]
|
||||
Preview: "using AuthorBuddy.Web.Data; using AuthorBuddy.Web.Models; using LiteDB; namespace AuthorBuddy.Web.Services; public class SettingsService : ISettingsSe..."
|
||||
- File: Services\ThemeService.cs
|
||||
Symbols: [ThemeService]
|
||||
Preview: "using Microsoft.JSInterop; using System.Reflection.Metadata.Ecma335; namespace AuthorBuddy.Web.Services; public class ThemeService : IThemeService { ..."
|
||||
- File: Services\TotpHelper.cs
|
||||
Symbols: [TotpHelper]
|
||||
Preview: "using System.Security.Cryptography; namespace AuthorBuddy.Web.Services; public static class TotpHelper { private const int TotpSize = 6; private c..."
|
||||
- File: Services\WikipediaService.cs
|
||||
Symbols: [WikipediaService]
|
||||
Preview: "using AuthorBuddy.Web.Models; using System.Text.Json; using System.Web; namespace AuthorBuddy.Web.Services; public class WikipediaService : IWikipedia..."
|
||||
- File: Components\Layout\ReconnectModal.razor.js
|
||||
Preview: "// Set up event handlers const reconnectModal = document.getElementById('components-reconnect-modal'); reconnectModal.addEventListener('components-rec..."
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -3,147 +3,12 @@
|
|||
"WorkspaceRootPath": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\",
|
||||
"Documents": [
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\install.md||{EFC0BB08-EA7D-40C6-A696-C870411A895B}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:install.md||{EFC0BB08-EA7D-40C6-A696-C870411A895B}"
|
||||
"AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|E:\\ollama_net\\ollama\\AuthorBuddy.Web\\AuthorBuddy.Web.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}",
|
||||
"RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:AuthorBuddy.Web.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\pages\\editor.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\pages\\editor.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\projectservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\projectservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\pages\\ollamaassistant.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\pages\\ollamaassistant.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\iollamasetupservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\iollamasetupservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\layout\\mainlayout.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\layout\\mainlayout.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\pages\\settings.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\pages\\settings.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\data\\userentity.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:data\\userentity.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\settingsservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\settingsservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\ithemeservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\ithemeservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\isettingsservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\isettingsservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\iragservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\iragservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\HP\\Desktop\\Website\\A7405018.jpg||{58961B49-13E0-48C0-9258-13CBC4D40279}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\themeservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\themeservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\assets\\logo.jpg||{58961B49-13E0-48C0-9258-13CBC4D40279}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:assets\\logo.jpg||{58961B49-13E0-48C0-9258-13CBC4D40279}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\redirecttologin.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\redirecttologin.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\pages\\login.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\pages\\login.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\totphelper.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\totphelper.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\models\\ollamasettings.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:models\\ollamasettings.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\filescannerservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\filescannerservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\models\\project.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:models\\project.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\ragservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\ragservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\layout\\navmenu.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\layout\\navmenu.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\AppDbContext.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:Data\\AppDbContext.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\app.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\app.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\models\\llm_model.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:models\\llm_model.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\models\\modelstatus.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:models\\modelstatus.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\pages\\models.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\pages\\models.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\ifilescannerservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\ifilescannerservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\wwwroot\\lib\\bootstrap\\dist\\css\\bootstrap-utilities.css||{A5401142-F49D-43DB-90B1-F57BA349E55C}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:wwwroot\\lib\\bootstrap\\dist\\css\\bootstrap-utilities.css||{A5401142-F49D-43DB-90B1-F57BA349E55C}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\layout\\mainlayout.razor.css||{A5401142-F49D-43DB-90B1-F57BA349E55C}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\layout\\mainlayout.razor.css||{A5401142-F49D-43DB-90B1-F57BA349E55C}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\iprojectservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\iprojectservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\data\\projectentity.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:data\\projectentity.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\authorbuddy.web.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:authorbuddy.web.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}"
|
||||
"AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|E:\\ollama_net\\ollama\\AuthorBuddy.Web\\appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}",
|
||||
"RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}"
|
||||
}
|
||||
],
|
||||
"DocumentGroupContainers": [
|
||||
|
|
@ -153,424 +18,11 @@
|
|||
"DocumentGroups": [
|
||||
{
|
||||
"DockedWidth": 200,
|
||||
"SelectedChildIndex": 1,
|
||||
"SelectedChildIndex": 0,
|
||||
"Children": [
|
||||
{
|
||||
"$type": "Bookmark",
|
||||
"Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 0,
|
||||
"Title": "INSTALL.md",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\INSTALL.md",
|
||||
"RelativeDocumentMoniker": "INSTALL.md",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\INSTALL.md",
|
||||
"RelativeToolTip": "INSTALL.md",
|
||||
"ViewState": "AgIAAAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001818|",
|
||||
"WhenOpened": "2026-05-31T17:07:33.423Z",
|
||||
"EditorCaption": ""
|
||||
},
|
||||
{
|
||||
"$type": "Bookmark",
|
||||
"Name": "RST:0:0:{b3f9f954-0ca5-0091-76b4-34a47f44c954}"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 4,
|
||||
"Title": "IOllamaSetupService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IOllamaSetupService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\IOllamaSetupService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IOllamaSetupService.cs",
|
||||
"RelativeToolTip": "Services\\IOllamaSetupService.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-30T08:35:51.087Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 13,
|
||||
"Title": "A7405018.jpg",
|
||||
"DocumentMoniker": "C:\\Users\\HP\\Desktop\\Website\\A7405018.jpg",
|
||||
"ToolTip": "C:\\Users\\HP\\Desktop\\Website\\A7405018.jpg",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001533|",
|
||||
"WhenOpened": "2026-05-23T05:52:56.265Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 9,
|
||||
"Title": "SettingsService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\SettingsService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\SettingsService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\SettingsService.cs",
|
||||
"RelativeToolTip": "Services\\SettingsService.cs",
|
||||
"ViewState": "AgIAAE0BAAAAAAAAAAAAAC0BAAAMAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-21T17:37:24.777Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 7,
|
||||
"Title": "Settings.razor",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Settings.razor",
|
||||
"RelativeDocumentMoniker": "Components\\Pages\\Settings.razor",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Settings.razor",
|
||||
"RelativeToolTip": "Components\\Pages\\Settings.razor",
|
||||
"ViewState": "AgIAAJsAAAAAAAAAAAAAAKwAAAAWAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
|
||||
"WhenOpened": "2026-05-15T16:43:26.6Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 1,
|
||||
"Title": "Editor.razor",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Editor.razor",
|
||||
"RelativeDocumentMoniker": "Components\\Pages\\Editor.razor",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Editor.razor",
|
||||
"RelativeToolTip": "Components\\Pages\\Editor.razor",
|
||||
"ViewState": "AgIAAFsCAAAAAAAAAAAiwEgDAAAkAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
|
||||
"WhenOpened": "2026-05-16T13:01:59.743Z",
|
||||
"EditorCaption": ""
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 2,
|
||||
"Title": "ProjectService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ProjectService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\ProjectService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ProjectService.cs",
|
||||
"RelativeToolTip": "Services\\ProjectService.cs",
|
||||
"ViewState": "AgIAAN0AAAAAAAAAAAApwO0AAAAMAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-15T15:56:55.229Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 3,
|
||||
"Title": "OllamaAssistant.razor",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\OllamaAssistant.razor",
|
||||
"RelativeDocumentMoniker": "Components\\Pages\\OllamaAssistant.razor",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\OllamaAssistant.razor",
|
||||
"RelativeToolTip": "Components\\Pages\\OllamaAssistant.razor",
|
||||
"ViewState": "AgIAANoAAAAAAAAAAAAiwPIAAAAZAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
|
||||
"WhenOpened": "2026-05-20T05:14:39.037Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 5,
|
||||
"Title": "MainLayout.razor",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\MainLayout.razor",
|
||||
"RelativeDocumentMoniker": "Components\\Layout\\MainLayout.razor",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\MainLayout.razor",
|
||||
"RelativeToolTip": "Components\\Layout\\MainLayout.razor",
|
||||
"ViewState": "AgIAACABAAAAAAAAAAApwDQBAAARAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
|
||||
"WhenOpened": "2026-05-15T16:04:36.444Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 8,
|
||||
"Title": "UserEntity.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\UserEntity.cs",
|
||||
"RelativeDocumentMoniker": "Data\\UserEntity.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\UserEntity.cs",
|
||||
"RelativeToolTip": "Data\\UserEntity.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAsAAAAVAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-24T04:52:42.227Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 6,
|
||||
"Title": "Program.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Program.cs",
|
||||
"RelativeDocumentMoniker": "Program.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Program.cs",
|
||||
"RelativeToolTip": "Program.cs",
|
||||
"ViewState": "AgIAACwAAAAAAAAAAAAQwD8AAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-20T04:07:21.54Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 10,
|
||||
"Title": "IThemeService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IThemeService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\IThemeService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IThemeService.cs",
|
||||
"RelativeToolTip": "Services\\IThemeService.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAqwAgAAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-21T17:32:01.784Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 16,
|
||||
"Title": "RedirectToLogin.razor",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\RedirectToLogin.razor",
|
||||
"RelativeDocumentMoniker": "Components\\RedirectToLogin.razor",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\RedirectToLogin.razor",
|
||||
"RelativeToolTip": "Components\\RedirectToLogin.razor",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAsAAAAIAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
|
||||
"WhenOpened": "2026-05-21T16:31:16.612Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 11,
|
||||
"Title": "ISettingsService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ISettingsService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\ISettingsService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ISettingsService.cs",
|
||||
"RelativeToolTip": "Services\\ISettingsService.cs",
|
||||
"ViewState": "AgIAABwAAAAAAAAAAAA9wC4AAAAZAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-21T16:35:36.655Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 12,
|
||||
"Title": "IRagService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IRagService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\IRagService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IRagService.cs",
|
||||
"RelativeToolTip": "Services\\IRagService.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAsAAAAVAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-18T15:37:44.565Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 17,
|
||||
"Title": "Login.razor",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Login.razor",
|
||||
"RelativeDocumentMoniker": "Components\\Pages\\Login.razor",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Login.razor",
|
||||
"RelativeToolTip": "Components\\Pages\\Login.razor",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
|
||||
"WhenOpened": "2026-05-21T08:47:41.996Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 14,
|
||||
"Title": "ThemeService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ThemeService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\ThemeService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ThemeService.cs",
|
||||
"RelativeToolTip": "Services\\ThemeService.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAABEAAAAdAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-16T13:35:33.991Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 15,
|
||||
"Title": "logo.jpg",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Assets\\logo.jpg",
|
||||
"RelativeDocumentMoniker": "Assets\\logo.jpg",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Assets\\logo.jpg",
|
||||
"RelativeToolTip": "Assets\\logo.jpg",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001533|",
|
||||
"WhenOpened": "2026-05-21T17:15:10.852Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 19,
|
||||
"Title": "OllamaSettings.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\OllamaSettings.cs",
|
||||
"RelativeDocumentMoniker": "Models\\OllamaSettings.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\OllamaSettings.cs",
|
||||
"RelativeToolTip": "Models\\OllamaSettings.cs",
|
||||
"ViewState": "AgIAAAYAAAAAAAAAAAAkwBQAAAASAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-16T13:39:46.179Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 24,
|
||||
"Title": "AppDbContext.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\AppDbContext.cs",
|
||||
"RelativeDocumentMoniker": "Data\\AppDbContext.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\AppDbContext.cs",
|
||||
"RelativeToolTip": "Data\\AppDbContext.cs",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-14T10:50:13.284Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 20,
|
||||
"Title": "FileScannerService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\FileScannerService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\FileScannerService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\FileScannerService.cs",
|
||||
"RelativeToolTip": "Services\\FileScannerService.cs",
|
||||
"ViewState": "AgIAABIAAAAAAAAAAAAQwCUAAAAWAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-16T06:02:53.46Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 18,
|
||||
"Title": "TotpHelper.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\TotpHelper.cs",
|
||||
"RelativeDocumentMoniker": "Services\\TotpHelper.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\TotpHelper.cs",
|
||||
"RelativeToolTip": "Services\\TotpHelper.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-21T08:20:49.461Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 21,
|
||||
"Title": "Project.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\Project.cs",
|
||||
"RelativeDocumentMoniker": "Models\\Project.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\Project.cs",
|
||||
"RelativeToolTip": "Models\\Project.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAYAAAAaAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-15T15:57:10.2Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 22,
|
||||
"Title": "RagService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\RagService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\RagService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\RagService.cs",
|
||||
"RelativeToolTip": "Services\\RagService.cs",
|
||||
"ViewState": "AgIAABIAAAAAAAAAAAAxwBoAAAAIAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-18T15:40:05.413Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 23,
|
||||
"Title": "NavMenu.razor",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\NavMenu.razor",
|
||||
"RelativeDocumentMoniker": "Components\\Layout\\NavMenu.razor",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\NavMenu.razor",
|
||||
"RelativeToolTip": "Components\\Layout\\NavMenu.razor",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
|
||||
"WhenOpened": "2026-05-15T16:04:37.739Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 25,
|
||||
"Title": "App.razor",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\App.razor",
|
||||
"RelativeDocumentMoniker": "Components\\App.razor",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\App.razor",
|
||||
"RelativeToolTip": "Components\\App.razor",
|
||||
"ViewState": "AgIAAAYAAAAAAAAAAAAAABgAAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
|
||||
"WhenOpened": "2026-05-16T05:42:06.383Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 28,
|
||||
"Title": "Models.razor",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Models.razor",
|
||||
"RelativeDocumentMoniker": "Components\\Pages\\Models.razor",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Models.razor",
|
||||
"RelativeToolTip": "Components\\Pages\\Models.razor",
|
||||
"ViewState": "AgIAAGgAAAAAAAAAAAApwHQAAAAtAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
|
||||
"WhenOpened": "2026-05-16T12:51:13.798Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 26,
|
||||
"Title": "LLM_Model.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\LLM_Model.cs",
|
||||
"RelativeDocumentMoniker": "Models\\LLM_Model.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\LLM_Model.cs",
|
||||
"RelativeToolTip": "Models\\LLM_Model.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAqwBQAAAAzAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-16T12:53:35.486Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 27,
|
||||
"Title": "ModelStatus.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\ModelStatus.cs",
|
||||
"RelativeDocumentMoniker": "Models\\ModelStatus.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\ModelStatus.cs",
|
||||
"RelativeToolTip": "Models\\ModelStatus.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAABEAAAADAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-16T13:01:24.508Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 31,
|
||||
"Title": "MainLayout.razor.css",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\MainLayout.razor.css",
|
||||
"RelativeDocumentMoniker": "Components\\Layout\\MainLayout.razor.css",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\MainLayout.razor.css",
|
||||
"RelativeToolTip": "Components\\Layout\\MainLayout.razor.css",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003000|",
|
||||
"WhenOpened": "2026-05-15T16:04:50.455Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 29,
|
||||
"Title": "IFileScannerService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IFileScannerService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\IFileScannerService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IFileScannerService.cs",
|
||||
"RelativeToolTip": "Services\\IFileScannerService.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAIAAAAeAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-16T06:02:56.726Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 30,
|
||||
"Title": "bootstrap-utilities.css",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\wwwroot\\lib\\bootstrap\\dist\\css\\bootstrap-utilities.css",
|
||||
"RelativeDocumentMoniker": "wwwroot\\lib\\bootstrap\\dist\\css\\bootstrap-utilities.css",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\wwwroot\\lib\\bootstrap\\dist\\css\\bootstrap-utilities.css",
|
||||
"RelativeToolTip": "wwwroot\\lib\\bootstrap\\dist\\css\\bootstrap-utilities.css",
|
||||
"ViewState": "AgIAAOgIAAAAAAAAAAAAAPYIAAAIAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003000|",
|
||||
"WhenOpened": "2026-05-15T16:28:43.874Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 33,
|
||||
"Title": "ProjectEntity.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\ProjectEntity.cs",
|
||||
"RelativeDocumentMoniker": "Data\\ProjectEntity.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\ProjectEntity.cs",
|
||||
"RelativeToolTip": "Data\\ProjectEntity.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-14T10:50:06.549Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 32,
|
||||
"Title": "IProjectService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IProjectService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\IProjectService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IProjectService.cs",
|
||||
"RelativeToolTip": "Services\\IProjectService.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAbAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-14T10:46:30.166Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 34,
|
||||
"Title": "AuthorBuddy.Web.csproj",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\AuthorBuddy.Web.csproj",
|
||||
"RelativeDocumentMoniker": "AuthorBuddy.Web.csproj",
|
||||
|
|
@ -578,11 +30,12 @@
|
|||
"RelativeToolTip": "AuthorBuddy.Web.csproj",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000758|",
|
||||
"WhenOpened": "2026-05-14T10:14:30.128Z"
|
||||
"WhenOpened": "2026-05-14T10:14:30.128Z",
|
||||
"EditorCaption": ""
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 35,
|
||||
"DocumentIndex": 1,
|
||||
"Title": "appsettings.json",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\appsettings.json",
|
||||
"RelativeDocumentMoniker": "appsettings.json",
|
||||
|
|
@ -590,7 +43,16 @@
|
|||
"RelativeToolTip": "appsettings.json",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001642|",
|
||||
"WhenOpened": "2026-05-14T10:14:25.605Z"
|
||||
"WhenOpened": "2026-05-14T10:14:25.605Z",
|
||||
"EditorCaption": ""
|
||||
},
|
||||
{
|
||||
"$type": "Bookmark",
|
||||
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
|
||||
},
|
||||
{
|
||||
"$type": "Bookmark",
|
||||
"Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,147 +3,12 @@
|
|||
"WorkspaceRootPath": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\",
|
||||
"Documents": [
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\install.md||{EFC0BB08-EA7D-40C6-A696-C870411A895B}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:install.md||{EFC0BB08-EA7D-40C6-A696-C870411A895B}"
|
||||
"AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|E:\\ollama_net\\ollama\\AuthorBuddy.Web\\AuthorBuddy.Web.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}",
|
||||
"RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:AuthorBuddy.Web.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\pages\\editor.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\pages\\editor.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\projectservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\projectservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\pages\\ollamaassistant.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\pages\\ollamaassistant.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\iollamasetupservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\iollamasetupservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\layout\\mainlayout.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\layout\\mainlayout.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:program.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\pages\\settings.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\pages\\settings.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\data\\userentity.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:data\\userentity.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\settingsservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\settingsservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\ithemeservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\ithemeservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\isettingsservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\isettingsservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\iragservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\iragservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|C:\\Users\\HP\\Desktop\\Website\\A7405018.jpg||{58961B49-13E0-48C0-9258-13CBC4D40279}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\themeservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\themeservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\assets\\logo.jpg||{58961B49-13E0-48C0-9258-13CBC4D40279}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:assets\\logo.jpg||{58961B49-13E0-48C0-9258-13CBC4D40279}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\redirecttologin.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\redirecttologin.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\pages\\login.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\pages\\login.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\totphelper.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\totphelper.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\models\\ollamasettings.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:models\\ollamasettings.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\filescannerservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\filescannerservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\models\\project.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:models\\project.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\ragservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\ragservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\layout\\navmenu.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\layout\\navmenu.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\AppDbContext.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:Data\\AppDbContext.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\app.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\app.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\models\\llm_model.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:models\\llm_model.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\models\\modelstatus.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:models\\modelstatus.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\pages\\models.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\pages\\models.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\ifilescannerservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\ifilescannerservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\wwwroot\\lib\\bootstrap\\dist\\css\\bootstrap-utilities.css||{A5401142-F49D-43DB-90B1-F57BA349E55C}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:wwwroot\\lib\\bootstrap\\dist\\css\\bootstrap-utilities.css||{A5401142-F49D-43DB-90B1-F57BA349E55C}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\components\\layout\\mainlayout.razor.css||{A5401142-F49D-43DB-90B1-F57BA349E55C}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\layout\\mainlayout.razor.css||{A5401142-F49D-43DB-90B1-F57BA349E55C}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\services\\iprojectservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\iprojectservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\data\\projectentity.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:data\\projectentity.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\authorbuddy.web.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:authorbuddy.web.csproj||{FA3CD31E-987B-443A-9B81-186104E8DAC1}"
|
||||
},
|
||||
{
|
||||
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}",
|
||||
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}"
|
||||
"AbsoluteMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|E:\\ollama_net\\ollama\\AuthorBuddy.Web\\appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}",
|
||||
"RelativeMoniker": "D:0:0:{A2FE74E1-B743-11D0-AE1A-00A0C90FFFC3}|\u003CMiscFiles\u003E|solutionrelative:appsettings.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}"
|
||||
}
|
||||
],
|
||||
"DocumentGroupContainers": [
|
||||
|
|
@ -157,420 +22,15 @@
|
|||
"Children": [
|
||||
{
|
||||
"$type": "Bookmark",
|
||||
"Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}"
|
||||
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
|
||||
},
|
||||
{
|
||||
"$type": "Bookmark",
|
||||
"Name": "RST:0:0:{b3f9f954-0ca5-0091-76b4-34a47f44c954}"
|
||||
"Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 0,
|
||||
"Title": "INSTALL.md",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\INSTALL.md",
|
||||
"RelativeDocumentMoniker": "INSTALL.md",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\INSTALL.md",
|
||||
"RelativeToolTip": "INSTALL.md",
|
||||
"ViewState": "AgIAAFQAAAAAAAAAAAAAAGwAAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001818|",
|
||||
"WhenOpened": "2026-05-31T17:07:33.423Z",
|
||||
"EditorCaption": ""
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 4,
|
||||
"Title": "IOllamaSetupService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IOllamaSetupService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\IOllamaSetupService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IOllamaSetupService.cs",
|
||||
"RelativeToolTip": "Services\\IOllamaSetupService.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-30T08:35:51.087Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 13,
|
||||
"Title": "A7405018.jpg",
|
||||
"DocumentMoniker": "C:\\Users\\HP\\Desktop\\Website\\A7405018.jpg",
|
||||
"ToolTip": "C:\\Users\\HP\\Desktop\\Website\\A7405018.jpg",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001533|",
|
||||
"WhenOpened": "2026-05-23T05:52:56.265Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 9,
|
||||
"Title": "SettingsService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\SettingsService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\SettingsService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\SettingsService.cs",
|
||||
"RelativeToolTip": "Services\\SettingsService.cs",
|
||||
"ViewState": "AgIAAE0BAAAAAAAAAAAAAC0BAAAMAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-21T17:37:24.777Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 7,
|
||||
"Title": "Settings.razor",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Settings.razor",
|
||||
"RelativeDocumentMoniker": "Components\\Pages\\Settings.razor",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Settings.razor",
|
||||
"RelativeToolTip": "Components\\Pages\\Settings.razor",
|
||||
"ViewState": "AgIAAJsAAAAAAAAAAAAAAKwAAAAWAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
|
||||
"WhenOpened": "2026-05-15T16:43:26.6Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 1,
|
||||
"Title": "Editor.razor",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Editor.razor",
|
||||
"RelativeDocumentMoniker": "Components\\Pages\\Editor.razor",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Editor.razor",
|
||||
"RelativeToolTip": "Components\\Pages\\Editor.razor",
|
||||
"ViewState": "AgIAAFsCAAAAAAAAAAAiwEgDAAAkAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
|
||||
"WhenOpened": "2026-05-16T13:01:59.743Z",
|
||||
"EditorCaption": ""
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 2,
|
||||
"Title": "ProjectService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ProjectService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\ProjectService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ProjectService.cs",
|
||||
"RelativeToolTip": "Services\\ProjectService.cs",
|
||||
"ViewState": "AgIAAN0AAAAAAAAAAAApwO0AAAAMAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-15T15:56:55.229Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 3,
|
||||
"Title": "OllamaAssistant.razor",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\OllamaAssistant.razor",
|
||||
"RelativeDocumentMoniker": "Components\\Pages\\OllamaAssistant.razor",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\OllamaAssistant.razor",
|
||||
"RelativeToolTip": "Components\\Pages\\OllamaAssistant.razor",
|
||||
"ViewState": "AgIAANoAAAAAAAAAAAAiwPIAAAAZAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
|
||||
"WhenOpened": "2026-05-20T05:14:39.037Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 5,
|
||||
"Title": "MainLayout.razor",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\MainLayout.razor",
|
||||
"RelativeDocumentMoniker": "Components\\Layout\\MainLayout.razor",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\MainLayout.razor",
|
||||
"RelativeToolTip": "Components\\Layout\\MainLayout.razor",
|
||||
"ViewState": "AgIAACABAAAAAAAAAAApwDQBAAARAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
|
||||
"WhenOpened": "2026-05-15T16:04:36.444Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 8,
|
||||
"Title": "UserEntity.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\UserEntity.cs",
|
||||
"RelativeDocumentMoniker": "Data\\UserEntity.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\UserEntity.cs",
|
||||
"RelativeToolTip": "Data\\UserEntity.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAsAAAAVAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-24T04:52:42.227Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 6,
|
||||
"Title": "Program.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Program.cs",
|
||||
"RelativeDocumentMoniker": "Program.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Program.cs",
|
||||
"RelativeToolTip": "Program.cs",
|
||||
"ViewState": "AgIAACwAAAAAAAAAAAAQwD8AAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-20T04:07:21.54Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 10,
|
||||
"Title": "IThemeService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IThemeService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\IThemeService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IThemeService.cs",
|
||||
"RelativeToolTip": "Services\\IThemeService.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAqwAgAAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-21T17:32:01.784Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 16,
|
||||
"Title": "RedirectToLogin.razor",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\RedirectToLogin.razor",
|
||||
"RelativeDocumentMoniker": "Components\\RedirectToLogin.razor",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\RedirectToLogin.razor",
|
||||
"RelativeToolTip": "Components\\RedirectToLogin.razor",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAsAAAAIAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
|
||||
"WhenOpened": "2026-05-21T16:31:16.612Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 11,
|
||||
"Title": "ISettingsService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ISettingsService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\ISettingsService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ISettingsService.cs",
|
||||
"RelativeToolTip": "Services\\ISettingsService.cs",
|
||||
"ViewState": "AgIAABwAAAAAAAAAAAA9wC4AAAAZAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-21T16:35:36.655Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 12,
|
||||
"Title": "IRagService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IRagService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\IRagService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IRagService.cs",
|
||||
"RelativeToolTip": "Services\\IRagService.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAsAAAAVAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-18T15:37:44.565Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 17,
|
||||
"Title": "Login.razor",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Login.razor",
|
||||
"RelativeDocumentMoniker": "Components\\Pages\\Login.razor",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Login.razor",
|
||||
"RelativeToolTip": "Components\\Pages\\Login.razor",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
|
||||
"WhenOpened": "2026-05-21T08:47:41.996Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 14,
|
||||
"Title": "ThemeService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ThemeService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\ThemeService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ThemeService.cs",
|
||||
"RelativeToolTip": "Services\\ThemeService.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAABEAAAAdAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-16T13:35:33.991Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 15,
|
||||
"Title": "logo.jpg",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Assets\\logo.jpg",
|
||||
"RelativeDocumentMoniker": "Assets\\logo.jpg",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Assets\\logo.jpg",
|
||||
"RelativeToolTip": "Assets\\logo.jpg",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001533|",
|
||||
"WhenOpened": "2026-05-21T17:15:10.852Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 19,
|
||||
"Title": "OllamaSettings.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\OllamaSettings.cs",
|
||||
"RelativeDocumentMoniker": "Models\\OllamaSettings.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\OllamaSettings.cs",
|
||||
"RelativeToolTip": "Models\\OllamaSettings.cs",
|
||||
"ViewState": "AgIAAAYAAAAAAAAAAAAkwBQAAAASAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-16T13:39:46.179Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 24,
|
||||
"Title": "AppDbContext.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\AppDbContext.cs",
|
||||
"RelativeDocumentMoniker": "Data\\AppDbContext.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\AppDbContext.cs",
|
||||
"RelativeToolTip": "Data\\AppDbContext.cs",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-14T10:50:13.284Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 20,
|
||||
"Title": "FileScannerService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\FileScannerService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\FileScannerService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\FileScannerService.cs",
|
||||
"RelativeToolTip": "Services\\FileScannerService.cs",
|
||||
"ViewState": "AgIAABIAAAAAAAAAAAAQwCUAAAAWAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-16T06:02:53.46Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 18,
|
||||
"Title": "TotpHelper.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\TotpHelper.cs",
|
||||
"RelativeDocumentMoniker": "Services\\TotpHelper.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\TotpHelper.cs",
|
||||
"RelativeToolTip": "Services\\TotpHelper.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-21T08:20:49.461Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 21,
|
||||
"Title": "Project.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\Project.cs",
|
||||
"RelativeDocumentMoniker": "Models\\Project.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\Project.cs",
|
||||
"RelativeToolTip": "Models\\Project.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAYAAAAaAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-15T15:57:10.2Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 22,
|
||||
"Title": "RagService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\RagService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\RagService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\RagService.cs",
|
||||
"RelativeToolTip": "Services\\RagService.cs",
|
||||
"ViewState": "AgIAABIAAAAAAAAAAAAxwBoAAAAIAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-18T15:40:05.413Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 23,
|
||||
"Title": "NavMenu.razor",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\NavMenu.razor",
|
||||
"RelativeDocumentMoniker": "Components\\Layout\\NavMenu.razor",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\NavMenu.razor",
|
||||
"RelativeToolTip": "Components\\Layout\\NavMenu.razor",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
|
||||
"WhenOpened": "2026-05-15T16:04:37.739Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 25,
|
||||
"Title": "App.razor",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\App.razor",
|
||||
"RelativeDocumentMoniker": "Components\\App.razor",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\App.razor",
|
||||
"RelativeToolTip": "Components\\App.razor",
|
||||
"ViewState": "AgIAAAYAAAAAAAAAAAAAABgAAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
|
||||
"WhenOpened": "2026-05-16T05:42:06.383Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 28,
|
||||
"Title": "Models.razor",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Models.razor",
|
||||
"RelativeDocumentMoniker": "Components\\Pages\\Models.razor",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Models.razor",
|
||||
"RelativeToolTip": "Components\\Pages\\Models.razor",
|
||||
"ViewState": "AgIAAGgAAAAAAAAAAAApwHQAAAAtAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
|
||||
"WhenOpened": "2026-05-16T12:51:13.798Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 26,
|
||||
"Title": "LLM_Model.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\LLM_Model.cs",
|
||||
"RelativeDocumentMoniker": "Models\\LLM_Model.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\LLM_Model.cs",
|
||||
"RelativeToolTip": "Models\\LLM_Model.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAqwBQAAAAzAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-16T12:53:35.486Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 27,
|
||||
"Title": "ModelStatus.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\ModelStatus.cs",
|
||||
"RelativeDocumentMoniker": "Models\\ModelStatus.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\ModelStatus.cs",
|
||||
"RelativeToolTip": "Models\\ModelStatus.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAABEAAAADAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-16T13:01:24.508Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 31,
|
||||
"Title": "MainLayout.razor.css",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\MainLayout.razor.css",
|
||||
"RelativeDocumentMoniker": "Components\\Layout\\MainLayout.razor.css",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\MainLayout.razor.css",
|
||||
"RelativeToolTip": "Components\\Layout\\MainLayout.razor.css",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003000|",
|
||||
"WhenOpened": "2026-05-15T16:04:50.455Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 29,
|
||||
"Title": "IFileScannerService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IFileScannerService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\IFileScannerService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IFileScannerService.cs",
|
||||
"RelativeToolTip": "Services\\IFileScannerService.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAIAAAAeAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-16T06:02:56.726Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 30,
|
||||
"Title": "bootstrap-utilities.css",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\wwwroot\\lib\\bootstrap\\dist\\css\\bootstrap-utilities.css",
|
||||
"RelativeDocumentMoniker": "wwwroot\\lib\\bootstrap\\dist\\css\\bootstrap-utilities.css",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\wwwroot\\lib\\bootstrap\\dist\\css\\bootstrap-utilities.css",
|
||||
"RelativeToolTip": "wwwroot\\lib\\bootstrap\\dist\\css\\bootstrap-utilities.css",
|
||||
"ViewState": "AgIAAOgIAAAAAAAAAAAAAPYIAAAIAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003000|",
|
||||
"WhenOpened": "2026-05-15T16:28:43.874Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 33,
|
||||
"Title": "ProjectEntity.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\ProjectEntity.cs",
|
||||
"RelativeDocumentMoniker": "Data\\ProjectEntity.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\ProjectEntity.cs",
|
||||
"RelativeToolTip": "Data\\ProjectEntity.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-14T10:50:06.549Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 32,
|
||||
"Title": "IProjectService.cs",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IProjectService.cs",
|
||||
"RelativeDocumentMoniker": "Services\\IProjectService.cs",
|
||||
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IProjectService.cs",
|
||||
"RelativeToolTip": "Services\\IProjectService.cs",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAbAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||
"WhenOpened": "2026-05-14T10:46:30.166Z"
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 34,
|
||||
"Title": "AuthorBuddy.Web.csproj",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\AuthorBuddy.Web.csproj",
|
||||
"RelativeDocumentMoniker": "AuthorBuddy.Web.csproj",
|
||||
|
|
@ -578,11 +38,12 @@
|
|||
"RelativeToolTip": "AuthorBuddy.Web.csproj",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000758|",
|
||||
"WhenOpened": "2026-05-14T10:14:30.128Z"
|
||||
"WhenOpened": "2026-05-14T10:14:30.128Z",
|
||||
"EditorCaption": ""
|
||||
},
|
||||
{
|
||||
"$type": "Document",
|
||||
"DocumentIndex": 35,
|
||||
"DocumentIndex": 1,
|
||||
"Title": "appsettings.json",
|
||||
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\appsettings.json",
|
||||
"RelativeDocumentMoniker": "appsettings.json",
|
||||
|
|
@ -590,7 +51,8 @@
|
|||
"RelativeToolTip": "appsettings.json",
|
||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001642|",
|
||||
"WhenOpened": "2026-05-14T10:14:25.605Z"
|
||||
"WhenOpened": "2026-05-14T10:14:25.605Z",
|
||||
"EditorCaption": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
BIN
Assets/logo.jpg
BIN
Assets/logo.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 181 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 202 KiB |
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
|
|
@ -8,14 +8,15 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DiffPlex" Version="1.9.0" />
|
||||
<PackageReference Include="DocumentFormat.OpenXml" Version="3.5.1" />
|
||||
<PackageReference Include="LiteDB" Version="5.0.21" />
|
||||
<PackageReference Include="QuestPDF" Version="2026.5.0" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.8" />
|
||||
<PackageReference Include="Ollama" Version="1.15.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="Resources\**" CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Update="vec0.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
<Solution>
|
||||
<Project Path="AuthorBuddy.Web.csproj" />
|
||||
</Solution>
|
||||
|
|
@ -15,13 +15,9 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<CascadingAuthenticationState>
|
||||
<Routes @rendermode="InteractiveServer" />
|
||||
</CascadingAuthenticationState>
|
||||
<Routes />
|
||||
<ReconnectModal />
|
||||
<script src="@Assets["_framework/blazor.web.js"]"></script>
|
||||
<script src="@Assets["js/editor-toolbar.js"]"></script>
|
||||
<script src="@Assets["js/download.js"]"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,118 +0,0 @@
|
|||
@using AuthorBuddy.Web.Models
|
||||
@inject IProjectService ProjectService
|
||||
|
||||
<div class="category-selector" @onclick:stopPropagation>
|
||||
@foreach (var cat in _allCategories)
|
||||
{
|
||||
var isActive = (Current & cat) == cat;
|
||||
var disabled = _disabledCategories.Contains(cat);
|
||||
<button class="category-btn @(isActive ? "active" : "") @(disabled ? "disabled" : "")"
|
||||
@onclick="() => ToggleCategory(cat)"
|
||||
disabled="@disabled"
|
||||
title="@CategoryName(cat)">
|
||||
@CategoryIcon(cat)
|
||||
<span class="cat-label">@CategoryName(cat)</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.category-selector {
|
||||
display: inline-flex;
|
||||
gap: 3px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.category-btn {
|
||||
font-size: 11px;
|
||||
padding: 1px 6px;
|
||||
border: 1px solid var(--border-color, #ccc);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-card, #f5f5f5);
|
||||
color: var(--text-main, #333);
|
||||
cursor: pointer;
|
||||
line-height: 1.5;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.category-btn.active {
|
||||
background: var(--accent-color, #4a90d9);
|
||||
color: #fff;
|
||||
border-color: var(--accent-color, #4a90d9);
|
||||
}
|
||||
.category-btn.disabled {
|
||||
opacity: 0.35;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.category-btn:not(.disabled):hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
.cat-label {
|
||||
font-size: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private static readonly FileCategory[] _allCategories = new[]
|
||||
{
|
||||
FileCategory.Character,
|
||||
FileCategory.Exposee,
|
||||
FileCategory.Fact,
|
||||
FileCategory.Document
|
||||
};
|
||||
|
||||
private HashSet<FileCategory> _disabledCategories = new();
|
||||
|
||||
[Parameter] public FileCategory Current { get; set; }
|
||||
[Parameter] public int ProjectId { get; set; }
|
||||
[Parameter] public string FilePath { get; set; } = string.Empty;
|
||||
[Parameter] public EventCallback<FileCategory> OnCategoryChanged { get; set; }
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
UpdateDisabled();
|
||||
}
|
||||
|
||||
private void UpdateDisabled()
|
||||
{
|
||||
_disabledCategories.Clear();
|
||||
foreach (var cat in _allCategories)
|
||||
{
|
||||
if (cat == Current) continue;
|
||||
var combined = Current | cat;
|
||||
if (!FileCategoryHelper.IsValid(combined))
|
||||
_disabledCategories.Add(cat);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ToggleCategory(FileCategory cat)
|
||||
{
|
||||
if (_disabledCategories.Contains(cat)) return;
|
||||
|
||||
var newCategory = Current.HasFlag(cat) ? Current & ~cat : Current | cat;
|
||||
|
||||
if (!FileCategoryHelper.IsValid(newCategory)) return;
|
||||
|
||||
await ProjectService.SetFileCategoryAsync(ProjectId, FilePath, newCategory);
|
||||
await OnCategoryChanged.InvokeAsync(newCategory);
|
||||
}
|
||||
|
||||
private static string CategoryName(FileCategory cat) => cat switch
|
||||
{
|
||||
FileCategory.Character => "Charakter",
|
||||
FileCategory.Exposee => "Exposé",
|
||||
FileCategory.Fact => "Fakt",
|
||||
FileCategory.Document => "Dokument",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
private static string CategoryIcon(FileCategory cat) => cat switch
|
||||
{
|
||||
FileCategory.Character => "👤",
|
||||
FileCategory.Exposee => "📋",
|
||||
FileCategory.Fact => "📌",
|
||||
FileCategory.Document => "📄",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
@inject IJSRuntime JS
|
||||
@inject ISettingsService SettingsService
|
||||
@inject IProjectService ProjectService
|
||||
|
||||
<div class="copy-create-toolbar">
|
||||
<button class="btn btn-sm" @onclick="CopyToClipboard" title="In Zwischenablage kopieren">📋 Kopieren</button>
|
||||
<button class="btn btn-sm" @onclick="ShowCreateDialog" title="Als neue .md Datei speichern">📄 Als MD speichern</button>
|
||||
</div>
|
||||
|
||||
@if (showCreateDialog)
|
||||
{
|
||||
<div class="modal-overlay">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-title">Neue .md Datei erstellen</div>
|
||||
<div class="modal-body">
|
||||
@if (!string.IsNullOrEmpty(_projectPath))
|
||||
{
|
||||
<div class="form-group">
|
||||
<label>Zielverzeichnis:</label>
|
||||
<span style="font-size:11px;color:var(--border-color);word-break:break-all;">@_projectPath</span>
|
||||
</div>
|
||||
}
|
||||
<div class="form-group">
|
||||
<label>Dateiname:</label>
|
||||
<input @bind="newFileName" placeholder="Dateiname (ohne .md)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button @onclick="CancelCreate">Abbrechen</button>
|
||||
<button @onclick="CreateFile" class="primary" disabled="@(string.IsNullOrWhiteSpace(newFileName))">Erstellen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<style>
|
||||
.copy-create-toolbar {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
margin-left: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.copy-create-toolbar .btn-sm {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Text { get; set; } = string.Empty;
|
||||
[Parameter] public string? DefaultFileName { get; set; }
|
||||
[Parameter] public EventCallback<string> OnFileCreated { get; set; }
|
||||
|
||||
private bool showCreateDialog;
|
||||
private string newFileName = string.Empty;
|
||||
private int? _projectId;
|
||||
private string? _projectPath;
|
||||
private bool _isCopyDisabled;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_projectId = SettingsService.CurrentProjectId;
|
||||
if (_projectId.HasValue && _projectId > 0)
|
||||
{
|
||||
var project = await ProjectService.GetProjectAsync(_projectId.Value);
|
||||
_projectPath = project?.RootPath;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(DefaultFileName))
|
||||
newFileName = DefaultFileName;
|
||||
}
|
||||
|
||||
private async Task CopyToClipboard()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Text) || _isCopyDisabled) return;
|
||||
_isCopyDisabled = true;
|
||||
try
|
||||
{
|
||||
await JS.InvokeVoidAsync("navigator.clipboard.writeText", Text);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback for older browsers
|
||||
try
|
||||
{
|
||||
var textArea = System.Text.Encodings.Web.JavaScriptEncoder.Default.Encode(Text);
|
||||
await JS.InvokeVoidAsync("eval", $"navigator.clipboard.writeText('{textArea}')");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isCopyDisabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowCreateDialog()
|
||||
{
|
||||
newFileName = DefaultFileName ?? string.Empty;
|
||||
showCreateDialog = true;
|
||||
}
|
||||
|
||||
private void CancelCreate()
|
||||
{
|
||||
showCreateDialog = false;
|
||||
newFileName = string.Empty;
|
||||
}
|
||||
|
||||
private async Task CreateFile()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(newFileName) || string.IsNullOrEmpty(Text)) return;
|
||||
|
||||
try
|
||||
{
|
||||
var fileName = newFileName.Trim();
|
||||
if (!fileName.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
|
||||
fileName += ".md";
|
||||
|
||||
string savePath;
|
||||
if (!string.IsNullOrEmpty(_projectPath) && Directory.Exists(_projectPath))
|
||||
{
|
||||
savePath = Path.Combine(_projectPath, fileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
savePath = Path.Combine(Directory.GetCurrentDirectory(), fileName);
|
||||
}
|
||||
|
||||
if (File.Exists(savePath))
|
||||
{
|
||||
var dir = Path.GetDirectoryName(savePath)!;
|
||||
var name = Path.GetFileNameWithoutExtension(savePath);
|
||||
var ext = Path.GetExtension(savePath);
|
||||
var counter = 1;
|
||||
do
|
||||
{
|
||||
savePath = Path.Combine(dir, $"{name}_{counter}{ext}");
|
||||
counter++;
|
||||
} while (File.Exists(savePath));
|
||||
}
|
||||
|
||||
await File.WriteAllTextAsync(savePath, Text);
|
||||
showCreateDialog = false;
|
||||
await OnFileCreated.InvokeAsync(Path.GetFileName(savePath));
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
@using DiffPlex
|
||||
@using DiffPlex.DiffBuilder
|
||||
@using DiffPlex.DiffBuilder.Model
|
||||
|
||||
<div class="diff-view">
|
||||
@if (lines is not null)
|
||||
{
|
||||
@foreach (var line in lines.Lines)
|
||||
{
|
||||
<div class="diff-line diff-@line.Type.ToString().ToLowerInvariant()">
|
||||
<span class="diff-line-num">@line.Position</span>
|
||||
<span class="diff-line-text">@line.Text</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string Original { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string Corrected { get; set; } = string.Empty;
|
||||
|
||||
private DiffPaneModel? lines;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
lines = InlineDiffBuilder.Diff(Original, Corrected);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,584 +1,112 @@
|
|||
@inherits LayoutComponentBase
|
||||
@implements IDisposable
|
||||
@inject IProjectService ProjectService
|
||||
@inject ISettingsService SettingsService
|
||||
@inject IThemeService ThemeService
|
||||
@inject IOllamaSetupService SetupService
|
||||
@inject OllamaSettings OllamaSettings
|
||||
@inject IAuthService AuthService
|
||||
@inject CustomAuthStateProvider AuthStateProvider
|
||||
@inject ILocalizationService Loc
|
||||
@inject OperationState OpState
|
||||
@using AuthorBuddy.Web.Models
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<div class="splash-screen">
|
||||
<div class="splash-bg" style="background-image:url('/Assets/splash.jpg')"></div>
|
||||
<div class="splash-content">
|
||||
<div class="splash-logo">
|
||||
<img src="/Assets/logo.jpg" alt="Author Buddy" />
|
||||
</div>
|
||||
<div class="splash-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="page">
|
||||
<div class="page">
|
||||
<div class="main-content">
|
||||
@if (_isAuthenticated)
|
||||
{
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-logo">
|
||||
<img src="/Assets/logo.jpg" alt="Author Buddy" />
|
||||
</div>
|
||||
<div class="nav-section">
|
||||
<button class="nav-section-header" @onclick='() => ToggleSection("file")'>
|
||||
<span class="nav-icon">@(activeSection == "file" ? "▼" : "▶")</span>
|
||||
@Loc["nav.menu.file"]
|
||||
Datei
|
||||
</button>
|
||||
@if (activeSection == "file")
|
||||
{
|
||||
<div class="nav-section-body">
|
||||
<button class="nav-item" @onclick="CreateNewProject">@Loc["nav.menu.file.new_project"]</button>
|
||||
<button class="nav-item" @onclick="OpenEditor">@Loc["nav.menu.file.open_editor"]</button>
|
||||
<button class="nav-item" @onclick="CreateNewProject">Neues Projekt</button>
|
||||
<button class="nav-item" @onclick="OpenProject">Öffnen</button>
|
||||
<div class="nav-separator"></div>
|
||||
<button class="nav-item" @onclick="ExitApp">@Loc["nav.menu.file.exit"]</button>
|
||||
<button class="nav-item" @onclick="ExitApp">Beenden</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="nav-section">
|
||||
<button class="nav-section-header" @onclick='() => ToggleSection("view")'>
|
||||
<span class="nav-icon">@(activeSection == "view" ? "▼" : "▶")</span>
|
||||
Ansicht
|
||||
</button>
|
||||
@if (activeSection == "view")
|
||||
{
|
||||
<div class="nav-section-body">
|
||||
<button class="nav-item" @onclick="OpenEditor">Editor öffnen</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="nav-section">
|
||||
<button class="nav-section-header" @onclick='() => ToggleSection("extras")'>
|
||||
<span class="nav-icon">@(activeSection == "extras" ? "▼" : "▶")</span>
|
||||
@Loc["nav.menu.extras"]
|
||||
Extras
|
||||
</button>
|
||||
@if (activeSection == "extras")
|
||||
{
|
||||
<div class="nav-section-body">
|
||||
<button class="nav-item" @onclick='() => NavigateTo("/settings")'>@Loc["nav.menu.extras.settings"]</button>
|
||||
@if (_currentUserRole == UserRole.Administrator)
|
||||
{
|
||||
<button class="nav-item" @onclick='() => NavigateTo("/models")'>@Loc["nav.menu.extras.manage_models"]</button>
|
||||
<button class="nav-item" @onclick='() => NavigateTo("/assistant")'>@Loc["nav.menu.extras.assistant"]</button>
|
||||
<button class="nav-item" @onclick='() => NavigateTo("/setup-assistant")'>Setup Assistent</button>
|
||||
<div class="nav-separator"></div>
|
||||
<button class="nav-item" @onclick='() => NavigateTo("/user-management")'>@Loc["nav.menu.extras.user_management"]</button>
|
||||
<button class="nav-item" @onclick='() => NavigateTo("/db-admin")'>📊 DB-Admin</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="nav-item" @onclick='() => NavigateTo("/assistant")'>@Loc["nav.menu.extras.assistant"]</button>
|
||||
}
|
||||
<button class="nav-item" @onclick='() => NavigateTo("/settings")'>Einstellungen</button>
|
||||
<button class="nav-item" @onclick='() => NavigateTo("/models")'>Modelle verwalten</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="nav-section">
|
||||
<button class="nav-section-header" @onclick='() => ToggleSection("projects")'>
|
||||
<span class="nav-icon">@(activeSection == "projects" ? "▼" : "▶")</span>
|
||||
@Loc["nav.menu.projects"]
|
||||
Projekte
|
||||
</button>
|
||||
@if (activeSection == "projects")
|
||||
{
|
||||
<div class="nav-section-body">
|
||||
<button class="nav-item nav-item-toggle" @onclick='() => showProjectList = !showProjectList'>
|
||||
<span class="nav-icon">@(showProjectList ? "▼" : "▶")</span>
|
||||
@Loc["nav.projects.list_title"]
|
||||
</button>
|
||||
@if (showProjectList)
|
||||
{
|
||||
<div class="nav-sublist">
|
||||
@if (projects == null)
|
||||
{
|
||||
<span class="nav-hint">@Loc["nav.projects.loading"]</span>
|
||||
<span class="nav-hint">Lade Projekte...</span>
|
||||
}
|
||||
else if (projects.Count == 0)
|
||||
{
|
||||
<span class="nav-hint">@Loc["nav.projects.empty"]</span>
|
||||
<span class="nav-hint">Keine Projekte vorhanden.</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var p in projects)
|
||||
{
|
||||
<div class="nav-item nav-item-sub @(p.Id == currentProjectId ? "active" : "")">
|
||||
<button class="nav-item-text" @onclick='() => SelectProject(p)'>@p.Name</button>
|
||||
<button class="nav-item-icon" @onclick='() => BeginCopyProject(p)' title="@Loc["nav.projects.copy"]">⧉</button>
|
||||
</div>
|
||||
<button class="nav-item" @onclick='() => SelectProject(p)'>@p.Name</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="nav-separator"></div>
|
||||
<button class="nav-item" @onclick="CreateNewProject">@Loc["nav.projects.create"]</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (currentProjectId > 0 && fileTree != null)
|
||||
{
|
||||
<div class="nav-section nav-section-open">
|
||||
<div class="nav-section-header">
|
||||
<span>@Loc["nav.projects.files"]</span>
|
||||
<span class="nav-header-actions">
|
||||
<button class="nav-item-icon" @onclick="RefreshFileTree" title="@Loc["nav.projects.refresh_tree"]">↻</button>
|
||||
@if (_selectedFiles.Count > 0)
|
||||
{
|
||||
<button class="nav-item-icon" @onclick="() => _showExportDialog = true" title="Export (@(_selectedFiles.Count))">⬇</button>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div class="nav-section-body">
|
||||
@foreach (var item in fileTree)
|
||||
{
|
||||
var cat = item.Category;
|
||||
<div class="tree-item @(item.IsDirectory ? "dir" : "file") @(item.FullPath == currentFilePath ? "active" : "")"
|
||||
@onclick="() => OpenFile(item)">
|
||||
@if (!item.IsDirectory)
|
||||
{
|
||||
<input type="checkbox" class="tree-checkbox"
|
||||
checked="@_selectedFiles.Contains(item.FullPath)"
|
||||
@onclick:stopPropagation
|
||||
@onchange="() => ToggleFileSelection(item.FullPath)" />
|
||||
}
|
||||
<span class="tree-icon">@(item.IsDirectory ? "📁" : "📄")</span>
|
||||
<span class="tree-name">@item.Name</span>
|
||||
@if (cat != FileCategory.None)
|
||||
{
|
||||
<span class="tree-category-badge @FileCategoryHelper.CssClass(cat)" title="@FileCategoryHelper.DisplayName(cat)">@FileCategoryHelper.DisplayIcon(cat)</span>
|
||||
}
|
||||
@if (item.IsDirectory)
|
||||
{
|
||||
<button class="tree-rename" @onclick="() => BeginNewFile(item)" @onclick:stopPropagation title="@Loc["nav.projects.file.new"]">+</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="tree-cat-btn" @onclick="() => ToggleCategoryEditor(item)" @onclick:stopPropagation title="Kategorie">🏷</button>
|
||||
<button class="tree-rename" @onclick="() => BeginRename(item)" @onclick:stopPropagation title="@Loc["nav.projects.file.rename"]">✎</button>
|
||||
}
|
||||
</div>
|
||||
@if (_editingCategoryPath == item.FullPath)
|
||||
{
|
||||
<div class="tree-cat-editor" @onclick:stopPropagation>
|
||||
<CategorySelector Current="@item.Category" ProjectId="@currentProjectId" FilePath="@item.FullPath" OnCategoryChanged="@(v => OnCategoryChanged(item, v))" />
|
||||
<button class="tree-cat-close" @onclick="() => _editingCategoryPath = null">✕</button>
|
||||
</div>
|
||||
}
|
||||
@foreach (var child in item.Children)
|
||||
{
|
||||
var ccat = child.Category;
|
||||
<div class="tree-item file tree-child @(child.FullPath == currentFilePath ? "active" : "")"
|
||||
@onclick="() => OpenFile(child)">
|
||||
<input type="checkbox" class="tree-checkbox"
|
||||
checked="@_selectedFiles.Contains(child.FullPath)"
|
||||
@onclick:stopPropagation
|
||||
@onchange="() => ToggleFileSelection(child.FullPath)" />
|
||||
<span class="tree-icon">📄</span>
|
||||
<span class="tree-name">@child.Name</span>
|
||||
@if (ccat != FileCategory.None)
|
||||
{
|
||||
<span class="tree-category-badge @FileCategoryHelper.CssClass(ccat)" title="@FileCategoryHelper.DisplayName(ccat)">@FileCategoryHelper.DisplayIcon(ccat)</span>
|
||||
}
|
||||
<button class="tree-cat-btn" @onclick="() => ToggleCategoryEditor(child)" @onclick:stopPropagation title="Kategorie">🏷</button>
|
||||
<button class="tree-rename" @onclick="() => BeginRename(child)" @onclick:stopPropagation title="@Loc["nav.projects.file.rename"]">✎</button>
|
||||
</div>
|
||||
@if (_editingCategoryPath == child.FullPath)
|
||||
{
|
||||
<div class="tree-cat-editor" @onclick:stopPropagation>
|
||||
<CategorySelector Current="@child.Category" ProjectId="@currentProjectId" FilePath="@child.FullPath" OnCategoryChanged="@(v => OnCategoryChanged(child, v))" />
|
||||
<button class="tree-cat-close" @onclick="() => _editingCategoryPath = null">✕</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (_showExportDialog)
|
||||
{
|
||||
<div class="modal-overlay">
|
||||
<div class="modal-dialog" style="min-width:360px;">
|
||||
<div class="modal-title">@Loc["export.title"]</div>
|
||||
<div class="modal-body">
|
||||
<p style="margin-bottom:12px;">@Loc["export.file_count"]: <strong>@_selectedFiles.Count</strong></p>
|
||||
<div class="form-group" style="margin-bottom:12px;">
|
||||
<label>@Loc["export.label.filename"]</label>
|
||||
<input @bind="exportFileName" placeholder="@Loc["export.placeholder.filename"]" />
|
||||
<small class="form-hint">@Loc["export.hint.filename"]</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>@Loc["export.label.format"]</label>
|
||||
<select class="form-select" @bind="exportFormat">
|
||||
<option value="Html">HTML (.html)</option>
|
||||
<option value="Markdown">@Loc["export.format.markdown"] (.md)</option>
|
||||
<option value="Zip">@Loc["export.format.zip"] (.zip)</option>
|
||||
<option value="Docx">DOCX (.docx)</option>
|
||||
<option value="Pdf">PDF (.pdf)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button @onclick='() => _showExportDialog = false'>@Loc["common.cancel"]</button>
|
||||
<button @onclick="DoExport" class="primary">@Loc["export.button.export"]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (showRenameDialog)
|
||||
{
|
||||
<div class="modal-overlay">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-title">@Loc["nav.rename.title"]</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>@Loc["nav.rename.label.new_name"]</label>
|
||||
<input @bind="renameNewName" placeholder="@Loc["nav.rename.placeholder"]" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button @onclick="CancelRename">@Loc["common.cancel"]</button>
|
||||
<button @onclick="ConfirmRename" disabled="@(string.IsNullOrWhiteSpace(renameNewName))" class="primary">@Loc["nav.rename.button.confirm"]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (showCopyProjectDialog)
|
||||
{
|
||||
<div class="modal-overlay">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-title">@Loc["nav.copy_project.title"]</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>@Loc["nav.copy_project.label.name"]</label>
|
||||
<input @bind="copyProjectName" placeholder="@Loc["nav.copy_project.placeholder.name"]" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>@Loc["nav.copy_project.label.path"]</label>
|
||||
<input @bind="copyProjectPath" placeholder="@Loc["nav.copy_project.placeholder.path"]" />
|
||||
<small class="form-hint">Erlaubter Pfad: @ProjectPathHelper.AllowedBasePath</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button @onclick="CancelCopyProject">@Loc["common.cancel"]</button>
|
||||
<button @onclick="ConfirmCopyProject" disabled="@(string.IsNullOrWhiteSpace(copyProjectName) || string.IsNullOrWhiteSpace(copyProjectPath))" class="primary">@Loc["nav.copy_project.button.copy"]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (showNewFileDialog)
|
||||
{
|
||||
<div class="modal-overlay">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-title">@Loc["nav.newfile.title"]</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>@Loc["nav.newfile.label.filename"]</label>
|
||||
<input @bind="newFileName" placeholder="@Loc["nav.newfile.placeholder"]" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button @onclick="CancelNewFile">@Loc["common.cancel"]</button>
|
||||
<button @onclick="ConfirmNewFile" disabled="@(string.IsNullOrWhiteSpace(newFileName))" class="primary">@Loc["nav.newfile.button.create"]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(_username))
|
||||
{
|
||||
<div class="nav-section" style="border-top:1px solid var(--border-color);margin-top:8px;padding-top:4px;">
|
||||
<div class="nav-section-header" style="cursor:default;">
|
||||
<span style="font-size:11px;font-weight:normal;">@Loc["nav.user.logged_in_as"] @_username</span>
|
||||
</div>
|
||||
<div class="nav-section-body">
|
||||
<button class="nav-item" @onclick="NavigateToSetup2FA">@Loc["nav.user.manage_2fa"]</button>
|
||||
<button class="nav-item" @onclick="Logout">@Loc["nav.user.logout"]</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="sidebar-spacer"></div>
|
||||
<div class="sidebar-bottom-btns">
|
||||
<button class="sidebar-info-btn" @onclick="ToggleInfoOverlay" title="Info">i</button>
|
||||
<button class="sidebar-info-btn" @onclick='() => Navigation.NavigateTo("/help")' title="@Loc["nav.menu.extras.help"]">?</button>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
}
|
||||
|
||||
<main class="detail-panel">
|
||||
@if (currentProjectId > 0)
|
||||
{
|
||||
<div class="main-header">
|
||||
<h2>@currentProjectName</h2>
|
||||
</div>
|
||||
}
|
||||
@Body
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer class="status-bar">
|
||||
<span class="status-dot connected"></span>
|
||||
<span class="status-text">@OpState.StatusText</span>
|
||||
@if (OpState.IsBusy)
|
||||
<span class="status-text">@statusText</span>
|
||||
@if (isBusy)
|
||||
{
|
||||
<progress value="@OpState.Progress" max="100"></progress>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(OpState.ThoughtText))
|
||||
{
|
||||
<span class="status-thought">@OpState.ThoughtText</span>
|
||||
<progress value="@operationProgress" max="100"></progress>
|
||||
}
|
||||
</footer>
|
||||
</div>
|
||||
}
|
||||
|
||||
<style>
|
||||
.tree-category-badge {
|
||||
font-size: 11px;
|
||||
margin-left: 4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.tree-cat-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
padding: 0 2px;
|
||||
opacity: 0.5;
|
||||
line-height: 1;
|
||||
}
|
||||
.tree-cat-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.tree-cat-editor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px 4px 24px;
|
||||
background: var(--bg-card, #f0f0f0);
|
||||
border-bottom: 1px solid var(--border-color, #ddd);
|
||||
}
|
||||
.tree-cat-close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted, #999);
|
||||
padding: 0 4px;
|
||||
}
|
||||
.tree-cat-close:hover {
|
||||
color: var(--text-main, #333);
|
||||
}
|
||||
</style>
|
||||
|
||||
@if (_showInfoOverlay)
|
||||
{
|
||||
<div class="splash-screen" @onclick="CloseInfoOverlay">
|
||||
<div class="splash-bg" style="background-image:url('/Assets/splash.jpg')"></div>
|
||||
<div class="splash-content info-content" @onclick:stopPropagation>
|
||||
<div class="splash-logo">
|
||||
<img src="/Assets/logo.jpg" alt="Author Buddy" />
|
||||
</div>
|
||||
<h1>Author Buddy</h1>
|
||||
<p class="info-version">Version 1.0</p>
|
||||
<p class="info-copyright">© 2026 Author Buddy. Alle Rechte vorbehalten.</p>
|
||||
<p class="info-homepage">
|
||||
<a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>
|
||||
</p>
|
||||
<button class="info-close-btn" @onclick="CloseInfoOverlay">Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private string _username = string.Empty;
|
||||
private bool _isAuthenticated;
|
||||
private UserRole _currentUserRole;
|
||||
private bool _isLoading = true;
|
||||
private bool _showInfoOverlay;
|
||||
private string? activeSection;
|
||||
private bool showProjectList;
|
||||
private List<Project>? projects;
|
||||
private List<FileTreeItem>? fileTree;
|
||||
private int currentProjectId;
|
||||
private string? currentProjectName;
|
||||
private string? currentFilePath;
|
||||
private string copyProjectName = string.Empty;
|
||||
private string copyProjectPath = string.Empty;
|
||||
private Project? copyProjectSource;
|
||||
private bool showRenameDialog;
|
||||
private string renameNewName = string.Empty;
|
||||
private FileTreeItem? renameTarget;
|
||||
private bool showNewFileDialog;
|
||||
private string newFileName = string.Empty;
|
||||
private FileTreeItem? newFileParentDir;
|
||||
private bool showCopyProjectDialog;
|
||||
private Action? _opStateHandler;
|
||||
private string? _editingCategoryPath;
|
||||
private HashSet<string> _selectedFiles = new();
|
||||
private bool _showExportDialog;
|
||||
private string exportFileName = string.Empty;
|
||||
private string exportFormat = "Html";
|
||||
private string statusText = "Bereit";
|
||||
private bool isBusy;
|
||||
private double operationProgress;
|
||||
|
||||
[Inject]
|
||||
private NavigationManager Navigation { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
private IJSRuntime JS { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
private IExportService ExportService { get; set; } = default!;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_opStateHandler = () => InvokeAsync(StateHasChanged);
|
||||
OpState.OnChange += _opStateHandler;
|
||||
await SettingsService.InitializeAsync();
|
||||
await Loc.InitAsync();
|
||||
OpState.SetStatus(Loc["common.status.ready"]);
|
||||
Navigation.LocationChanged += OnLocationChanged;
|
||||
|
||||
// Startup Setup Check
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
var result = await SetupService.AnalyzeSettingsAsync();
|
||||
if (!result.IsCorrect)
|
||||
{
|
||||
OpState.SetStatus("Ollama-Einstellungen suboptimal. Setup-Assistent empfohlen.", false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task LoadCurrentUser()
|
||||
{
|
||||
var user = await AuthService.GetCurrentUserAsync();
|
||||
_username = user?.Name ?? string.Empty;
|
||||
_isAuthenticated = user != null;
|
||||
_currentUserRole = user?.Role ?? UserRole.Author;
|
||||
}
|
||||
|
||||
private async Task AutoLoadLastProject()
|
||||
{
|
||||
if (currentProjectId > 0) return;
|
||||
var lastId = await SettingsService.GetLastProjectIdAsync();
|
||||
if (lastId == null) return;
|
||||
var project = projects?.FirstOrDefault(p => p.Id == lastId.Value);
|
||||
if (project != null)
|
||||
{
|
||||
SettingsService.CurrentProjectId = project.Id;
|
||||
await SettingsService.InitializeAsync();
|
||||
await SelectProject(project);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
var themeChanged = OllamaSettings.Theme != ThemeService.Theme;
|
||||
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
await ThemeService.ApplyTheme(OllamaSettings.Theme);
|
||||
|
||||
// Auto-login runs AFTER circuit establishment (correct scoped services)
|
||||
await LoadCurrentUser();
|
||||
#if DEBUG
|
||||
if (!_isAuthenticated && Program.DebugAutoLogin)
|
||||
{
|
||||
var users = await SettingsService.GetUsersAsync();
|
||||
var first = users.FirstOrDefault(u => u.Role == UserRole.Administrator);
|
||||
if (first != null)
|
||||
{
|
||||
await AuthService.AdminBypassLoginAsync(first.Id);
|
||||
AuthStateProvider.NotifyStateChanged();
|
||||
await LoadCurrentUser();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (_isAuthenticated)
|
||||
{
|
||||
await LoadProjects();
|
||||
await LoadProjectContext();
|
||||
await AutoLoadLastProject();
|
||||
}
|
||||
|
||||
_isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_opStateHandler != null)
|
||||
OpState.OnChange -= _opStateHandler;
|
||||
Navigation.LocationChanged -= OnLocationChanged;
|
||||
}
|
||||
|
||||
private async void OnLocationChanged(object? sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e)
|
||||
{
|
||||
await LoadCurrentUser();
|
||||
if (_isAuthenticated)
|
||||
{
|
||||
await LoadProjects();
|
||||
await LoadProjectContext();
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task LoadProjectContext()
|
||||
{
|
||||
currentProjectId = SettingsService.CurrentProjectId ?? 0;
|
||||
currentFilePath = SettingsService.CurrentFilePath;
|
||||
|
||||
if (currentProjectId > 0)
|
||||
{
|
||||
var project = await ProjectService.GetProjectAsync(currentProjectId);
|
||||
currentProjectName = project?.Name;
|
||||
await LoadFileTree();
|
||||
}
|
||||
else
|
||||
{
|
||||
currentProjectName = null;
|
||||
fileTree = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadFileTree()
|
||||
{
|
||||
try
|
||||
{
|
||||
fileTree = await ProjectService.GetProjectFileTreeAsync(currentProjectId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
fileTree = new();
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleCategoryEditor(FileTreeItem item)
|
||||
{
|
||||
if (_editingCategoryPath == item.FullPath)
|
||||
_editingCategoryPath = null;
|
||||
else
|
||||
_editingCategoryPath = item.FullPath;
|
||||
}
|
||||
|
||||
private void OnCategoryChanged(FileTreeItem item, FileCategory newCategory)
|
||||
{
|
||||
item.Category = newCategory;
|
||||
_editingCategoryPath = null;
|
||||
}
|
||||
|
||||
private async Task LoadProjects()
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentUser = AuthService.CurrentUser;
|
||||
int? filterUserId = currentUser?.Role == UserRole.Administrator ? null : currentUser?.Id;
|
||||
projects = await ProjectService.GetAllProjectsAsync(filterUserId);
|
||||
projects = await ProjectService.GetAllProjectsAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -600,69 +128,13 @@ else
|
|||
private void CreateNewProject()
|
||||
{
|
||||
activeSection = null;
|
||||
Navigation.NavigateTo("/editor/newproject");
|
||||
Navigation.NavigateTo("/editor?action=newproject");
|
||||
}
|
||||
|
||||
private void BeginCopyProject(Project project)
|
||||
private void OpenProject()
|
||||
{
|
||||
copyProjectSource = project;
|
||||
copyProjectName = project.Name + " (Kopie)";
|
||||
copyProjectPath = ProjectPathHelper.SuggestPath(project.Name + "_Kopie");
|
||||
showCopyProjectDialog = true;
|
||||
}
|
||||
|
||||
private void CancelCopyProject()
|
||||
{
|
||||
showCopyProjectDialog = false;
|
||||
copyProjectSource = null;
|
||||
copyProjectName = string.Empty;
|
||||
copyProjectPath = string.Empty;
|
||||
}
|
||||
|
||||
private async Task ConfirmCopyProject()
|
||||
{
|
||||
if (copyProjectSource == null || string.IsNullOrWhiteSpace(copyProjectName) || string.IsNullOrWhiteSpace(copyProjectPath))
|
||||
return;
|
||||
|
||||
if (!ProjectPathHelper.IsPathAllowed(copyProjectPath))
|
||||
{
|
||||
OpState.SetStatus($"⚠ Pfad nicht erlaubt. Verwende: {ProjectPathHelper.AllowedBasePath}", true);
|
||||
showCopyProjectDialog = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var userId = (await AuthService.GetCurrentUserAsync())?.Id;
|
||||
if (userId == null) return;
|
||||
|
||||
OpState.SetStatus($"{Loc["nav.status.project_copied"]} {copyProjectName}...", true);
|
||||
StateHasChanged();
|
||||
|
||||
var newId = await ProjectService.CopyProjectAsync(copyProjectSource.Id, copyProjectName, copyProjectPath, userId.Value);
|
||||
await LoadProjects();
|
||||
|
||||
var newProject = projects?.FirstOrDefault(p => p.Id == newId);
|
||||
if (newProject != null)
|
||||
{
|
||||
await SelectProject(newProject);
|
||||
}
|
||||
|
||||
OpState.SetStatus($"{Loc["nav.status.project_copied"]} {copyProjectName}", true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OpState.SetStatus($"{Loc["common.error"]} {ex.Message}", true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
OpState.SetStatus(OpState.StatusText, false);
|
||||
showCopyProjectDialog = false;
|
||||
copyProjectSource = null;
|
||||
copyProjectName = string.Empty;
|
||||
copyProjectPath = string.Empty;
|
||||
StateHasChanged();
|
||||
}
|
||||
activeSection = null;
|
||||
Navigation.NavigateTo("/editor?action=open");
|
||||
}
|
||||
|
||||
private void OpenEditor()
|
||||
|
|
@ -677,199 +149,17 @@ else
|
|||
Navigation.NavigateTo(path);
|
||||
}
|
||||
|
||||
private void ToggleInfoOverlay()
|
||||
{
|
||||
_showInfoOverlay = !_showInfoOverlay;
|
||||
}
|
||||
|
||||
private void CloseInfoOverlay()
|
||||
{
|
||||
_showInfoOverlay = false;
|
||||
}
|
||||
|
||||
private void NavigateToSetup2FA()
|
||||
private void SelectProject(Project project)
|
||||
{
|
||||
activeSection = null;
|
||||
Navigation.NavigateTo("/setup-2fa");
|
||||
Navigation.NavigateTo($"/editor?projectId={project.Id}");
|
||||
}
|
||||
|
||||
private async Task Logout()
|
||||
public void SetStatus(string text, bool busy = false, double progress = 0)
|
||||
{
|
||||
activeSection = null;
|
||||
await AuthService.LogoutAsync();
|
||||
_username = string.Empty;
|
||||
AuthStateProvider.NotifyStateChanged();
|
||||
Navigation.NavigateTo("/login", true);
|
||||
}
|
||||
|
||||
private async Task SelectProject(Project project)
|
||||
{
|
||||
activeSection = "files";
|
||||
SettingsService.CurrentProjectId = project.Id;
|
||||
currentProjectId = project.Id;
|
||||
currentProjectName = project.Name;
|
||||
SettingsService.CurrentFilePath = null;
|
||||
await SettingsService.SaveLastProjectIdAsync(project.Id);
|
||||
await SettingsService.LoadProjectSettingsAsync();
|
||||
await LoadFileTree();
|
||||
Navigation.NavigateTo($"/editor/index/{project.Id}");
|
||||
}
|
||||
|
||||
private async Task RefreshFileTree()
|
||||
{
|
||||
await LoadFileTree();
|
||||
statusText = text;
|
||||
isBusy = busy;
|
||||
operationProgress = progress;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void BeginRename(FileTreeItem item)
|
||||
{
|
||||
renameTarget = item;
|
||||
renameNewName = item.Name;
|
||||
showRenameDialog = true;
|
||||
}
|
||||
|
||||
private void CancelRename()
|
||||
{
|
||||
showRenameDialog = false;
|
||||
renameTarget = null;
|
||||
renameNewName = string.Empty;
|
||||
}
|
||||
|
||||
private async Task ConfirmRename()
|
||||
{
|
||||
if (renameTarget == null || string.IsNullOrWhiteSpace(renameNewName)) return;
|
||||
|
||||
try
|
||||
{
|
||||
var dir = Path.GetDirectoryName(renameTarget.FullPath)!;
|
||||
var newPath = Path.Combine(dir, renameNewName);
|
||||
if (File.Exists(newPath))
|
||||
{
|
||||
OpState.SetStatus(Loc["nav.error.file_exists"]);
|
||||
return;
|
||||
}
|
||||
File.Move(renameTarget.FullPath, newPath);
|
||||
|
||||
if (currentFilePath == renameTarget.FullPath)
|
||||
SettingsService.CurrentFilePath = newPath;
|
||||
|
||||
OpState.SetStatus($"{Loc["nav.status.renamed_to"]} {renameNewName}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OpState.SetStatus($"{Loc["common.error"]} {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
showRenameDialog = false;
|
||||
renameTarget = null;
|
||||
renameNewName = string.Empty;
|
||||
await LoadFileTree();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void BeginNewFile(FileTreeItem dir)
|
||||
{
|
||||
newFileParentDir = dir;
|
||||
newFileName = string.Empty;
|
||||
showNewFileDialog = true;
|
||||
}
|
||||
|
||||
private void CancelNewFile()
|
||||
{
|
||||
showNewFileDialog = false;
|
||||
newFileParentDir = null;
|
||||
newFileName = string.Empty;
|
||||
}
|
||||
|
||||
private async Task ConfirmNewFile()
|
||||
{
|
||||
if (newFileParentDir == null || string.IsNullOrWhiteSpace(newFileName)) return;
|
||||
|
||||
try
|
||||
{
|
||||
var path = Path.Combine(newFileParentDir.FullPath, newFileName);
|
||||
if (!path.EndsWith(".md", StringComparison.OrdinalIgnoreCase))
|
||||
path += ".md";
|
||||
if (File.Exists(path))
|
||||
{
|
||||
OpState.SetStatus(Loc["nav.error.file_exists"]);
|
||||
return;
|
||||
}
|
||||
File.WriteAllText(path, $"# {Path.GetFileNameWithoutExtension(path)}\n\n");
|
||||
OpState.SetStatus($"{Loc["nav.status.created"]} {Path.GetFileName(path)}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OpState.SetStatus($"{Loc["common.error"]} {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
showNewFileDialog = false;
|
||||
newFileParentDir = null;
|
||||
newFileName = string.Empty;
|
||||
await LoadFileTree();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenFile(FileTreeItem item)
|
||||
{
|
||||
if (item.IsDirectory) return;
|
||||
activeSection = null;
|
||||
SettingsService.CurrentFilePath = item.FullPath;
|
||||
Navigation.NavigateTo($"/editor/file/{currentProjectId}?t={DateTime.Now.Ticks}");
|
||||
}
|
||||
|
||||
private void ToggleFileSelection(string path)
|
||||
{
|
||||
if (!_selectedFiles.Remove(path))
|
||||
_selectedFiles.Add(path);
|
||||
}
|
||||
|
||||
private async Task DoExport()
|
||||
{
|
||||
var format = exportFormat switch
|
||||
{
|
||||
"Html" => ExportFormat.Html,
|
||||
"Markdown" => ExportFormat.Markdown,
|
||||
"Zip" => ExportFormat.Zip,
|
||||
"Docx" => ExportFormat.Docx,
|
||||
"Pdf" => ExportFormat.Pdf,
|
||||
_ => ExportFormat.Html
|
||||
};
|
||||
|
||||
var ext = format switch
|
||||
{
|
||||
ExportFormat.Html => ".html",
|
||||
ExportFormat.Markdown => ".md",
|
||||
ExportFormat.Zip => ".zip",
|
||||
ExportFormat.Docx => ".docx",
|
||||
ExportFormat.Pdf => ".pdf",
|
||||
_ => ".html"
|
||||
};
|
||||
|
||||
var name = string.IsNullOrWhiteSpace(exportFileName)
|
||||
? $"Export_{DateTime.Now:yyyyMMdd_HHmmss}"
|
||||
: exportFileName;
|
||||
|
||||
var data = await ExportService.ExportAsync(currentProjectId, _selectedFiles.ToList(), format);
|
||||
|
||||
var mime = format switch
|
||||
{
|
||||
ExportFormat.Html => "text/html",
|
||||
ExportFormat.Markdown => "text/markdown",
|
||||
ExportFormat.Zip => "application/zip",
|
||||
ExportFormat.Docx => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
ExportFormat.Pdf => "application/pdf",
|
||||
_ => "text/html"
|
||||
};
|
||||
|
||||
await JS.InvokeVoidAsync("downloadFile", $"{name}{ext}", Convert.ToBase64String(data), mime);
|
||||
|
||||
_showExportDialog = false;
|
||||
_selectedFiles.Clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,19 +9,6 @@ main {
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
.main-header {
|
||||
padding: 12px 24px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--card-bg);
|
||||
}
|
||||
|
||||
.main-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
color-scheme: light only;
|
||||
background: lightyellow;
|
||||
|
|
|
|||
|
|
@ -1,195 +0,0 @@
|
|||
@page "/db-admin"
|
||||
@attribute [Authorize]
|
||||
@inject IDbAdminService DbAdminSvc
|
||||
@inject ILocalizationService Loc
|
||||
@inject NavigationManager Navigation
|
||||
@inject IAuthService AuthService
|
||||
|
||||
<PageTitle>Datenbank Administration - Author Buddy</PageTitle>
|
||||
|
||||
<div class="dbadmin-page">
|
||||
<h3>Datenbank Administration</h3>
|
||||
|
||||
<div class="dbadmin-toolbar">
|
||||
<select @bind="selectedCollection" @bind:after="LoadRecords">
|
||||
@foreach (var c in collections)
|
||||
{
|
||||
<option value="@c.Name">@c.Name (@c.Count Einträge)</option>
|
||||
}
|
||||
</select>
|
||||
<span style="font-size:12px;color:var(--border-color);margin-left:8px;">
|
||||
@records.Count Einträge in "@selectedCollection"
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@if (records.Count == 0)
|
||||
{
|
||||
<p style="padding:24px;color:var(--border-color);">Leere Sammlung.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="dbadmin-table-wrapper">
|
||||
<table class="dbadmin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
@foreach (var key in columnOrder)
|
||||
{
|
||||
<th>@key</th>
|
||||
}
|
||||
<th style="width:130px;">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var record in records)
|
||||
{
|
||||
<tr>
|
||||
<td><strong>@record.Id</strong></td>
|
||||
@foreach (var key in columnOrder)
|
||||
{
|
||||
<td class="dbadmin-cell" title="@(record.Fields.GetValueOrDefault(key) ?? "(null)")">
|
||||
@Truncate(record.Fields.GetValueOrDefault(key), 60)
|
||||
</td>
|
||||
}
|
||||
<td class="dbadmin-actions">
|
||||
<button class="btn btn-sm" @onclick="() => StartEdit(record)">Bearbeiten</button>
|
||||
<button class="btn btn-sm btn-danger" @onclick="() => ConfirmDelete(record)">Löschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(statusMessage))
|
||||
{
|
||||
<div class="dbadmin-status">@statusMessage</div>
|
||||
}
|
||||
|
||||
<button class="btn btn-secondary" @onclick='() => Navigation.NavigateTo("/settings")' style="margin-top:12px;">Zurück</button>
|
||||
</div>
|
||||
|
||||
@if (editingRecord != null)
|
||||
{
|
||||
<div class="modal-overlay" @onclick="CancelEdit">
|
||||
<div class="modal-dialog" @onclick:stopPropagation>
|
||||
<div class="modal-title">Datensatz bearbeiten (ID: @editingRecord.Id) — @selectedCollection</div>
|
||||
<div class="modal-body">
|
||||
@foreach (var key in columnOrder)
|
||||
{
|
||||
var idx = key;
|
||||
<div class="form-group">
|
||||
<label>@key</label>
|
||||
<input value="@(editValues.GetValueOrDefault(idx) ?? string.Empty)" @onchange="(ChangeEventArgs e) => { editValues[idx] = e.Value?.ToString() ?? string.Empty; }" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button @onclick="CancelEdit">Abbrechen</button>
|
||||
<button @onclick="SaveEdit" class="primary">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (deletingRecord != null)
|
||||
{
|
||||
<div class="modal-overlay" @onclick="CancelDelete">
|
||||
<div class="modal-dialog modal-sm" @onclick:stopPropagation>
|
||||
<div class="modal-title">Löschen bestätigen</div>
|
||||
<div class="modal-body">
|
||||
<p>Datensatz <strong>ID @deletingRecord.Id</strong> aus <strong>@selectedCollection</strong> löschen?</p>
|
||||
<p style="font-size:12px;color:var(--border-color);">Diese Aktion kann nicht rückgängig gemacht werden.</p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button @onclick="CancelDelete">Abbrechen</button>
|
||||
<button @onclick="DeleteRecord" class="btn-danger">Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private List<DbCollectionInfo> collections = new();
|
||||
private string selectedCollection = "users";
|
||||
private List<DbRecord> records = new();
|
||||
private List<string> columnOrder = new();
|
||||
private string statusMessage = string.Empty;
|
||||
|
||||
private DbRecord? editingRecord;
|
||||
private Dictionary<string, string?> editValues = new();
|
||||
|
||||
private DbRecord? deletingRecord;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
var user = AuthService.CurrentUser;
|
||||
if (user?.Role != UserRole.Administrator)
|
||||
{
|
||||
Navigation.NavigateTo("/");
|
||||
return;
|
||||
}
|
||||
|
||||
collections = DbAdminSvc.GetCollections();
|
||||
if (collections.Count > 0)
|
||||
selectedCollection = collections[0].Name;
|
||||
LoadRecords();
|
||||
}
|
||||
|
||||
private void LoadRecords()
|
||||
{
|
||||
records = DbAdminSvc.GetAll(selectedCollection);
|
||||
columnOrder = records.Count > 0
|
||||
? records[0].Fields.Keys.ToList()
|
||||
: new List<string>();
|
||||
statusMessage = string.Empty;
|
||||
}
|
||||
|
||||
private void StartEdit(DbRecord record)
|
||||
{
|
||||
editingRecord = record;
|
||||
editValues = new Dictionary<string, string?>(record.Fields);
|
||||
}
|
||||
|
||||
private void CancelEdit()
|
||||
{
|
||||
editingRecord = null;
|
||||
editValues.Clear();
|
||||
}
|
||||
|
||||
private void SaveEdit()
|
||||
{
|
||||
if (editingRecord == null) return;
|
||||
var ok = DbAdminSvc.Update(selectedCollection, editingRecord.Id, editValues);
|
||||
statusMessage = ok ? "Gespeichert." : "Fehler beim Speichern.";
|
||||
editingRecord = null;
|
||||
editValues.Clear();
|
||||
LoadRecords();
|
||||
}
|
||||
|
||||
private void ConfirmDelete(DbRecord record)
|
||||
{
|
||||
deletingRecord = record;
|
||||
}
|
||||
|
||||
private void CancelDelete()
|
||||
{
|
||||
deletingRecord = null;
|
||||
}
|
||||
|
||||
private void DeleteRecord()
|
||||
{
|
||||
if (deletingRecord == null) return;
|
||||
var ok = DbAdminSvc.Delete(selectedCollection, deletingRecord.Id);
|
||||
statusMessage = ok ? "Gelöscht." : "Fehler beim Löschen.";
|
||||
deletingRecord = null;
|
||||
LoadRecords();
|
||||
}
|
||||
|
||||
private static string? Truncate(string? val, int max)
|
||||
{
|
||||
if (val == null) return null;
|
||||
return val.Length > max ? val[..max] + "..." : val;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,246 +1,39 @@
|
|||
@page "/editor"
|
||||
@page "/editor/{action}"
|
||||
@page "/editor/{action}/{projectId:int}"
|
||||
@using System.Threading
|
||||
@inject IOllamaService OllamaService
|
||||
@inject IProjectService ProjectService
|
||||
@inject IRagService RagService
|
||||
@inject ISettingsService SettingsService
|
||||
@inject IWikipediaService WikipediaService
|
||||
@inject NavigationManager Navigation
|
||||
@inject ILocalizationService Loc
|
||||
@inject IAuthService AuthService
|
||||
@inject OllamaSettings OllamaSettings
|
||||
@inject OperationState OpState
|
||||
@inject IJSRuntime JS
|
||||
|
||||
@implements IDisposable
|
||||
@rendermode InteractiveServer
|
||||
@attribute [Authorize]
|
||||
|
||||
<PageTitle>@Loc["page.title.editor"]</PageTitle>
|
||||
|
||||
<div class="editor-page">
|
||||
<textarea class="editor-area"
|
||||
@oninput="OnContentChanged"
|
||||
value="@editorContent"
|
||||
placeholder="@Loc["editor.placeholder.content"]"></textarea>
|
||||
<div class="editor-format-toolbar">
|
||||
<button class="fmt-btn" @onclick="() => InsertHeading(1)" title="Überschrift 1">H1</button>
|
||||
<button class="fmt-btn" @onclick="() => InsertHeading(2)" title="Überschrift 2">H2</button>
|
||||
<button class="fmt-btn" @onclick="() => InsertHeading(3)" title="Überschrift 3">H3</button>
|
||||
<button class="fmt-btn" @onclick="() => InsertHeading(4)" title="Überschrift 4">H4</button>
|
||||
<button class="fmt-btn" @onclick="() => InsertHeading(5)" title="Überschrift 5">H5</button>
|
||||
<span class="fmt-sep"></span>
|
||||
<button class="fmt-btn" @onclick='() => WrapSelection("**", "**")' title="Fett"><b>B</b></button>
|
||||
<button class="fmt-btn" @onclick='() => WrapSelection("*", "*")' title="Kursiv"><i>I</i></button>
|
||||
<button class="fmt-btn" @onclick='() => WrapSelection("<u>", "</u>")' title="Unterstrichen"><u>U</u></button>
|
||||
<span class="fmt-sep"></span>
|
||||
<button class="fmt-btn" @onclick="() => InsertList(false)" title="Aufzählung">UL</button>
|
||||
<button class="fmt-btn" @onclick="() => InsertList(true)" title="Nummerierte Liste">OL</button>
|
||||
<span class="fmt-sep"></span>
|
||||
<button class="fmt-btn" @onclick="() => IndentLines()" title="Einrücken">→</button>
|
||||
<button class="fmt-btn" @onclick="() => OutdentLines()" title="Ausrücken">←</button>
|
||||
</div>
|
||||
<CopyCreateToolbar Text="@editorContent" DefaultFileName="editor_text" />
|
||||
<PageTitle>Editor - Author Buddy</PageTitle>
|
||||
|
||||
<div class="editor-container">
|
||||
<textarea @bind="editorContent" placeholder="Schreibe hier deinen Text..."></textarea>
|
||||
<div class="editor-toolbar">
|
||||
<span style="flex: 1; font-size: 12px; color: var(--border-color); display:flex;align-items:center;gap:8px;">
|
||||
@if (_contentModified)
|
||||
{
|
||||
<span class="editor-modified-badge">●</span>
|
||||
}
|
||||
<span style="flex: 1; font-size: 12px; color: var(--border-color);">
|
||||
@statusText
|
||||
</span>
|
||||
@if (isChecking)
|
||||
{
|
||||
<progress value="@checkProgress" max="100"></progress>
|
||||
}
|
||||
<button @onclick="CheckStyle" disabled="@isChecking">@Loc["editor.button.style_check"]</button>
|
||||
<button @onclick="SpellCheck" disabled="@isChecking">@Loc["editor.button.spell_check"]</button>
|
||||
<button @onclick="SaveCurrentFile" disabled="@(string.IsNullOrEmpty(currentFile))">@Loc["editor.button.save"]</button>
|
||||
<button @onclick="ShowVersionDialog" disabled="@(string.IsNullOrEmpty(currentFile))">@Loc["editor.version.button.save"]</button>
|
||||
<button @onclick="ShowVersionBrowser" disabled="@(string.IsNullOrEmpty(currentFile))">@Loc["editor.version.button.browse"]</button>
|
||||
<button @onclick="OpenAssistant">@Loc["editor.button.assistant"]</button>
|
||||
<button @onclick="IndexProject" disabled="@isChecking">@Loc["editor.button.index_project"]</button>
|
||||
</div>
|
||||
|
||||
@if (showVersionDialog)
|
||||
{
|
||||
<div class="modal-overlay">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-title">@Loc["editor.version.title"]</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>@Loc["editor.version.label.comment"]</label>
|
||||
<input @bind="versionComment" placeholder="@Loc["editor.version.placeholder.comment"]" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button @onclick="() => showVersionDialog = false">@Loc["common.cancel"]</button>
|
||||
<button @onclick="SaveVersioned" class="primary">@Loc["editor.version.button.save"]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="search-section">
|
||||
<input value="@searchQuery" @oninput="HandleSearchInput" placeholder="@Loc["editor.search.placeholder"]"
|
||||
disabled="@isChecking" />
|
||||
<button @onclick="RagSearch" disabled=@buttonDisabled>@Loc["editor.button.search_rag"]</button>
|
||||
<button @onclick="SearchWikipedia" disabled=@buttonDisabled>@Loc["editor.button.search_wikipedia"]</button>
|
||||
</div>
|
||||
|
||||
@if (showSpellDiff && !string.IsNullOrEmpty(correctedText))
|
||||
{
|
||||
<div class="results-area">
|
||||
<div class="diff-panel">
|
||||
<h4 style="display:inline">@Loc["editor.spellcheck.heading"]</h4>
|
||||
<CopyCreateToolbar Text="@correctedText" DefaultFileName="korrigierter_text" />
|
||||
<DiffView Original="@originalText" Corrected="@correctedText" />
|
||||
<div class="diff-actions">
|
||||
<button class="btn btn-primary" @onclick="ApplySpellCorrections">@Loc["editor.spellcheck.button.apply"]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (showVersionDiff && _selectedVersion != null)
|
||||
{
|
||||
<div class="results-area">
|
||||
<div class="diff-panel">
|
||||
<h4>@Loc["editor.version.diff_title"] @_selectedVersion.Timestamp:dd.MM.yyyy HH:mm</h4>
|
||||
@if (!string.IsNullOrEmpty(_selectedVersion.Comment))
|
||||
{
|
||||
<p style="font-size:11px;color:var(--border-color);margin:0 0 8px 0;">@Loc["editor.version.label.comment"]: @_selectedVersion.Comment</p>
|
||||
}
|
||||
<DiffView Original="@editorContent" Corrected="@_versionDiffContent" />
|
||||
<div class="diff-actions">
|
||||
<button class="btn btn-primary" @onclick="() => RestoreVersion(_selectedVersion)">@Loc["editor.version.button.restore"]</button>
|
||||
<button class="btn btn-secondary" @onclick="CloseVersionBrowser">@Loc["common.cancel"]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (showVersionBrowser)
|
||||
{
|
||||
<div class="results-area">
|
||||
<h4>@Loc["editor.version.list_title"]</h4>
|
||||
@if (_versionList == null || _versionList.Count == 0)
|
||||
{
|
||||
<p>@Loc["editor.version.empty"]</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="version-list">
|
||||
@foreach (var v in _versionList)
|
||||
{
|
||||
<div class="version-item @(v == _selectedVersion ? "active" : "")">
|
||||
<div class="version-item-info">
|
||||
<span class="version-date">@v.Timestamp.ToString("dd.MM.yyyy HH:mm")</span>
|
||||
@if (!string.IsNullOrEmpty(v.Comment))
|
||||
{
|
||||
<span class="version-comment">@v.Comment</span>
|
||||
}
|
||||
</div>
|
||||
<div class="version-item-actions">
|
||||
<button class="btn btn-sm" @onclick="() => ShowVersionDiff(v)">@Loc["editor.version.button.diff"]</button>
|
||||
<button class="btn btn-sm btn-primary" @onclick="() => RestoreVersion(v)">@Loc["editor.version.button.restore"]</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<button class="btn btn-secondary" @onclick="CloseVersionBrowser">@Loc["common.cancel"]</button>
|
||||
</div>
|
||||
}
|
||||
else if (ragResults != null && ragResults.Count > 0)
|
||||
{
|
||||
<div class="results-area">
|
||||
<h4>@Loc["editor.rag.heading"]</h4>
|
||||
@foreach (var r in ragResults)
|
||||
{
|
||||
<div class="fact-card">
|
||||
<div class="fact-similarity">@Loc["editor.rag.label.similarity"] @r.Similarity.ToString("P1")</div>
|
||||
<div class="fact-path">@r.FilePath</div>
|
||||
<div class="fact-text">@r.Text</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else if (wikipediaResults != null && wikipediaResults.Count > 0)
|
||||
{
|
||||
<div class="results-area">
|
||||
<h4>@Loc["editor.wikipedia.heading"]</h4>
|
||||
@foreach (var r in wikipediaResults)
|
||||
{
|
||||
<div class="fact-card">
|
||||
<div class="fact-similarity">@(r.Language == "de" ? "🇩🇪" : "🇬🇧") @r.Language.ToUpper()</div>
|
||||
<div class="fact-path">
|
||||
<a href="@r.Url" target="_blank">@r.Title</a>
|
||||
</div>
|
||||
<div class="fact-text">@r.Snippet</div>
|
||||
@if (!string.IsNullOrEmpty(r.Extract))
|
||||
{
|
||||
<div class="fact-extract">@r.Extract</div>
|
||||
}
|
||||
<div class="fact-actions">
|
||||
<button @onclick="() => LoadFullArticle(r)" disabled="@(loadedArticles.ContainsKey(r.Url))">
|
||||
@(loadedArticles.ContainsKey(r.Url)
|
||||
? Loc["editor.wikipedia.status.loaded"]
|
||||
: Loc["editor.wikipedia.button.load"])
|
||||
<button @onclick="CheckStyle" disabled="@isChecking">
|
||||
Stil prüfen
|
||||
</button>
|
||||
<button @onclick="() => SaveAsFact(r)" disabled="@(string.IsNullOrWhiteSpace(r.Extract) || currentProjectId == 0)">
|
||||
@Loc["editor.wikipedia.button.save_as_fact"]
|
||||
<button @onclick="SpellCheck" disabled="@isChecking">
|
||||
Rechtschreibung prüfen
|
||||
</button>
|
||||
<button @onclick="RagSearch">
|
||||
Fakten suchen (RAG)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(analysisResult) && !showVersionBrowser)
|
||||
{
|
||||
<div class="results-area">
|
||||
<h4 style="display:inline">@Loc["editor.result.heading"]</h4>
|
||||
<CopyCreateToolbar Text="@analysisResult" />
|
||||
<div class="result-text">@analysisResult</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (showCreateDialog)
|
||||
@if (!string.IsNullOrEmpty(analysisResult))
|
||||
{
|
||||
<div class="modal-overlay">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-title">@Loc["editor.create_project.title"]</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label>@Loc["editor.create_project.label.name"]</label>
|
||||
<input @bind="newProjectName" placeholder="@Loc["editor.create_project.placeholder.name"]" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>@Loc["editor.create_project.label.path"]</label>
|
||||
<input @bind="newProjectPath" placeholder="@Loc["editor.create_project.placeholder.path"]" />
|
||||
<small class="form-hint">Erlaubter Pfad: @ProjectPathHelper.AllowedBasePath</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Vorlage</label>
|
||||
<div class="template-radio-group">
|
||||
<label class="template-radio">
|
||||
<input type="radio" name="template" value="@ProjectTemplate.Novel" checked="@(selectedTemplate == ProjectTemplate.Novel)" @onchange="() => selectedTemplate = ProjectTemplate.Novel" />
|
||||
<span>📖 Roman</span>
|
||||
</label>
|
||||
<label class="template-radio">
|
||||
<input type="radio" name="template" value="@ProjectTemplate.Technical" checked="@(selectedTemplate == ProjectTemplate.Technical)" @onchange="() => selectedTemplate = ProjectTemplate.Technical" />
|
||||
<span>⚙️ Technische Vorlage (Requirements)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button @onclick="CancelCreateProject">@Loc["common.cancel"]</button>
|
||||
<button @onclick="CreateProject" disabled="@(() => string.IsNullOrWhiteSpace(newProjectName) || string.IsNullOrWhiteSpace(newProjectPath))" class="primary">@Loc["editor.button.create"]</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="border-top: 2px solid var(--border-color); padding: 12px; max-height: 300px; overflow-y: auto;">
|
||||
<h4 style="margin-bottom: 8px;">Ergebnis:</h4>
|
||||
<div style="white-space: pre-wrap; font-size: 13px; line-height: 1.5;">@analysisResult</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
|
@ -252,170 +45,19 @@
|
|||
public int? ProjectId { get; set; }
|
||||
|
||||
private string editorContent = string.Empty;
|
||||
private string searchQuery = string.Empty;
|
||||
private string statusText = string.Empty;
|
||||
private string statusText = "Bereit";
|
||||
private bool isChecking;
|
||||
private bool buttonDisabled;
|
||||
private double checkProgress;
|
||||
private string analysisResult = string.Empty;
|
||||
private string originalText = string.Empty;
|
||||
private string correctedText = string.Empty;
|
||||
private bool showSpellDiff;
|
||||
private List<RagSearchResult>? ragResults;
|
||||
private List<WikipediaResult>? wikipediaResults;
|
||||
|
||||
private int currentProjectId;
|
||||
private string? currentFile;
|
||||
|
||||
private bool showCreateDialog;
|
||||
private string newProjectName = string.Empty;
|
||||
private string newProjectPath = string.Empty;
|
||||
private ProjectTemplate selectedTemplate = ProjectTemplate.Novel;
|
||||
private Dictionary<string, bool> loadedArticles = new();
|
||||
private bool showVersionDialog;
|
||||
private string versionComment = string.Empty;
|
||||
private VersionEntry? latestVersion;
|
||||
|
||||
private Timer? _autosaveTimer;
|
||||
private string _lastSavedContent = string.Empty;
|
||||
private bool _contentModified;
|
||||
private bool showVersionBrowser;
|
||||
private bool showVersionDiff;
|
||||
private List<VersionEntry> _versionList = new();
|
||||
private VersionEntry? _selectedVersion;
|
||||
private string _versionDiffContent = string.Empty;
|
||||
|
||||
private record VersionEntry(string FilePath, DateTime Timestamp, string? Comment);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
statusText = Loc["common.status.ready"];
|
||||
|
||||
Navigation.LocationChanged += OnLocationChanged;
|
||||
|
||||
await LoadProjectContext();
|
||||
|
||||
var pendingOutput = SettingsService.PendingAssistantOutput;
|
||||
if (!string.IsNullOrEmpty(pendingOutput))
|
||||
if (Action == "newproject")
|
||||
{
|
||||
SettingsService.PendingAssistantOutput = null;
|
||||
editorContent = pendingOutput;
|
||||
_lastSavedContent = editorContent;
|
||||
statusText = Loc["editor.status.imported_from_assistant"];
|
||||
// The native folder dialog isn't available in Blazor.
|
||||
// In a real app, you'd provide a form to enter project name and path.
|
||||
statusText = "Bitte nutze die Einstellungen zum Erstellen eines neuen Projekts.";
|
||||
}
|
||||
|
||||
if (string.Equals(Action, "newproject", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
newProjectName = string.Empty;
|
||||
newProjectPath = ProjectPathHelper.SuggestPath("Projekt");
|
||||
showCreateDialog = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnLocationChanged(object? sender, LocationChangedEventArgs e)
|
||||
{
|
||||
await LoadProjectContext();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Navigation.LocationChanged -= OnLocationChanged;
|
||||
_autosaveTimer?.Dispose();
|
||||
}
|
||||
|
||||
private void OnContentChanged(ChangeEventArgs e)
|
||||
{
|
||||
editorContent = e.Value?.ToString() ?? string.Empty;
|
||||
_contentModified = editorContent != _lastSavedContent;
|
||||
ResetAutosaveTimer();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void ResetAutosaveTimer()
|
||||
{
|
||||
_autosaveTimer?.Dispose();
|
||||
_autosaveTimer = new Timer(AutosaveCallback, null, TimeSpan.FromSeconds(2), Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
|
||||
private async void AutosaveCallback(object? state)
|
||||
{
|
||||
if (string.IsNullOrEmpty(currentFile) || !_contentModified || isChecking) return;
|
||||
|
||||
try
|
||||
{
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
await File.WriteAllTextAsync(currentFile, editorContent);
|
||||
_lastSavedContent = editorContent;
|
||||
_contentModified = false;
|
||||
statusText = Loc["editor.status.autosaved"];
|
||||
StateHasChanged();
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenAssistant()
|
||||
{
|
||||
SettingsService.PendingAssistantInput = editorContent;
|
||||
Navigation.NavigateTo("/assistant");
|
||||
}
|
||||
|
||||
private void HandleSearchInput(ChangeEventArgs e)
|
||||
{
|
||||
searchQuery = e.Value?.ToString() ?? string.Empty;
|
||||
buttonDisabled = isChecking || string.IsNullOrWhiteSpace(searchQuery);
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task LoadProjectContext()
|
||||
{
|
||||
if (ProjectId.HasValue)
|
||||
{
|
||||
currentProjectId = ProjectId.Value;
|
||||
SettingsService.CurrentProjectId = currentProjectId;
|
||||
}
|
||||
else if (SettingsService.CurrentProjectId.HasValue)
|
||||
{
|
||||
currentProjectId = SettingsService.CurrentProjectId.Value;
|
||||
}
|
||||
|
||||
if (currentProjectId > 0)
|
||||
await SettingsService.LoadProjectSettingsAsync();
|
||||
|
||||
var filePath = SettingsService.CurrentFilePath;
|
||||
if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
|
||||
{
|
||||
currentFile = filePath;
|
||||
editorContent = await File.ReadAllTextAsync(filePath);
|
||||
_lastSavedContent = editorContent;
|
||||
_contentModified = false;
|
||||
statusText = $"{Loc["editor.status.loaded"]} {Path.GetFileName(filePath)}";
|
||||
RefreshLatestVersion();
|
||||
|
||||
var styleModel = await SettingsService.GetStyleModelAsync();
|
||||
if (!string.IsNullOrEmpty(styleModel))
|
||||
{
|
||||
var ctx = await OllamaService.GetModelContextWindowAsync(styleModel);
|
||||
if (ctx.Success && editorContent.Length > ctx.ContextWindow * 0.8)
|
||||
{
|
||||
statusText = $"⚠ {Loc["editor.warning.context"]} ({editorContent.Length} Tokens ≈ {ctx.ContextWindow} Kontext)";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearResults()
|
||||
{
|
||||
showSpellDiff = false;
|
||||
showVersionBrowser = false;
|
||||
showVersionDiff = false;
|
||||
ragResults = null;
|
||||
wikipediaResults = null;
|
||||
analysisResult = string.Empty;
|
||||
}
|
||||
|
||||
private async Task CheckStyle()
|
||||
|
|
@ -423,20 +65,20 @@
|
|||
if (string.IsNullOrWhiteSpace(editorContent)) return;
|
||||
|
||||
isChecking = true;
|
||||
statusText = Loc["editor.status.analyzing_style"];
|
||||
statusText = "Ollama analysiert Stil...";
|
||||
checkProgress = 50;
|
||||
ClearResults();
|
||||
|
||||
try
|
||||
{
|
||||
analysisResult = await OllamaService.StreamStyleCheckAsync(editorContent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
analysisResult = $"{Loc["common.error"]} {ex.Message}";
|
||||
analysisResult = $"Fehler: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
statusText = Loc["common.status.ready"];
|
||||
statusText = "Bereit";
|
||||
isChecking = false;
|
||||
checkProgress = 0;
|
||||
}
|
||||
|
|
@ -447,454 +89,35 @@
|
|||
if (string.IsNullOrWhiteSpace(editorContent)) return;
|
||||
|
||||
isChecking = true;
|
||||
statusText = Loc["editor.status.checking_spelling"];
|
||||
statusText = "Ollama prüft Rechtschreibung...";
|
||||
checkProgress = 50;
|
||||
ClearResults();
|
||||
|
||||
try
|
||||
{
|
||||
originalText = editorContent;
|
||||
correctedText = "";
|
||||
OpState.SetStatus(Loc["editor.status.checking_spelling"], true);
|
||||
OpState.SetThought("");
|
||||
|
||||
await OllamaService.ChatStream(OllamaSettings.SpellingModel, editorContent, async chunk =>
|
||||
{
|
||||
correctedText += chunk;
|
||||
OpState.AppendThought(chunk);
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}, OllamaSettings.SpellingPrompt, OllamaSettings.SpellingTemp);
|
||||
|
||||
showSpellDiff = true;
|
||||
OpState.SetStatus(Loc["common.status.ready"], false);
|
||||
analysisResult = await OllamaService.QuickSpellCheckAsync(editorContent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
analysisResult = $"{Loc["common.error"]} {ex.Message}";
|
||||
OpState.SetStatus($"{Loc["common.error"]} {ex.Message}", false);
|
||||
analysisResult = $"Fehler: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
OpState.Reset();
|
||||
statusText = Loc["common.status.ready"];
|
||||
statusText = "Bereit";
|
||||
isChecking = false;
|
||||
checkProgress = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplySpellCorrections()
|
||||
{
|
||||
if (string.IsNullOrEmpty(correctedText)) return;
|
||||
editorContent = correctedText;
|
||||
_contentModified = editorContent != _lastSavedContent;
|
||||
showSpellDiff = false;
|
||||
ResetAutosaveTimer();
|
||||
statusText = Loc["editor.status.corrections_applied"];
|
||||
}
|
||||
|
||||
private async Task RagSearch()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(searchQuery)) return;
|
||||
if (string.IsNullOrWhiteSpace(editorContent)) return;
|
||||
|
||||
isChecking = true;
|
||||
statusText = Loc["editor.status.searching_rag"];
|
||||
ClearResults();
|
||||
analysisResult = string.Empty;
|
||||
statusText = "Suche verwandte Fakten...";
|
||||
|
||||
try
|
||||
{
|
||||
if (currentProjectId == 0)
|
||||
{
|
||||
analysisResult = Loc["editor.rag.error.no_project"];
|
||||
return;
|
||||
}
|
||||
|
||||
ragResults = await RagService.SearchAsync(currentProjectId, searchQuery, 5);
|
||||
|
||||
if (ragResults.Count == 0)
|
||||
{
|
||||
analysisResult = Loc["editor.rag.error.no_facts"];
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
analysisResult = $"{Loc["editor.rag.error.generic"]} {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
statusText = Loc["common.status.ready"];
|
||||
await Task.Delay(500);
|
||||
analysisResult = "RAG-Suche: Stelle sicher, dass ein Projekt geladen und indexiert wurde.";
|
||||
statusText = "Bereit";
|
||||
isChecking = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SearchWikipedia()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(searchQuery)) return;
|
||||
|
||||
isChecking = true;
|
||||
statusText = Loc["editor.status.searching_wikipedia"];
|
||||
ClearResults();
|
||||
analysisResult = string.Empty;
|
||||
loadedArticles.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
var request = new WikipediaSearchRequest
|
||||
{
|
||||
Query = searchQuery,
|
||||
SearchGerman = true,
|
||||
SearchEnglish = true,
|
||||
MaxResults = 3
|
||||
};
|
||||
|
||||
wikipediaResults = await WikipediaService.SearchAsync(request);
|
||||
|
||||
foreach (var result in wikipediaResults)
|
||||
{
|
||||
var extract = await WikipediaService.GetExtractAsync(result.Title, result.Language);
|
||||
if (extract != null)
|
||||
result.Extract = extract.Length > 800 ? extract[..800] + "..." : extract;
|
||||
}
|
||||
|
||||
if (wikipediaResults.Count == 0)
|
||||
analysisResult = Loc["editor.wikipedia.error.not_found"];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
analysisResult = $"{Loc["editor.wikipedia.error.generic"]} {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
statusText = Loc["common.status.ready"];
|
||||
isChecking = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadFullArticle(WikipediaResult result)
|
||||
{
|
||||
statusText = $"{Loc["editor.status.loading_article"]} {result.Title}...";
|
||||
|
||||
try
|
||||
{
|
||||
var fullText = await WikipediaService.GetFullArticleAsync(result.Title, result.Language);
|
||||
if (fullText != null)
|
||||
{
|
||||
result.Extract = fullText;
|
||||
loadedArticles[result.Url] = true;
|
||||
statusText = Loc["editor.wikipedia.status.loaded"];
|
||||
}
|
||||
else
|
||||
{
|
||||
statusText = Loc["editor.wikipedia.error.load_failed"];
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
statusText = $"{Loc["common.error"]} {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveAsFact(WikipediaResult result)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(result.Extract) || currentProjectId == 0) return;
|
||||
|
||||
statusText = Loc["editor.status.saving_fact"];
|
||||
|
||||
try
|
||||
{
|
||||
var projects = await ProjectService.GetAllProjectsAsync();
|
||||
var project = projects.FirstOrDefault(p => p.Id == currentProjectId);
|
||||
if (project == null)
|
||||
{
|
||||
statusText = Loc["editor.error.project_not_found"];
|
||||
return;
|
||||
}
|
||||
|
||||
var factsDir = Path.Combine(project.RootPath, "01_Fakten");
|
||||
Directory.CreateDirectory(factsDir);
|
||||
|
||||
var safeName = SanitizeFilename(searchQuery);
|
||||
var filePath = Path.Combine(factsDir, $"{safeName}.md");
|
||||
|
||||
var content = $"# {result.Title}\n" +
|
||||
$"- Quelle: [{result.Title}]({result.Url})\n" +
|
||||
$"- Sprache: {result.Language.ToUpper()}\n" +
|
||||
$"- Abgerufen: {DateTime.Now:dd.MM.yyyy HH:mm}\n\n" +
|
||||
result.Extract;
|
||||
|
||||
await File.WriteAllTextAsync(filePath, content);
|
||||
statusText = $"{Loc["editor.status.saved_as"]} {safeName}.md";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
statusText = $"{Loc["editor.error.save_failed"]} {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private static string SanitizeFilename(string name)
|
||||
{
|
||||
var invalid = Path.GetInvalidFileNameChars();
|
||||
var sanitized = new string(name.Where(c => !invalid.Contains(c)).ToArray());
|
||||
return string.IsNullOrWhiteSpace(sanitized) ? "fakt" : sanitized.Trim();
|
||||
}
|
||||
|
||||
private void CancelCreateProject()
|
||||
{
|
||||
showCreateDialog = false;
|
||||
Navigation.NavigateTo("/editor");
|
||||
}
|
||||
|
||||
private async Task CreateProject()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(newProjectName) || string.IsNullOrWhiteSpace(newProjectPath)) return;
|
||||
|
||||
if (!ProjectPathHelper.IsPathAllowed(newProjectPath))
|
||||
{
|
||||
showCreateDialog = false;
|
||||
statusText = $"⚠ Pfad nicht erlaubt. Verwende: {ProjectPathHelper.AllowedBasePath}";
|
||||
return;
|
||||
}
|
||||
|
||||
isChecking = true;
|
||||
statusText = Loc["editor.status.creating_project"];
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(newProjectPath);
|
||||
var userId = AuthService.CurrentUser?.Id ?? 0;
|
||||
var id = await ProjectService.CreateNewProjectAsync(newProjectName, newProjectPath, userId, selectedTemplate);
|
||||
SettingsService.CurrentProjectId = id;
|
||||
await SettingsService.LoadProjectSettingsAsync();
|
||||
showCreateDialog = false;
|
||||
Navigation.NavigateTo($"/editor/index/{id}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
analysisResult = $"{Loc["editor.error.create_failed"]} {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isChecking = false;
|
||||
statusText = Loc["common.status.ready"];
|
||||
}
|
||||
}
|
||||
|
||||
private async Task IndexProject()
|
||||
{
|
||||
if (currentProjectId == 0)
|
||||
{
|
||||
analysisResult = Loc["editor.index.error.no_project"];
|
||||
return;
|
||||
}
|
||||
|
||||
isChecking = true;
|
||||
statusText = Loc["editor.status.indexing"];
|
||||
analysisResult = string.Empty;
|
||||
ragResults = null;
|
||||
|
||||
try
|
||||
{
|
||||
var projects = await ProjectService.GetAllProjectsAsync();
|
||||
var project = projects.FirstOrDefault(p => p.Id == currentProjectId);
|
||||
if (project == null)
|
||||
{
|
||||
analysisResult = Loc["editor.index.error.not_found"];
|
||||
return;
|
||||
}
|
||||
|
||||
var p = new Progress<string>(msg =>
|
||||
{
|
||||
statusText = msg;
|
||||
InvokeAsync(StateHasChanged);
|
||||
});
|
||||
|
||||
await RagService.IndexProjectAsync(currentProjectId, project.RootPath, p);
|
||||
analysisResult = Loc["editor.index.status.success"];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
analysisResult = $"{Loc["editor.index.error.generic"]} {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
statusText = Loc["common.status.ready"];
|
||||
isChecking = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveCurrentFile()
|
||||
{
|
||||
if (string.IsNullOrEmpty(currentFile)) return;
|
||||
await File.WriteAllTextAsync(currentFile, editorContent);
|
||||
_lastSavedContent = editorContent;
|
||||
_contentModified = false;
|
||||
statusText = $"{Loc["editor.status.saved"]} {Path.GetFileName(currentFile)}";
|
||||
}
|
||||
|
||||
private void ShowVersionDialog()
|
||||
{
|
||||
showVersionDialog = true;
|
||||
versionComment = string.Empty;
|
||||
}
|
||||
|
||||
private async Task SaveVersioned()
|
||||
{
|
||||
if (string.IsNullOrEmpty(currentFile)) return;
|
||||
|
||||
var dir = Path.GetDirectoryName(currentFile)!;
|
||||
var name = Path.GetFileNameWithoutExtension(currentFile);
|
||||
var ext = Path.GetExtension(currentFile);
|
||||
var versionsDir = Path.Combine(dir, "_versions");
|
||||
Directory.CreateDirectory(versionsDir);
|
||||
|
||||
var stamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||
var versionPath = Path.Combine(versionsDir, $"{name}_{stamp}{ext}");
|
||||
|
||||
await File.WriteAllTextAsync(currentFile, editorContent);
|
||||
await File.WriteAllTextAsync(versionPath, editorContent);
|
||||
var meta = $"Kommentar: {versionComment}";
|
||||
await File.WriteAllTextAsync(versionPath + ".meta", meta);
|
||||
|
||||
statusText = $"{Loc["editor.status.version_saved"]} {name}_{stamp}{ext}";
|
||||
showVersionDialog = false;
|
||||
versionComment = string.Empty;
|
||||
_lastSavedContent = editorContent;
|
||||
_contentModified = false;
|
||||
RefreshLatestVersion();
|
||||
}
|
||||
|
||||
private void ShowVersionBrowser()
|
||||
{
|
||||
ClearResults();
|
||||
LoadVersionList();
|
||||
showVersionBrowser = true;
|
||||
}
|
||||
|
||||
private void CloseVersionBrowser()
|
||||
{
|
||||
showVersionBrowser = false;
|
||||
showVersionDiff = false;
|
||||
_selectedVersion = null;
|
||||
}
|
||||
|
||||
private void LoadVersionList()
|
||||
{
|
||||
_versionList.Clear();
|
||||
if (string.IsNullOrEmpty(currentFile)) return;
|
||||
|
||||
var dir = Path.GetDirectoryName(currentFile)!;
|
||||
var name = Path.GetFileNameWithoutExtension(currentFile);
|
||||
var ext = Path.GetExtension(currentFile);
|
||||
var versionsDir = Path.Combine(dir, "_versions");
|
||||
if (!Directory.Exists(versionsDir)) return;
|
||||
|
||||
var pattern = $"{name}_*{ext}";
|
||||
var files = Directory.GetFiles(versionsDir, pattern).OrderByDescending(f => f).ToList();
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var metaFile = file + ".meta";
|
||||
string? comment = null;
|
||||
if (File.Exists(metaFile))
|
||||
{
|
||||
var lines = File.ReadAllLines(metaFile);
|
||||
if (lines.Length > 0 && lines[0].StartsWith("Kommentar: "))
|
||||
comment = lines[0]["Kommentar: ".Length..];
|
||||
}
|
||||
|
||||
var fileName = Path.GetFileNameWithoutExtension(file);
|
||||
var stamp = fileName[(name.Length + 1)..];
|
||||
var parsed = DateTime.TryParseExact(stamp, "yyyyMMdd_HHmmss", null,
|
||||
System.Globalization.DateTimeStyles.None, out var dt);
|
||||
|
||||
_versionList.Add(new VersionEntry(
|
||||
file,
|
||||
parsed ? dt : File.GetLastWriteTime(file),
|
||||
comment
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private async void ShowVersionDiff(VersionEntry version)
|
||||
{
|
||||
try
|
||||
{
|
||||
_versionDiffContent = await File.ReadAllTextAsync(version.FilePath);
|
||||
_selectedVersion = version;
|
||||
showVersionDiff = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
statusText = $"{Loc["common.error"]} {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private async void RestoreVersion(VersionEntry version)
|
||||
{
|
||||
try
|
||||
{
|
||||
editorContent = await File.ReadAllTextAsync(version.FilePath);
|
||||
_contentModified = editorContent != _lastSavedContent;
|
||||
ResetAutosaveTimer();
|
||||
statusText = $"{Loc["editor.version.status_loaded_prefix"]} {version.Timestamp:dd.MM.yyyy HH:mm} {Loc["editor.version.status_loaded_suffix"]}";
|
||||
showVersionBrowser = false;
|
||||
showVersionDiff = false;
|
||||
_selectedVersion = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
statusText = $"{Loc["common.error"]} {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WrapSelection(string prefix, string suffix)
|
||||
=> await JS.InvokeVoidAsync("EditorToolbar.wrapSelection", prefix, suffix);
|
||||
|
||||
private async Task InsertHeading(int level)
|
||||
=> await JS.InvokeVoidAsync("EditorToolbar.insertHeading", level);
|
||||
|
||||
private async Task InsertList(bool ordered)
|
||||
=> await JS.InvokeVoidAsync("EditorToolbar.insertList", ordered);
|
||||
|
||||
private async Task IndentLines()
|
||||
=> await JS.InvokeVoidAsync("EditorToolbar.indentLines");
|
||||
|
||||
private async Task OutdentLines()
|
||||
=> await JS.InvokeVoidAsync("EditorToolbar.outdentLines");
|
||||
|
||||
private void RefreshLatestVersion()
|
||||
{
|
||||
latestVersion = null;
|
||||
if (string.IsNullOrEmpty(currentFile)) return;
|
||||
|
||||
var dir = Path.GetDirectoryName(currentFile)!;
|
||||
var name = Path.GetFileNameWithoutExtension(currentFile);
|
||||
var ext = Path.GetExtension(currentFile);
|
||||
var versionsDir = Path.Combine(dir, "_versions");
|
||||
if (!Directory.Exists(versionsDir)) return;
|
||||
|
||||
var pattern = $"{name}_*{ext}";
|
||||
var files = Directory.GetFiles(versionsDir, pattern).OrderByDescending(f => f).ToList();
|
||||
if (files.Count == 0) return;
|
||||
|
||||
var latest = files.First();
|
||||
var metaFile = latest + ".meta";
|
||||
string? comment = null;
|
||||
if (File.Exists(metaFile))
|
||||
{
|
||||
var lines = File.ReadAllLines(metaFile);
|
||||
if (lines.Length > 0 && lines[0].StartsWith("Kommentar: "))
|
||||
comment = lines[0]["Kommentar: ".Length..];
|
||||
}
|
||||
|
||||
var fileName = Path.GetFileNameWithoutExtension(latest);
|
||||
var stamp = fileName[(name.Length + 1)..];
|
||||
var parsed = DateTime.TryParseExact(stamp, "yyyyMMdd_HHmmss", null,
|
||||
System.Globalization.DateTimeStyles.None, out var dt);
|
||||
latestVersion = new VersionEntry(
|
||||
latest,
|
||||
parsed ? dt : File.GetLastWriteTime(latest),
|
||||
comment
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
@page "/Error"
|
||||
@inject ILocalizationService Loc
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>@Loc["page.title.error"]</PageTitle>
|
||||
<PageTitle>Fehler</PageTitle>
|
||||
|
||||
<h1>@Loc["error.heading"]</h1>
|
||||
<p>@Loc["error.message"]</p>
|
||||
<a href="/">@Loc["error.link.back_to_home"]</a>
|
||||
<h1>Ein Fehler ist aufgetreten</h1>
|
||||
<p>Entschuldigung, es ist ein unerwarteter Fehler aufgetreten.</p>
|
||||
<a href="/">Zurück zur Startseite</a>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
|
|
|
|||
|
|
@ -1,240 +0,0 @@
|
|||
@page "/help"
|
||||
@attribute [Authorize]
|
||||
@inject ILocalizationService Loc
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<PageTitle>@Loc["page.title.help"] - Author Buddy</PageTitle>
|
||||
|
||||
<div class="help-page">
|
||||
<h3>@Loc["page.title.help"]</h3>
|
||||
|
||||
@if (Loc.CurrentLanguage == "en")
|
||||
{
|
||||
<section class="help-section">
|
||||
<h4 class="help-section-title">📋 Overview</h4>
|
||||
<p>AuthorBuddy is an AI-assisted writing environment for authors and requirements engineers. It connects to local LLMs (via Ollama or llama.cpp) to help you edit, analyze, and develop your texts.</p>
|
||||
<p>Key features at a glance: project management with templates, Markdown editor with versioning, AI writing assistant with multiple modes, spell checking, style analysis, fact-checking (RAG), and Wikipedia research.</p>
|
||||
</section>
|
||||
|
||||
<section class="help-section">
|
||||
<h4 class="help-section-title">📁 Managing Projects</h4>
|
||||
<p><strong>Creating a project:</strong> Click <em>File → New Project</em> in the sidebar. Choose between the <strong>Novel</strong> template (for fiction with drafts, facts, characters) or the <strong>Technical</strong> template (for requirements documents with specifications and use cases).</p>
|
||||
<p><strong>Opening a project:</strong> All your projects are listed under <em>Projects</em> in the sidebar. Click any project to open it in the editor.</p>
|
||||
<p><strong>Copying a project:</strong> Use the context menu on a project to create a full copy including all files and settings.</p>
|
||||
</section>
|
||||
|
||||
<section class="help-section">
|
||||
<h4 class="help-section-title">✍️ Editor</h4>
|
||||
<p>The editor is your main workspace for writing Markdown texts.</p>
|
||||
<ul>
|
||||
<li><strong>Autosave:</strong> Your text is saved automatically every 2 seconds.</li>
|
||||
<li><strong>Manual save:</strong> Press <kbd>Ctrl+S</kbd> or click <em>Save</em>.</li>
|
||||
<li><strong>Versions:</strong> Click <em>Version</em> to create a snapshot. Browse and restore old versions via the <em>Versions</em> button.</li>
|
||||
<li><strong>Diff view:</strong> Compare any two versions side by side.</li>
|
||||
<li><strong>Spell check:</strong> Click <em>Spell Check</em> to have the AI correct spelling and grammar.</li>
|
||||
<li><strong>Style check:</strong> Click <em>Style Check</em> to get AI feedback on your writing style.</li>
|
||||
<li><strong>RAG search:</strong> After indexing your project, search through your own texts via the <em>Facts (RAG)</em> button.</li>
|
||||
<li><strong>Wikipedia research:</strong> Search Wikipedia (German and English) directly from the editor. Save articles as facts.</li>
|
||||
<li><strong>File categories:</strong> Tag your <code>.md</code> files with 🏷 in the file tree. Categories include Character, Exposé, Fact, and Document. The Exposé is automatically used as context in the assistant.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="help-section">
|
||||
<h4 class="help-section-title">🤖 AI Assistant</h4>
|
||||
<p>The assistant offers multiple specialized modes. Select a mode from the dropdown, enter your text, and click <em>Execute</em>.</p>
|
||||
|
||||
<h5>Novel modes (always available):</h5>
|
||||
<ul>
|
||||
<li><strong>Editing:</strong> Style optimization — makes your text more vivid and engaging.</li>
|
||||
<li><strong>Advocatus Diaboli:</strong> Takes a radical opposing view to reveal weaknesses in your argument.</li>
|
||||
<li><strong>Hook Finder:</strong> Generates 5 captivating opening sentences.</li>
|
||||
<li><strong>ELI5:</strong> Simplifies complex passages.</li>
|
||||
<li><strong>Co-Author:</strong> Creative brainstorming for plot and character development.</li>
|
||||
<li><strong>RAG:</strong> Factual query against your project's indexed content.</li>
|
||||
</ul>
|
||||
|
||||
<h5>Technical modes (only with Technical template):</h5>
|
||||
<ul>
|
||||
<li><strong>Requirements Spec:</strong> Create a requirements specification per IREB standards.</li>
|
||||
<li><strong>Functional Spec:</strong> Create a detailed functional specification.</li>
|
||||
<li><strong>Use Cases:</strong> Define use cases with actors, flows, and exceptions.</li>
|
||||
<li><strong>Stakeholder Analysis:</strong> Identify and analyze project stakeholders.</li>
|
||||
<li><strong>Glossary:</strong> Create a technical glossary.</li>
|
||||
<li><strong>Quality Requirements:</strong> Define quality requirements per ISO 25010.</li>
|
||||
<li><strong>Risk Analysis:</strong> Identify and assess project risks.</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Temperature:</strong> The temperature slider controls the AI's creativity (0 = precise/factual, 1 = balanced, 2 = chaotic/experimental).</p>
|
||||
<p><strong>Custom prompts:</strong> Click <em>Edit Prompt</em> to modify the system prompt for the current session. <em>Reset</em> restores the default.</p>
|
||||
<p><strong>Send to editor:</strong> Click the button to transfer the assistant's output directly into the editor.</p>
|
||||
</section>
|
||||
|
||||
<section class="help-section">
|
||||
<h4 class="help-section-title">⚙️ Settings</h4>
|
||||
<p>In the settings page you can configure your personal preferences:</p>
|
||||
<ul>
|
||||
<li><strong>Appearance:</strong> Choose between Light, Dark, and Retro (Windows 95) themes.</li>
|
||||
<li><strong>Language:</strong> Switch between German and English.</li>
|
||||
<li><strong>Prompts:</strong> Customize the system prompts and temperatures for each assistant mode.</li>
|
||||
</ul>
|
||||
<p>When a project is loaded, settings apply to that project. Without a project, your personal default settings are used.</p>
|
||||
</section>
|
||||
|
||||
<section class="help-section">
|
||||
<h4 class="help-section-title">❓ FAQ</h4>
|
||||
<dl>
|
||||
<dt>How do I index my project for RAG search?</dt>
|
||||
<dd>Open the editor with your project, then click <em>Index Project</em>. This scans all <code>.md</code> files and creates embeddings for similarity search.</dd>
|
||||
|
||||
<dt>Why can't I see the technical assistant modes?</dt>
|
||||
<dd>Technical modes (Requirements Spec, Use Cases, etc.) only appear when you have a project with the <strong>Technical</strong> template open.</dd>
|
||||
|
||||
<dt>Why are some file categories disabled?</dt>
|
||||
<dd>Exposé and Document categories cannot be combined with other categories. Only one file per project can be the Exposé.</dd>
|
||||
|
||||
<dt>How do I set up 2FA?</dt>
|
||||
<dd>Go to <em>Settings → Set up 2FA</em>, scan the QR code with an authenticator app, and enter a verification code.</dd>
|
||||
</dl>
|
||||
</section>
|
||||
}
|
||||
else
|
||||
{
|
||||
<section class="help-section">
|
||||
<h4 class="help-section-title">📋 Übersicht</h4>
|
||||
<p>AuthorBuddy ist eine KI-gestützte Schreibumgebung für Autoren und Requirements Engineers. Sie verbindet sich mit lokalen LLMs (via Ollama oder llama.cpp) und hilft dir, Texte zu bearbeiten, zu analysieren und weiterzuentwickeln.</p>
|
||||
<p>Die wichtigsten Funktionen auf einen Blick: Projektverwaltung mit Vorlagen, Markdown-Editor mit Versionierung, KI-Assistent mit mehreren Modi, Rechtschreibprüfung, Stilanalyse, Faktenabfrage (RAG) und Wikipedia-Recherche.</p>
|
||||
</section>
|
||||
|
||||
<section class="help-section">
|
||||
<h4 class="help-section-title">📁 Projekte verwalten</h4>
|
||||
<p><strong>Projekt anlegen:</strong> Klicke in der Seitenleiste auf <em>Datei → Neues Projekt</em>. Wähle zwischen der <strong>Roman</strong>-Vorlage (für Belletristik mit Drafts, Fakten, Charakteren) oder der <strong>Technischen</strong> Vorlage (für Anforderungsdokumente mit Lastenheft und Use Cases).</p>
|
||||
<p><strong>Projekt öffnen:</strong> Alle Projekte werden unter <em>Projekte</em> in der Seitenleiste aufgelistet. Klicke auf ein Projekt, um es im Editor zu öffnen.</p>
|
||||
<p><strong>Projekt kopieren:</strong> Nutze das Kontextmenü eines Projekts, um eine vollständige Kopie inkl. aller Dateien und Einstellungen zu erstellen.</p>
|
||||
</section>
|
||||
|
||||
<section class="help-section">
|
||||
<h4 class="help-section-title">✍️ Editor</h4>
|
||||
<p>Der Editor ist dein zentraler Arbeitsbereich zum Schreiben von Markdown-Texten.</p>
|
||||
<ul>
|
||||
<li><strong>Autosave:</strong> Dein Text wird automatisch alle 2 Sekunden gespeichert.</li>
|
||||
<li><strong>Manuell speichern:</strong> Drücke <kbd>Strg+S</kbd> oder klicke auf <em>Speichern</em>.</li>
|
||||
<li><strong>Versionierung:</strong> Klicke auf <em>Version</em>, um einen Snapshot zu erstellen. Alte Versionen kannst du über den <em>Versionen</em>-Button durchsuchen und wiederherstellen.</li>
|
||||
<li><strong>Diff-Ansicht:</strong> Vergleiche zwei Versionen nebeneinander.</li>
|
||||
<li><strong>Rechtschreibprüfung:</strong> Klicke auf <em>Rechtschreibung prüfen</em>, um Rechtschreib- und Grammatikfehler korrigieren zu lassen.</li>
|
||||
<li><strong>Stilanalyse:</strong> Klicke auf <em>Stil prüfen</em>, um KI-Feedback zu deinem Schreibstil zu erhalten.</li>
|
||||
<li><strong>RAG-Suche:</strong> Nach dem Indexieren deines Projekts kannst du über <em>Fakten suchen (RAG)</em> in deinen eigenen Texten suchen.</li>
|
||||
<li><strong>Wikipedia-Recherche:</strong> Suche direkt aus dem Editor auf Wikipedia (DE+EN). Speichere Artikel als Fakten.</li>
|
||||
<li><strong>Datei-Kategorien:</strong> Markiere deine <code>.md</code>-Dateien im Dateibaum mit 🏷. Kategorien: Charakter, Exposé, Fakt, Dokument. Das Exposé wird automatisch als Kontext im Assistenten verwendet.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="help-section">
|
||||
<h4 class="help-section-title">🤖 KI-Assistent</h4>
|
||||
<p>Der Assistent bietet verschiedene spezialisierte Modi. Wähle einen Modus aus dem Dropdown, gib deinen Text ein und klicke auf <em>Ausführen</em>.</p>
|
||||
|
||||
<h5>Roman-Modi (immer verfügbar):</h5>
|
||||
<ul>
|
||||
<li><strong>Lektorat:</strong> Stiloptimierung – macht den Text lebendiger und anschaulicher.</li>
|
||||
<li><strong>Advocatus Diaboli:</strong> Radikale Gegenposition, um Schwachstellen aufzuzeigen.</li>
|
||||
<li><strong>Hook-Finder:</strong> Generiert 5 fesselnde Einstiegssätze.</li>
|
||||
<li><strong>ELI5:</strong> Vereinfacht komplexe Passagen.</li>
|
||||
<li><strong>Co-Author:</strong> Kreatives Brainstorming für Handlung und Charaktere.</li>
|
||||
<li><strong>RAG:</strong> Faktenabfrage aus dem projekteigenen Index.</li>
|
||||
</ul>
|
||||
|
||||
<h5>Technische Modi (nur bei technischer Vorlage):</h5>
|
||||
<ul>
|
||||
<li><strong>Lastenheft:</strong> Erstellung eines Lastenhefts nach IREB-Standard.</li>
|
||||
<li><strong>Pflichtenheft:</strong> Detailliertes Pflichtenheft mit technischer Umsetzung.</li>
|
||||
<li><strong>Use Cases:</strong> Definition von Anwendungsfällen mit Akteuren und Abläufen.</li>
|
||||
<li><strong>Stakeholder-Analyse:</strong> Identifikation und Analyse aller Projektbeteiligten.</li>
|
||||
<li><strong>Glossar:</strong> Erstellung eines Fachglossars.</li>
|
||||
<li><strong>Qualitätsanforderungen:</strong> Definition nach ISO 25010.</li>
|
||||
<li><strong>Risikoanalyse:</strong> Identifikation und Bewertung von Projektrisiken.</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Temperatur:</strong> Der Temperatur-Regler beeinflusst die Kreativität der KI (0 = präzise/faktentreu, 1 = ausgewogen, 2 = chaotisch/experimentell).</p>
|
||||
<p><strong>Prompt bearbeiten:</strong> Klicke auf <em>Prompt bearbeiten</em>, um den System-Prompt für die aktuelle Session anzupassen. <em>Zurücksetzen</em> lädt den Standard-Prompt.</p>
|
||||
<p><strong>An Editor übermitteln:</strong> Der Button überträgt die Ausgabe des Assistenten direkt in den Editor.</p>
|
||||
</section>
|
||||
|
||||
<section class="help-section">
|
||||
<h4 class="help-section-title">⚙️ Einstellungen</h4>
|
||||
<p>In den Einstellungen kannst du deine persönlichen Präferenzen konfigurieren:</p>
|
||||
<ul>
|
||||
<li><strong>Design:</strong> Wähle zwischen Hell, Dunkel und Retro (Windows 95).</li>
|
||||
<li><strong>Sprache:</strong> Wechsle zwischen Deutsch und Englisch.</li>
|
||||
<li><strong>Prompts:</strong> Passe die System-Prompts und Temperaturen für jeden Assistenten-Modus an.</li>
|
||||
</ul>
|
||||
<p>Wenn ein Projekt geladen ist, gelten die Einstellungen für dieses Projekt. Ohne Projekt werden deine persönlichen Standard-Einstellungen verwendet.</p>
|
||||
</section>
|
||||
|
||||
<section class="help-section">
|
||||
<h4 class="help-section-title">❓ FAQ</h4>
|
||||
<dl>
|
||||
<dt>Wie indexiere ich mein Projekt für die RAG-Suche?</dt>
|
||||
<dd>Öffne den Editor mit deinem Projekt und klicke auf <em>Projekt indexieren</em>. Dabei werden alle <code>.md</code>-Dateien gescannt und Embeddings für die Ähnlichkeitssuche erstellt.</dd>
|
||||
|
||||
<dt>Warum sehe ich die technischen Assistenten-Modi nicht?</dt>
|
||||
<dd>Die technischen Modi (Lastenheft, Use Cases usw.) erscheinen nur, wenn du ein Projekt mit der <strong>Technischen</strong> Vorlage geöffnet hast.</dd>
|
||||
|
||||
<dt>Warum sind manche Datei-Kategorien deaktiviert?</dt>
|
||||
<dd>Exposé und Dokument können nicht mit anderen Kategorien kombiniert werden. Pro Projekt kann nur eine Datei das Exposé sein.</dd>
|
||||
|
||||
<dt>Wie richte ich 2FA ein?</dt>
|
||||
<dd>Gehe zu <em>Einstellungen → 2FA einrichten</em>, scanne den QR-Code mit einer Authenticator-App und gib einen Bestätigungscode ein.</dd>
|
||||
</dl>
|
||||
</section>
|
||||
}
|
||||
|
||||
<button class="btn btn-secondary" @onclick='() => Navigation.NavigateTo("/")' style="margin-top:24px;">← @(Loc.CurrentLanguage == "en" ? "Back to Dashboard" : "Zurück zum Dashboard")</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.help-page {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
.help-section {
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.help-section-title {
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
.help-page p {
|
||||
margin: 6px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.help-page ul {
|
||||
margin: 6px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.help-page li {
|
||||
margin: 3px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.help-page dl dt {
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.help-page dl dd {
|
||||
margin-left: 0;
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.help-page kbd {
|
||||
padding: 1px 5px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
background: var(--menu-bg);
|
||||
}
|
||||
.help-page h5 {
|
||||
margin: 10px 0 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,14 +1,11 @@
|
|||
@page "/"
|
||||
@attribute [Authorize]
|
||||
@inject IOllamaService OllamaService
|
||||
@inject IProjectService ProjectService
|
||||
@inject ILocalizationService Loc
|
||||
|
||||
<PageTitle>@Loc["page.title.home"]</PageTitle>
|
||||
<PageTitle>Dashboard - Author Buddy</PageTitle>
|
||||
|
||||
<div class="tab-bar">
|
||||
<div class="tab @GetTabClass("rag")" @onclick='() => activeTab = "rag"'>@Loc["home.tab.rag_facts"]</div>
|
||||
<div class="tab @GetTabClass("style")" @onclick='() => activeTab = "style"'>@Loc["home.tab.style_analysis"]</div>
|
||||
<div class="tab @GetTabClass("rag")" @onclick='() => activeTab = "rag"'>Gefundene Fakten (RAG)</div>
|
||||
<div class="tab @GetTabClass("style")" @onclick='() => activeTab = "style"'>Stil-Analyse</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
|
|
@ -16,7 +13,7 @@
|
|||
{
|
||||
@if (foundFacts.Count == 0)
|
||||
{
|
||||
<p style="color: var(--border-color);">@Loc["home.rag.empty"]</p>
|
||||
<p style="color: var(--border-color);">Keine Fakten gefunden. Öffne ein Projekt und scanne die Dateien.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,165 +0,0 @@
|
|||
@page "/login"
|
||||
@inject IAuthService AuthService
|
||||
@inject CustomAuthStateProvider AuthStateProvider
|
||||
@inject NavigationManager Navigation
|
||||
@inject ISettingsService SettingsService
|
||||
@inject ILocalizationService Loc
|
||||
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>@Loc["page.title.login"]</PageTitle>
|
||||
|
||||
<div class="auth-page">
|
||||
<div class="auth-card">
|
||||
<div class="auth-logo">
|
||||
<img src="/Assets/logo.jpg" alt="Author Buddy" />
|
||||
</div>
|
||||
<h2>@Loc["login.heading"]</h2>
|
||||
|
||||
@if (!show2FA)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label for="username">@Loc["login.label.username"]</label>
|
||||
<input id="username" @bind="username" @onkeydown="HandleKeyDown" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">@Loc["login.label.password"]</label>
|
||||
<input id="password" type="password" @bind="password" @onkeydown="HandleKeyDown" />
|
||||
</div>
|
||||
<div class="auth-actions">
|
||||
<button @onclick="DoLogin" disabled="@isBusy">@Loc["login.button.submit"]</button>
|
||||
<a href="/register">@Loc["login.link.register"]</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>@Loc["login.2fa.instruction"]</p>
|
||||
<div class="form-group">
|
||||
<label for="code">@Loc["login.2fa.label.code"]</label>
|
||||
<input id="code" @bind="twoFactorCode" maxlength="6" @onkeydown="HandleKeyDown" />
|
||||
</div>
|
||||
<div class="auth-actions">
|
||||
<button @onclick="Verify2FA" disabled="@isBusy">@Loc["login.2fa.button.verify"]</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
<div class="auth-error">@errorMessage</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string username = string.Empty;
|
||||
private string password = string.Empty;
|
||||
private string twoFactorCode = string.Empty;
|
||||
private string errorMessage = string.Empty;
|
||||
private bool isBusy;
|
||||
private bool show2FA;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (AuthService.IsLoggedIn)
|
||||
{
|
||||
Navigation.NavigateTo("/");
|
||||
return;
|
||||
}
|
||||
|
||||
var users = await SettingsService.GetUsersAsync();
|
||||
if (users.Count == 0)
|
||||
{
|
||||
Navigation.NavigateTo("/register");
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender && AuthService.IsLoggedIn)
|
||||
Navigation.NavigateTo("/");
|
||||
}
|
||||
|
||||
private async Task HandleKeyDown(KeyboardEventArgs e)
|
||||
{
|
||||
if (e.Key == "Enter")
|
||||
{
|
||||
if (show2FA)
|
||||
await Verify2FA();
|
||||
else
|
||||
await DoLogin();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DoLogin()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
errorMessage = Loc["login.error.empty_fields"];
|
||||
return;
|
||||
}
|
||||
|
||||
isBusy = true;
|
||||
errorMessage = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
var result = await AuthService.LoginAsync(username, password);
|
||||
if (AuthService.Is2FARequired)
|
||||
{
|
||||
show2FA = true;
|
||||
password = string.Empty;
|
||||
}
|
||||
else if (result)
|
||||
{
|
||||
AuthStateProvider.NotifyStateChanged();
|
||||
Navigation.NavigateTo("/");
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = Loc["login.error.invalid_credentials"];
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"{Loc["common.error"]} {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Verify2FA()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(twoFactorCode) || twoFactorCode.Length != 6)
|
||||
{
|
||||
errorMessage = Loc["login.2fa.error.invalid_code"];
|
||||
return;
|
||||
}
|
||||
|
||||
isBusy = true;
|
||||
errorMessage = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
var result = await AuthService.Verify2FAAsync(twoFactorCode);
|
||||
if (result)
|
||||
{
|
||||
AuthStateProvider.NotifyStateChanged();
|
||||
Navigation.NavigateTo("/");
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = Loc["login.2fa.error.code_failed"];
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"{Loc["common.error"]} {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isBusy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,18 @@
|
|||
@page "/models"
|
||||
@attribute [Authorize]
|
||||
@inject IOllamaService OllamaService
|
||||
@inject NavigationManager Navigation
|
||||
@inject ILocalizationService Loc
|
||||
@using Ollama
|
||||
|
||||
|
||||
<PageTitle>@Loc["page.title.models"]</PageTitle>
|
||||
<PageTitle>Modelle - Author Buddy</PageTitle>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||
<h3>@Loc["models.heading"]</h3>
|
||||
<button @onclick="RefreshModels" disabled="@isLoading">@Loc["models.button.refresh"]</button>
|
||||
<h3>Ollama Modelle verwalten</h3>
|
||||
<button @onclick="RefreshModels" disabled="@isLoading">Aktualisieren</button>
|
||||
</div>
|
||||
|
||||
@if (isLoading)
|
||||
{
|
||||
<p>@Loc["models.status.loading"]</p>
|
||||
<p>Lade Modelle...</p>
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
|
|
@ -26,10 +24,10 @@ else
|
|||
@foreach (var model in localModels)
|
||||
{
|
||||
<div class="model-card">
|
||||
<div class="model-name">@model.Name</div>
|
||||
<div class="model-name">@model.Model1</div>
|
||||
<div class="model-details">
|
||||
@Loc["models.label.size"] @FormatSize(model.Size) |
|
||||
@Loc["models.label.modified"] @model.ModifiedAt?.ToString("g")
|
||||
Größe: @FormatSize(model.Size) |
|
||||
Geändert: @model.ModifiedAt?.ToString("g")
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -37,14 +35,14 @@ else
|
|||
}
|
||||
|
||||
<div style="margin-top: 24px; border-top: 1px solid var(--border-color); padding-top: 16px;">
|
||||
<h4 style="margin-bottom: 8px;">@Loc["models.pull.heading"]</h4>
|
||||
<h4 style="margin-bottom: 8px;">Neues Modell herunterladen</h4>
|
||||
<div style="display: flex; gap: 8px; align-items: flex-end;">
|
||||
<div class="form-group" style="flex: 1; margin-bottom: 0;">
|
||||
<label for="pullModelName">@Loc["models.pull.label.name"]</label>
|
||||
<input id="pullModelName" @bind="modelToPull" placeholder="@Loc["models.pull.placeholder"]" />
|
||||
<label for="pullModelName">Modellname:</label>
|
||||
<input id="pullModelName" @bind="modelToPull" placeholder="z.B. llama3.2" />
|
||||
</div>
|
||||
<button @onclick="PullModel" disabled="@(isPulling || string.IsNullOrWhiteSpace(modelToPull))">
|
||||
@Loc["models.pull.button.download"]
|
||||
Herunterladen
|
||||
</button>
|
||||
</div>
|
||||
@if (isPulling)
|
||||
|
|
@ -66,7 +64,7 @@ else
|
|||
}
|
||||
|
||||
@code {
|
||||
private List<LLM_Model> localModels = new();
|
||||
private List<Ollama.Model> localModels = new();
|
||||
private bool isLoading;
|
||||
private string? errorMessage;
|
||||
private string modelToPull = string.Empty;
|
||||
|
|
@ -93,7 +91,7 @@ else
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"{Loc["models.error.loading"]} {ex.Message}";
|
||||
errorMessage = $"Fehler beim Laden der Modelle: {ex.Message}";
|
||||
localModels = new();
|
||||
}
|
||||
finally
|
||||
|
|
@ -108,25 +106,26 @@ else
|
|||
|
||||
isPulling = true;
|
||||
pullProgress = 0;
|
||||
pullStatus = Loc["models.pull.status.starting"];
|
||||
pullStatus = "Starte Download...";
|
||||
pullResult = null;
|
||||
|
||||
try
|
||||
{
|
||||
var progress = new Progress<string>(status =>
|
||||
var progress = new Progress<ModelStatus>(status =>
|
||||
{
|
||||
pullStatus = status;
|
||||
pullProgress = status.Progress;
|
||||
pullStatus = status.Status;
|
||||
InvokeAsync(StateHasChanged);
|
||||
});
|
||||
|
||||
await OllamaService.PullModelAsync(modelToPull, progress);
|
||||
pullResult = $"{Loc["models.pull.status.model"]} '{modelToPull}' {Loc["models.pull.status.success"]}";
|
||||
pullResult = $"Modell '{modelToPull}' erfolgreich heruntergeladen.";
|
||||
modelToPull = string.Empty;
|
||||
await RefreshModels();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
pullResult = $"{Loc["common.error"]} {ex.Message}";
|
||||
pullResult = $"Fehler: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -136,7 +135,7 @@ else
|
|||
|
||||
private static string FormatSize(long? bytes)
|
||||
{
|
||||
if (!bytes.HasValue) return "---";
|
||||
if (!bytes.HasValue) return "Unbekannt";
|
||||
if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB";
|
||||
if (bytes < 1024 * 1024 * 1024) return $"{bytes / (1024.0 * 1024):F1} MB";
|
||||
return $"{bytes / (1024.0 * 1024 * 1024):F1} GB";
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
@page "/not-found"
|
||||
@inject ILocalizationService Loc
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>@Loc["page.title.not_found"]</PageTitle>
|
||||
<PageTitle>Seite nicht gefunden</PageTitle>
|
||||
|
||||
<h1>@Loc["notfound.heading"]</h1>
|
||||
<p>@Loc["notfound.message"]</p>
|
||||
<a href="/">@Loc["notfound.link.back_to_home"]</a>
|
||||
<h1>404 - Seite nicht gefunden</h1>
|
||||
<p>Die angeforderte Seite existiert nicht.</p>
|
||||
<a href="/">Zurück zur Startseite</a>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
|
|
|
|||
|
|
@ -1,280 +0,0 @@
|
|||
@page "/assistant"
|
||||
@attribute [Authorize]
|
||||
@inject IOllamaService OllamaService
|
||||
@inject ISettingsService SettingsService
|
||||
@inject IRagService RagService
|
||||
@inject IProjectService ProjectService
|
||||
@inject NavigationManager Navigation
|
||||
@inject ILocalizationService Loc
|
||||
@inject OperationState OpState
|
||||
|
||||
<PageTitle>@Loc["page.title.assistant"]</PageTitle>
|
||||
|
||||
<div class="assistant-page">
|
||||
<div class="assistant-toolbar">
|
||||
<select value="@selectedMode" @onchange="OnModeChanged">
|
||||
<option value="lektorat">@Loc["assistant.mode.lektorat"]</option>
|
||||
<option value="advocatus">@Loc["assistant.mode.advocatus"]</option>
|
||||
<option value="hookfinder">@Loc["assistant.mode.hookfinder"]</option>
|
||||
<option value="eli5">@Loc["assistant.mode.eli5"]</option>
|
||||
<option value="coauthor">@Loc["assistant.mode.coauthor"]</option>
|
||||
<option value="rag">@Loc["assistant.mode.rag"]</option>
|
||||
@if (currentTemplate == ProjectTemplate.Technical)
|
||||
{
|
||||
<option disabled style="font-style:italic;">── Requirements ──</option>
|
||||
<option value="tech_lastenheft">@Loc["assistant.mode.tech_lastenheft"]</option>
|
||||
<option value="tech_pflichtenheft">@Loc["assistant.mode.tech_pflichtenheft"]</option>
|
||||
<option value="tech_usecase">@Loc["assistant.mode.tech_usecase"]</option>
|
||||
<option value="tech_stakeholder">@Loc["assistant.mode.tech_stakeholder"]</option>
|
||||
<option value="tech_glossar">@Loc["assistant.mode.tech_glossar"]</option>
|
||||
<option value="tech_qualitaet">@Loc["assistant.mode.tech_qualitaet"]</option>
|
||||
<option value="tech_risiko">@Loc["assistant.mode.tech_risiko"]</option>
|
||||
}
|
||||
</select>
|
||||
<button @onclick="RunAssistant" disabled="@(() => isRunning || string.IsNullOrWhiteSpace(inputText))">@Loc["assistant.button.execute"]</button>
|
||||
@if (!string.IsNullOrEmpty(outputText))
|
||||
{
|
||||
<button @onclick="SendToEditor" class="primary">@Loc["assistant.button.send_to_editor"]</button>
|
||||
}
|
||||
<button @onclick='() => showPromptEditor = !showPromptEditor' class="btn-small">
|
||||
@(showPromptEditor ? Loc["assistant.button.hide_prompt"] : Loc["assistant.button.edit_prompt"])
|
||||
</button>
|
||||
<span style="flex:1;font-size:12px;color:var(--border-color);text-align:right;">@statusText</span>
|
||||
@if (isRunning)
|
||||
{
|
||||
<progress value="@progress" max="100"></progress>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (showPromptEditor && selectedMode != "rag")
|
||||
{
|
||||
<div class="assistant-prompt-editor">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
|
||||
<strong style="font-size:13px;">@Loc["assistant.label.system_prompt"]</strong>
|
||||
<button class="btn-small" @onclick="ResetPrompt">@Loc["assistant.button.reset_prompt"]</button>
|
||||
</div>
|
||||
<textarea class="assistant-prompt-textarea" @bind="promptOverride" rows="4"></textarea>
|
||||
<div class="temp-slider" style="margin-top:6px;">
|
||||
<label class="temp-label">@Loc["settings.temperature.label"] <span class="temp-value">@temperatureOverride.ToString("F1")</span></label>
|
||||
<input type="range" min="0" max="2" step="0.1" value="@Fmt(temperatureOverride)" @oninput="OnTempChanged" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="assistant-panels">
|
||||
<div class="assistant-input-panel">
|
||||
<h4 style="display:inline">@Loc["assistant.label.input_text"]</h4>
|
||||
<CopyCreateToolbar Text="@inputText" DefaultFileName="assistent_eingabe" />
|
||||
<textarea class="assistant-textarea" @bind="inputText" placeholder="@Loc["assistant.placeholder.input"]"></textarea>
|
||||
</div>
|
||||
<div class="assistant-output-panel">
|
||||
<h4 style="display:inline">@Loc["assistant.label.result"]</h4>
|
||||
<CopyCreateToolbar Text="@outputText" DefaultFileName="assistent_ausgabe" />
|
||||
<div class="assistant-output">
|
||||
@if (!string.IsNullOrEmpty(outputText))
|
||||
{
|
||||
<pre>@outputText</pre>
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
<div class="assistant-error">@errorMessage</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="assistant-hint">@Loc["assistant.hint.empty_result"]</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string selectedMode = "lektorat";
|
||||
private string inputText = string.Empty;
|
||||
private string outputText = string.Empty;
|
||||
private string errorMessage = string.Empty;
|
||||
private string statusText = string.Empty;
|
||||
private bool isRunning;
|
||||
private double progress;
|
||||
private bool showPromptEditor;
|
||||
private string promptOverride = string.Empty;
|
||||
private double temperatureOverride = 0.7;
|
||||
private ProjectTemplate currentTemplate = ProjectTemplate.Novel;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
statusText = Loc["common.status.ready"];
|
||||
await LoadPromptForMode();
|
||||
|
||||
var pid = SettingsService.CurrentProjectId;
|
||||
if (pid != null)
|
||||
{
|
||||
var project = await ProjectService.GetProjectAsync(pid.Value);
|
||||
if (project != null)
|
||||
currentTemplate = project.Template;
|
||||
}
|
||||
|
||||
var pending = SettingsService.PendingAssistantInput;
|
||||
if (!string.IsNullOrEmpty(pending))
|
||||
{
|
||||
inputText = pending;
|
||||
SettingsService.PendingAssistantInput = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnModeChanged(ChangeEventArgs e)
|
||||
{
|
||||
selectedMode = e.Value?.ToString() ?? "lektorat";
|
||||
await LoadPromptForMode();
|
||||
}
|
||||
|
||||
private async Task LoadPromptForMode()
|
||||
{
|
||||
if (selectedMode == "rag")
|
||||
{
|
||||
promptOverride = string.Empty;
|
||||
temperatureOverride = 0.3;
|
||||
return;
|
||||
}
|
||||
|
||||
var (prompt, temp) = selectedMode switch
|
||||
{
|
||||
"lektorat" => (await SettingsService.GetLektorSystemPromptAsync() ?? string.Empty, await SettingsService.GetLektorTempAsync()),
|
||||
"advocatus" => (await SettingsService.GetAdvocatusDiaboliPromptAsync() ?? string.Empty, await SettingsService.GetAdvocatusDiaboliTempAsync()),
|
||||
"hookfinder" => (await SettingsService.GetHookFinderPromptAsync() ?? string.Empty, await SettingsService.GetHookFinderTempAsync()),
|
||||
"eli5" => (await SettingsService.GetEli5PromptAsync() ?? string.Empty, await SettingsService.GetEli5TempAsync()),
|
||||
"coauthor" => (await SettingsService.GetCoAuthorBrainstormingPromptAsync() ?? string.Empty, await SettingsService.GetCoAuthorTempAsync()),
|
||||
"tech_lastenheft" => (await SettingsService.GetTechLastenheftPromptAsync() ?? string.Empty, await SettingsService.GetTechLastenheftTempAsync()),
|
||||
"tech_pflichtenheft" => (await SettingsService.GetTechPflichtenheftPromptAsync() ?? string.Empty, await SettingsService.GetTechPflichtenheftTempAsync()),
|
||||
"tech_usecase" => (await SettingsService.GetTechUseCasePromptAsync() ?? string.Empty, await SettingsService.GetTechUseCaseTempAsync()),
|
||||
"tech_stakeholder" => (await SettingsService.GetTechStakeholderPromptAsync() ?? string.Empty, await SettingsService.GetTechStakeholderTempAsync()),
|
||||
"tech_glossar" => (await SettingsService.GetTechGlossarPromptAsync() ?? string.Empty, await SettingsService.GetTechGlossarTempAsync()),
|
||||
"tech_qualitaet" => (await SettingsService.GetTechQualitaetPromptAsync() ?? string.Empty, await SettingsService.GetTechQualitaetTempAsync()),
|
||||
"tech_risiko" => (await SettingsService.GetTechRisikoPromptAsync() ?? string.Empty, await SettingsService.GetTechRisikoTempAsync()),
|
||||
_ => (string.Empty, 0.0)
|
||||
};
|
||||
|
||||
promptOverride = prompt;
|
||||
temperatureOverride = temp;
|
||||
}
|
||||
|
||||
private async Task ResetPrompt()
|
||||
{
|
||||
await LoadPromptForMode();
|
||||
}
|
||||
|
||||
private void OnTempChanged(ChangeEventArgs e)
|
||||
{
|
||||
if (e.Value is string s && double.TryParse(s, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var v))
|
||||
temperatureOverride = Math.Round(v, 1);
|
||||
}
|
||||
|
||||
private string Fmt(double v) => v.ToString("F1", System.Globalization.CultureInfo.InvariantCulture);
|
||||
|
||||
private async Task RunAssistant()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(inputText)) return;
|
||||
|
||||
isRunning = true;
|
||||
progress = 50;
|
||||
outputText = string.Empty;
|
||||
errorMessage = string.Empty;
|
||||
statusText = Loc["assistant.status.starting"];
|
||||
OpState.SetStatus(Loc["assistant.status.starting"], true);
|
||||
OpState.SetThought("");
|
||||
|
||||
try
|
||||
{
|
||||
if (selectedMode == "rag")
|
||||
{
|
||||
var projectId = SettingsService.CurrentProjectId;
|
||||
if (projectId == null)
|
||||
{
|
||||
errorMessage = Loc["assistant.error.no_project"];
|
||||
return;
|
||||
}
|
||||
|
||||
OpState.SetStatus(Loc["assistant.status.searching"], true);
|
||||
progress = 30;
|
||||
var results = await RagService.SearchAsync(projectId.Value, inputText, 5);
|
||||
if (results.Count == 0)
|
||||
{
|
||||
errorMessage = Loc["assistant.error.no_facts"];
|
||||
return;
|
||||
}
|
||||
|
||||
var facts = string.Join("\n\n---\n\n", results.Select(r => r.Text));
|
||||
progress = 70;
|
||||
OpState.SetStatus(Loc["assistant.status.processing"], true);
|
||||
outputText = await OllamaService.AnswerBasedOnFactsAsync(inputText, facts);
|
||||
OpState.SetThought(outputText);
|
||||
statusText = Loc["assistant.status.done"];
|
||||
OpState.SetStatus(Loc["assistant.status.done"], false);
|
||||
}
|
||||
else
|
||||
{
|
||||
var systemPrompt = promptOverride;
|
||||
|
||||
if (string.IsNullOrEmpty(systemPrompt))
|
||||
{
|
||||
errorMessage = Loc["assistant.error.no_prompt"];
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedMode is "coauthor" or "hookfinder" or "lektorat")
|
||||
{
|
||||
var projectId = SettingsService.CurrentProjectId;
|
||||
if (projectId != null)
|
||||
{
|
||||
var project = await ProjectService.GetProjectAsync(projectId.Value);
|
||||
if (project != null)
|
||||
{
|
||||
// Use Exposé file from category system
|
||||
var categories = await ProjectService.GetFileCategoriesAsync(projectId.Value);
|
||||
var exposeFile = categories.FirstOrDefault(c => c.Value == FileCategory.Exposee);
|
||||
if (!string.IsNullOrEmpty(exposeFile.Key) && File.Exists(exposeFile.Key))
|
||||
{
|
||||
var exposee = await File.ReadAllTextAsync(exposeFile.Key);
|
||||
if (!string.IsNullOrWhiteSpace(exposee))
|
||||
{
|
||||
systemPrompt += "\n\n" + Loc["assistant.exposee.context"] + "\n" + exposee;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var model = await SettingsService.GetStyleModelAsync() ?? "llama3.2";
|
||||
OpState.SetStatus(Loc["assistant.status.processing"], true);
|
||||
statusText = Loc["assistant.status.processing"];
|
||||
|
||||
await OllamaService.ChatStream(model, inputText, async chunk =>
|
||||
{
|
||||
outputText += chunk;
|
||||
OpState.AppendThought(chunk);
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}, systemPrompt, temperatureOverride);
|
||||
|
||||
statusText = Loc["assistant.status.done"];
|
||||
OpState.SetStatus(Loc["assistant.status.done"], false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"{Loc["common.error"]} {ex.Message}";
|
||||
statusText = Loc["common.status.error"];
|
||||
OpState.SetStatus($"{Loc["common.error"]} {ex.Message}", false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
isRunning = false;
|
||||
progress = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void SendToEditor()
|
||||
{
|
||||
SettingsService.PendingAssistantOutput = outputText;
|
||||
Navigation.NavigateTo("/editor");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,174 +0,0 @@
|
|||
@page "/setup-assistant"
|
||||
@inject IOllamaSetupService SetupService
|
||||
@inject ISettingsService SettingsService
|
||||
@inject OperationState OpState
|
||||
@inject ILocalizationService Loc
|
||||
@inject OllamaSettings CurrentSettings
|
||||
|
||||
<PageTitle>Ollama Setup Assistent</PageTitle>
|
||||
|
||||
<div class="setup-assistant-container">
|
||||
<div class="setup-header">
|
||||
<h1>Ollama Setup Assistent</h1>
|
||||
<p>Prüfen Sie Ihre Einstellungen für eine optimale Performance.</p>
|
||||
</div>
|
||||
|
||||
@if (recommendation == null)
|
||||
{
|
||||
<div class="setup-content centered">
|
||||
<button class="btn btn-primary" @onclick="RunAnalysis">Analyse starten</button>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="setup-content">
|
||||
<div class="status-card @(recommendation.IsCorrect ? "status-success" : "status-warning")">
|
||||
<h3>@recommendation.Message</h3>
|
||||
</div>
|
||||
|
||||
<div class="analysis-details">
|
||||
<h4>Analyse-Ergebnisse:</h4>
|
||||
<ul>
|
||||
@foreach (var finding in recommendation.Findings)
|
||||
{
|
||||
<li>@finding</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@if (!recommendation.IsCorrect)
|
||||
{
|
||||
<div class="recommendation-table">
|
||||
<h4>Empfohlene Einstellungen:</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Einstellung</th>
|
||||
<th>Aktuell</th>
|
||||
<th>Empfohlen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Ollama URL</td>
|
||||
<td>@CurrentSettings.OllamaUrl</td>
|
||||
<td>@recommendation.RecommendedSettings.OllamaUrl</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Style Model</td>
|
||||
<td>@CurrentSettings.StyleModel</td>
|
||||
<td>@recommendation.RecommendedSettings.StyleModel</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Embedding Model</td>
|
||||
<td>@CurrentSettings.EmbedModel</td>
|
||||
<td>@recommendation.RecommendedSettings.EmbedModel</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Spelling Model</td>
|
||||
<td>@CurrentSettings.SpellingModel</td>
|
||||
<td>@recommendation.RecommendedSettings.SpellingModel</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="setup-actions">
|
||||
<button class="btn btn-primary" @onclick="ApplySettings">Einstellungen übernehmen</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.setup-assistant-container {
|
||||
max-width: 800px;
|
||||
margin: 2rem auto;
|
||||
padding: 2rem;
|
||||
background: var(--bg-card);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow);
|
||||
color: var(--text-main);
|
||||
}
|
||||
.setup-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.setup-content.centered {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
}
|
||||
.status-card {
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
.status-success {
|
||||
background: var(--bg-success);
|
||||
color: var(--text-success);
|
||||
border: 1px solid var(--border-success);
|
||||
}
|
||||
.status-warning {
|
||||
background: var(--bg-warning);
|
||||
color: var(--text-warning);
|
||||
border: 1px solid var(--border-warning);
|
||||
}
|
||||
.analysis-details {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.analysis-details ul {
|
||||
list-style: disc;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
.recommendation-table table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.recommendation-table th, .recommendation-table td {
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.setup-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private SetupRecommendation? recommendation;
|
||||
private bool _applying;
|
||||
|
||||
private async Task RunAnalysis()
|
||||
{
|
||||
recommendation = await SetupService.AnalyzeSettingsAsync();
|
||||
}
|
||||
|
||||
private async Task ApplySettings()
|
||||
{
|
||||
if (recommendation?.RecommendedSettings == null || _applying) return;
|
||||
_applying = true;
|
||||
|
||||
try
|
||||
{
|
||||
var rec = recommendation.RecommendedSettings;
|
||||
|
||||
await SettingsService.SaveOllamaUrlAsync(rec.OllamaUrl);
|
||||
await SettingsService.SaveStyleModelAsync(rec.StyleModel);
|
||||
await SettingsService.SaveRagModelAsync(rec.EmbedModel);
|
||||
await SettingsService.SaveSpellingModelAsync(rec.SpellingModel);
|
||||
|
||||
OpState.SetStatus("Einstellungen wurden in der Datenbank gespeichert.", true);
|
||||
recommendation = await SetupService.AnalyzeSettingsAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_applying = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
@page "/register"
|
||||
@inject IAuthService AuthService
|
||||
@inject CustomAuthStateProvider AuthStateProvider
|
||||
@inject NavigationManager Navigation
|
||||
@inject ILocalizationService Loc
|
||||
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>@Loc["page.title.register"]</PageTitle>
|
||||
|
||||
<div class="auth-page">
|
||||
<div class="auth-card">
|
||||
<div class="auth-logo">
|
||||
<img src="/Assets/logo.jpg" alt="Author Buddy" />
|
||||
</div>
|
||||
<h2>@Loc["register.heading"]</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">@Loc["register.label.username"]</label>
|
||||
<input id="username" @bind="username" @onkeydown="HandleKeyDown" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">@Loc["register.label.password"]</label>
|
||||
<input id="password" type="password" @bind="password" @onkeydown="HandleKeyDown" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirmPassword">@Loc["register.label.confirm_password"]</label>
|
||||
<input id="confirmPassword" type="password" @bind="confirmPassword" @onkeydown="HandleKeyDown" />
|
||||
</div>
|
||||
<div class="auth-actions">
|
||||
<button @onclick="DoRegister" disabled="@isBusy">@Loc["register.button.submit"]</button>
|
||||
<a href="/login">@Loc["register.link.back_to_login"]</a>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
<div class="auth-error">@errorMessage</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string username = string.Empty;
|
||||
private string password = string.Empty;
|
||||
private string confirmPassword = string.Empty;
|
||||
private string errorMessage = string.Empty;
|
||||
private bool isBusy;
|
||||
|
||||
private async Task HandleKeyDown(KeyboardEventArgs e)
|
||||
{
|
||||
if (e.Key == "Enter")
|
||||
await DoRegister();
|
||||
}
|
||||
|
||||
private async Task DoRegister()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
{
|
||||
errorMessage = Loc["register.error.no_username"];
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
errorMessage = Loc["register.error.no_password"];
|
||||
return;
|
||||
}
|
||||
if (password != confirmPassword)
|
||||
{
|
||||
errorMessage = Loc["register.error.password_mismatch"];
|
||||
return;
|
||||
}
|
||||
if (password.Length < 4)
|
||||
{
|
||||
errorMessage = Loc["register.error.password_too_short"];
|
||||
return;
|
||||
}
|
||||
|
||||
isBusy = true;
|
||||
errorMessage = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
var result = await AuthService.RegisterAsync(username, password);
|
||||
if (result)
|
||||
{
|
||||
AuthStateProvider.NotifyStateChanged();
|
||||
Navigation.NavigateTo("/");
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = Loc["register.error.username_taken"];
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"{Loc["common.error"]} {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isBusy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,383 +1,98 @@
|
|||
@page "/settings"
|
||||
@attribute [Authorize]
|
||||
@rendermode InteractiveServer
|
||||
@inject ISettingsService SettingsService
|
||||
@inject IThemeService ThemeService
|
||||
@inject IDbContextFactory<AppDbContext> ContextFactory
|
||||
@inject IOllamaService OllamaService
|
||||
@inject OllamaSettings OllamaConfig
|
||||
@inject NavigationManager Navigation
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject IProjectService ProjectService
|
||||
@inject ILocalizationService Loc
|
||||
@inject IAuthService AuthService
|
||||
|
||||
<PageTitle>@Loc["page.title.settings"] - Author Buddy</PageTitle>
|
||||
<PageTitle>Einstellungen - Author Buddy</PageTitle>
|
||||
|
||||
<div class="settings-page">
|
||||
|
||||
@if (_isAdmin)
|
||||
{
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">@Loc["settings.section.user"]</h3>
|
||||
<h3 class="settings-section-title">Modelle</h3>
|
||||
<div class="form-group">
|
||||
<label for="userSelect">@Loc["settings.user.label.active"]</label>
|
||||
<select id="userSelect" value="@selectedUserId" @onchange="SwitchUser">
|
||||
@foreach (var u in users)
|
||||
{
|
||||
<option value="@u.Id">@u.Name (@(u.Role == AuthorBuddy.Web.Data.UserRole.Administrator ? Loc["settings.user.role.admin"] : Loc["settings.user.role.author"]))</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="newUserName">@Loc["settings.user.label.new"]</label>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<input id="newUserName" @bind="newUserName" placeholder="@Loc["settings.user.placeholder.name"]" style="flex: 1;" />
|
||||
<button class="btn btn-secondary" @onclick="AddUser">@Loc["settings.user.button.add"]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="settings-section" style="margin-bottom:16px;">
|
||||
<button class="btn btn-secondary" @onclick='() => Navigation.NavigateTo("/setup-2fa")'>@Loc["settings.user.link.2fa"]</button>
|
||||
</div>
|
||||
|
||||
<div class="settings-context">
|
||||
@if (currentProjectName != null)
|
||||
{
|
||||
<span class="settings-badge settings-badge-project">@Loc["settings.context.project"]: @currentProjectName</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="settings-badge settings-badge-user">@Loc["settings.context.user"]</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="settings-layout">
|
||||
|
||||
<div class="settings-column" >
|
||||
@if (_isAdmin)
|
||||
{
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">@Loc["settings.section.models"]</h3>
|
||||
<div class="form-group">
|
||||
<label for="ragModel">@Loc["settings.models.label.rag"]</label>
|
||||
<label for="ragModel">RAG-Modell (Faktenabfrage):</label>
|
||||
<select id="ragModel" @bind="ragModel">
|
||||
@if (availableModels.Count == 0)
|
||||
{
|
||||
<option value="">@Loc["settings.models.empty"]</option>
|
||||
<option value="">Keine Modelle gefunden</option>
|
||||
}
|
||||
@foreach (var m in availableModels)
|
||||
{
|
||||
<option value="@m.Name">@m.Name</option>
|
||||
<option value="@m.Model1">@m.Model1</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="styleModel">@Loc["settings.models.label.style"]</label>
|
||||
<label for="styleModel">Stil-Modell (Lektorat):</label>
|
||||
<select id="styleModel" @bind="styleModel">
|
||||
@if (availableModels.Count == 0)
|
||||
{
|
||||
<option value="">@Loc["settings.models.empty"]</option>
|
||||
<option value="">Keine Modelle gefunden</option>
|
||||
}
|
||||
@foreach (var m in availableModels)
|
||||
{
|
||||
<option value="@m.Name">@m.Name</option>
|
||||
<option value="@m.Model1">@m.Model1</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="spellingModel">@Loc["settings.models.label.spellcheck"]</label>
|
||||
<label for="spellingModel">Rechtschreibprüfungs-Modell:</label>
|
||||
<select id="spellingModel" @bind="spellingModel">
|
||||
@if (availableModels.Count == 0)
|
||||
{
|
||||
<option value="">@Loc["settings.models.empty"]</option>
|
||||
<option value="">Keine Modelle gefunden</option>
|
||||
}
|
||||
@foreach (var m in availableModels)
|
||||
{
|
||||
<option value="@m.Name">@m.Name</option>
|
||||
<option value="@m.Model1">@m.Model1</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">@Loc["settings.section.ollama_server"]</h3>
|
||||
<h3 class="settings-section-title">Ollama Server</h3>
|
||||
<div class="form-group">
|
||||
<label for="backendSelect">Backend</label>
|
||||
<select id="backendSelect" @bind="selectedBackend">
|
||||
<option value="Ollama">Ollama</option>
|
||||
<option value="llama.cpp">llama.cpp</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ollamaUrl">@Loc["settings.ollama.label.url"] (Ollama)</label>
|
||||
<div class="input-with-button">
|
||||
<label for="ollamaUrl">Server-URL:</label>
|
||||
<input id="ollamaUrl" @bind="ollamaUrl" placeholder="http://localhost:11434" />
|
||||
<button @onclick="TestConnection" class="btn-sm" disabled="@_isTestingConnection">@Loc["settings.ollama.test"]</button>
|
||||
</div>
|
||||
@if (_connectionTested)
|
||||
{
|
||||
<div class="ollama-status @(_connectionOk ? "ollama-ok" : "ollama-fail")">
|
||||
@(_connectionOk
|
||||
? $"✓ {_connectionLatency} ms {Loc["settings.ollama.online"]}"
|
||||
: $"✗ {Loc["settings.ollama.offline"]}")
|
||||
</div>
|
||||
}
|
||||
<div class="form-hint">@Loc["settings.ollama.hint.url"]</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="llamaCppUrl">URL (llama.cpp)</label>
|
||||
<input id="llamaCppUrl" @bind="llamaCppUrl" placeholder="http://localhost:8080" />
|
||||
<div class="form-hint">Nur aktiv wenn Backend auf "llama.cpp" gesetzt ist</div>
|
||||
<div class="form-hint">Adresse des Ollama-Servers (z. B. http://192.168.1.100:11434)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">@Loc["settings.section.diagnostics"]</h3>
|
||||
<button @onclick="RunAllDiagnostics" class="btn-sm" disabled="@_isDiagnosing">@Loc["settings.diagnostics.run"]</button>
|
||||
@if (_diagnosticsResults != null)
|
||||
{
|
||||
<div class="diagnostics-results">
|
||||
@foreach (var line in _diagnosticsResults)
|
||||
{
|
||||
<div class="diagnostics-line">@line</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">@Loc["settings.section.appearance"]</h3>
|
||||
<h3 class="settings-section-title">Design</h3>
|
||||
<div class="form-group">
|
||||
<label for="themeSelect">@Loc["settings.appearance.label.theme"]</label>
|
||||
<label for="themeSelect">Theme:</label>
|
||||
<select id="themeSelect" @bind="selectedTheme">
|
||||
<option value="light">@Loc["settings.appearance.theme.light"]</option>
|
||||
<option value="dark">@Loc["settings.appearance.theme.dark"]</option>
|
||||
<option value="w95">W95 @Loc["settings.appearance.theme.retro"]</option>
|
||||
<option value="light">Hell</option>
|
||||
<option value="dark">Dunkel</option>
|
||||
<option value="w95">W95 (Retro)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">@Loc["settings.section.language"]</h3>
|
||||
<h3 class="settings-section-title">Neues Projekt</h3>
|
||||
<div class="form-group">
|
||||
<label for="languageSelect">@Loc["settings.language.label"]</label>
|
||||
<select id="languageSelect" @bind="selectedLanguage">
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</div>
|
||||
<label for="projectName">Projektname:</label>
|
||||
<input id="projectName" @bind="newProjectName" placeholder="Projektname" />
|
||||
<div class="form-hint">Der Projektordner wird in "Dokumente\AuthorBuddy\" erstellt.</div>
|
||||
</div>
|
||||
<button @onclick="CreateProject" disabled="@(string.IsNullOrWhiteSpace(newProjectName))">Projekt erstellen</button>
|
||||
</div>
|
||||
|
||||
@if (currentProjectTemplate == null || currentProjectTemplate == ProjectTemplate.Novel)
|
||||
{
|
||||
<div class="settings-column">
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">@Loc["settings.section.editor_prompt"]</h3>
|
||||
<div class="form-group">
|
||||
<textarea id="lektorPrompt" @bind="lektorPrompt" rows="6"></textarea>
|
||||
</div>
|
||||
<div class="temp-slider">
|
||||
<label class="temp-label">@Loc["settings.temperature.label"] <span class="temp-value">@lektorTemp.ToString("F1")</span></label>
|
||||
<input type="range" min="0" max="2" step="0.1" value="@Fmt(lektorTemp)" @oninput="@((ChangeEventArgs e) => SetTemp(e, v => lektorTemp = v))" />
|
||||
<div class="temp-category">@GetTempCategory(lektorTemp)</div>
|
||||
<div class="temp-desc">@GetTempDescription(lektorTemp)</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="primary" @onclick="SaveSettings">Speichern</button>
|
||||
<button @onclick='() => Navigation.NavigateTo("/")'>Abbrechen</button>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">@Loc["settings.section.factcheck_template"]</h3>
|
||||
<div class="form-group">
|
||||
<textarea id="factCheckTemplate" @bind="factCheckTemplate" rows="6"></textarea>
|
||||
</div>
|
||||
<div class="temp-slider">
|
||||
<label class="temp-label">@Loc["settings.temperature.label"] <span class="temp-value">@factCheckTemp.ToString("F1")</span></label>
|
||||
<input type="range" min="0" max="2" step="0.1" value="@Fmt(factCheckTemp)" @oninput="@((ChangeEventArgs e) => SetTemp(e, v => factCheckTemp = v))" />
|
||||
<div class="temp-category">@GetTempCategory(factCheckTemp)</div>
|
||||
<div class="temp-desc">@GetTempDescription(factCheckTemp)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">@Loc["settings.section.spellcheck_prompt"]</h3>
|
||||
<div class="form-group">
|
||||
<textarea id="spellingPrompt" @bind="spellingPrompt" rows="6"></textarea>
|
||||
</div>
|
||||
<div class="temp-slider">
|
||||
<label class="temp-label">@Loc["settings.temperature.label"] <span class="temp-value">@spellingTemp.ToString("F1")</span></label>
|
||||
<input type="range" min="0" max="2" step="0.1" value="@Fmt(spellingTemp)" @oninput="@((ChangeEventArgs e) => SetTemp(e, v => spellingTemp = v))" />
|
||||
<div class="temp-category">@GetTempCategory(spellingTemp)</div>
|
||||
<div class="temp-desc">@GetTempDescription(spellingTemp)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (currentProjectTemplate == null || currentProjectTemplate == ProjectTemplate.Novel)
|
||||
{
|
||||
<div class="settings-column">
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">@Loc["settings.section.advocatus_diaboli"]</h3>
|
||||
<div class="form-group">
|
||||
<textarea id="advocatusDiaboliPrompt" @bind="advocatusDiaboliPrompt" rows="6"></textarea>
|
||||
</div>
|
||||
<div class="temp-slider">
|
||||
<label class="temp-label">@Loc["settings.temperature.label"] <span class="temp-value">@advocatusDiaboliTemp.ToString("F1")</span></label>
|
||||
<input type="range" min="0" max="2" step="0.1" value="@Fmt(advocatusDiaboliTemp)" @oninput="@((ChangeEventArgs e) => SetTemp(e, v => advocatusDiaboliTemp = v))" />
|
||||
<div class="temp-category">@GetTempCategory(advocatusDiaboliTemp)</div>
|
||||
<div class="temp-desc">@GetTempDescription(advocatusDiaboliTemp)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">@Loc["settings.section.hook_finder"]</h3>
|
||||
<div class="form-group">
|
||||
<textarea id="hookFinderPrompt" @bind="hookFinderPrompt" rows="6"></textarea>
|
||||
</div>
|
||||
<div class="temp-slider">
|
||||
<label class="temp-label">@Loc["settings.temperature.label"] <span class="temp-value">@hookFinderTemp.ToString("F1")</span></label>
|
||||
<input type="range" min="0" max="2" step="0.1" value="@Fmt(hookFinderTemp)" @oninput="@((ChangeEventArgs e) => SetTemp(e, v => hookFinderTemp = v))" />
|
||||
<div class="temp-category">@GetTempCategory(hookFinderTemp)</div>
|
||||
<div class="temp-desc">@GetTempDescription(hookFinderTemp)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">@Loc["settings.section.eli5"]</h3>
|
||||
<div class="form-group">
|
||||
<textarea id="eli5Prompt" @bind="eli5Prompt" rows="6"></textarea>
|
||||
</div>
|
||||
<div class="temp-slider">
|
||||
<label class="temp-label">@Loc["settings.temperature.label"] <span class="temp-value">@eli5Temp.ToString("F1")</span></label>
|
||||
<input type="range" min="0" max="2" step="0.1" value="@Fmt(eli5Temp)" @oninput="@((ChangeEventArgs e) => SetTemp(e, v => eli5Temp = v))" />
|
||||
<div class="temp-category">@GetTempCategory(eli5Temp)</div>
|
||||
<div class="temp-desc">@GetTempDescription(eli5Temp)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">@Loc["settings.section.coauthor_brainstorming"]</h3>
|
||||
<div class="form-group">
|
||||
<textarea id="coAuthorPrompt" @bind="coAuthorPrompt" rows="6"></textarea>
|
||||
</div>
|
||||
<div class="temp-slider">
|
||||
<label class="temp-label">@Loc["settings.temperature.label"] <span class="temp-value">@coAuthorTemp.ToString("F1")</span></label>
|
||||
<input type="range" min="0" max="2" step="0.1" value="@Fmt(coAuthorTemp)" @oninput="@((ChangeEventArgs e) => SetTemp(e, v => coAuthorTemp = v))" />
|
||||
<div class="temp-category">@GetTempCategory(coAuthorTemp)</div>
|
||||
<div class="temp-desc">@GetTempDescription(coAuthorTemp)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (currentProjectTemplate == null || currentProjectTemplate == ProjectTemplate.Technical)
|
||||
{
|
||||
<div class="settings-column">
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">@Loc["settings.section.tech_lastenheft"]</h3>
|
||||
<div class="form-group">
|
||||
<textarea id="techLastenheftPrompt" @bind="techLastenheftPrompt" rows="6"></textarea>
|
||||
</div>
|
||||
<div class="temp-slider">
|
||||
<label class="temp-label">@Loc["settings.temperature.label"] <span class="temp-value">@techLastenheftTemp.ToString("F1")</span></label>
|
||||
<input type="range" min="0" max="2" step="0.1" value="@Fmt(techLastenheftTemp)" @oninput="@((ChangeEventArgs e) => SetTemp(e, v => techLastenheftTemp = v))" />
|
||||
<div class="temp-category">@GetTempCategory(techLastenheftTemp)</div>
|
||||
<div class="temp-desc">@GetTempDescription(techLastenheftTemp)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">@Loc["settings.section.tech_pflichtenheft"]</h3>
|
||||
<div class="form-group">
|
||||
<textarea id="techPflichtenheftPrompt" @bind="techPflichtenheftPrompt" rows="6"></textarea>
|
||||
</div>
|
||||
<div class="temp-slider">
|
||||
<label class="temp-label">@Loc["settings.temperature.label"] <span class="temp-value">@techPflichtenheftTemp.ToString("F1")</span></label>
|
||||
<input type="range" min="0" max="2" step="0.1" value="@Fmt(techPflichtenheftTemp)" @oninput="@((ChangeEventArgs e) => SetTemp(e, v => techPflichtenheftTemp = v))" />
|
||||
<div class="temp-category">@GetTempCategory(techPflichtenheftTemp)</div>
|
||||
<div class="temp-desc">@GetTempDescription(techPflichtenheftTemp)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">@Loc["settings.section.tech_usecase"]</h3>
|
||||
<div class="form-group">
|
||||
<textarea id="techUseCasePrompt" @bind="techUseCasePrompt" rows="6"></textarea>
|
||||
</div>
|
||||
<div class="temp-slider">
|
||||
<label class="temp-label">@Loc["settings.temperature.label"] <span class="temp-value">@techUseCaseTemp.ToString("F1")</span></label>
|
||||
<input type="range" min="0" max="2" step="0.1" value="@Fmt(techUseCaseTemp)" @oninput="@((ChangeEventArgs e) => SetTemp(e, v => techUseCaseTemp = v))" />
|
||||
<div class="temp-category">@GetTempCategory(techUseCaseTemp)</div>
|
||||
<div class="temp-desc">@GetTempDescription(techUseCaseTemp)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">@Loc["settings.section.tech_stakeholder"]</h3>
|
||||
<div class="form-group">
|
||||
<textarea id="techStakeholderPrompt" @bind="techStakeholderPrompt" rows="6"></textarea>
|
||||
</div>
|
||||
<div class="temp-slider">
|
||||
<label class="temp-label">@Loc["settings.temperature.label"] <span class="temp-value">@techStakeholderTemp.ToString("F1")</span></label>
|
||||
<input type="range" min="0" max="2" step="0.1" value="@Fmt(techStakeholderTemp)" @oninput="@((ChangeEventArgs e) => SetTemp(e, v => techStakeholderTemp = v))" />
|
||||
<div class="temp-category">@GetTempCategory(techStakeholderTemp)</div>
|
||||
<div class="temp-desc">@GetTempDescription(techStakeholderTemp)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">@Loc["settings.section.tech_glossar"]</h3>
|
||||
<div class="form-group">
|
||||
<textarea id="techGlossarPrompt" @bind="techGlossarPrompt" rows="6"></textarea>
|
||||
</div>
|
||||
<div class="temp-slider">
|
||||
<label class="temp-label">@Loc["settings.temperature.label"] <span class="temp-value">@techGlossarTemp.ToString("F1")</span></label>
|
||||
<input type="range" min="0" max="2" step="0.1" value="@Fmt(techGlossarTemp)" @oninput="@((ChangeEventArgs e) => SetTemp(e, v => techGlossarTemp = v))" />
|
||||
<div class="temp-category">@GetTempCategory(techGlossarTemp)</div>
|
||||
<div class="temp-desc">@GetTempDescription(techGlossarTemp)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">@Loc["settings.section.tech_qualitaet"]</h3>
|
||||
<div class="form-group">
|
||||
<textarea id="techQualitaetPrompt" @bind="techQualitaetPrompt" rows="6"></textarea>
|
||||
</div>
|
||||
<div class="temp-slider">
|
||||
<label class="temp-label">@Loc["settings.temperature.label"] <span class="temp-value">@techQualitaetTemp.ToString("F1")</span></label>
|
||||
<input type="range" min="0" max="2" step="0.1" value="@Fmt(techQualitaetTemp)" @oninput="@((ChangeEventArgs e) => SetTemp(e, v => techQualitaetTemp = v))" />
|
||||
<div class="temp-category">@GetTempCategory(techQualitaetTemp)</div>
|
||||
<div class="temp-desc">@GetTempDescription(techQualitaetTemp)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">@Loc["settings.section.tech_risiko"]</h3>
|
||||
<div class="form-group">
|
||||
<textarea id="techRisikoPrompt" @bind="techRisikoPrompt" rows="6"></textarea>
|
||||
</div>
|
||||
<div class="temp-slider">
|
||||
<label class="temp-label">@Loc["settings.temperature.label"] <span class="temp-value">@techRisikoTemp.ToString("F1")</span></label>
|
||||
<input type="range" min="0" max="2" step="0.1" value="@Fmt(techRisikoTemp)" @oninput="@((ChangeEventArgs e) => SetTemp(e, v => techRisikoTemp = v))" />
|
||||
<div class="temp-category">@GetTempCategory(techRisikoTemp)</div>
|
||||
<div class="temp-desc">@GetTempDescription(techRisikoTemp)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button class="btn btn-primary" @onclick="SaveSettings">@Loc["settings.button.save"]</button>
|
||||
@if (!string.IsNullOrEmpty(statusMessage))
|
||||
{
|
||||
<span class="settings-status">@statusMessage</span>
|
||||
<div class="settings-status">@statusMessage</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
|
@ -386,74 +101,20 @@
|
|||
private string spellingModel = "phi3:mini";
|
||||
private string selectedTheme = "light";
|
||||
private string ollamaUrl = "http://localhost:11434";
|
||||
private string llamaCppUrl = "http://localhost:8080";
|
||||
private string selectedBackend = "Ollama";
|
||||
private string lektorPrompt = string.Empty;
|
||||
private string factCheckTemplate = string.Empty;
|
||||
private string spellingPrompt = string.Empty;
|
||||
private string advocatusDiaboliPrompt = string.Empty;
|
||||
private string hookFinderPrompt = string.Empty;
|
||||
private string eli5Prompt = string.Empty;
|
||||
private string coAuthorPrompt = string.Empty;
|
||||
private double lektorTemp = 0.7;
|
||||
private double factCheckTemp = 0.3;
|
||||
private double spellingTemp = 0.1;
|
||||
private double advocatusDiaboliTemp = 0.9;
|
||||
private double hookFinderTemp = 0.9;
|
||||
private double eli5Temp = 0.5;
|
||||
private double coAuthorTemp = 1.0;
|
||||
private string techLastenheftPrompt = string.Empty;
|
||||
private string techPflichtenheftPrompt = string.Empty;
|
||||
private string techUseCasePrompt = string.Empty;
|
||||
private string techStakeholderPrompt = string.Empty;
|
||||
private string techGlossarPrompt = string.Empty;
|
||||
private string techQualitaetPrompt = string.Empty;
|
||||
private string techRisikoPrompt = string.Empty;
|
||||
private double techLastenheftTemp = 0.3;
|
||||
private double techPflichtenheftTemp = 0.3;
|
||||
private double techUseCaseTemp = 0.4;
|
||||
private double techStakeholderTemp = 0.5;
|
||||
private double techGlossarTemp = 0.2;
|
||||
private double techQualitaetTemp = 0.3;
|
||||
private double techRisikoTemp = 0.5;
|
||||
private string selectedLanguage = "de";
|
||||
private string newProjectName = string.Empty;
|
||||
private string newUserName = string.Empty;
|
||||
private string statusMessage = string.Empty;
|
||||
private List<LLM_Model> availableModels = new();
|
||||
private List<UserEntity> users = new();
|
||||
private int selectedUserId;
|
||||
private string? currentProjectName;
|
||||
private ProjectTemplate? currentProjectTemplate;
|
||||
private bool _isAdmin;
|
||||
private bool _connectionOk;
|
||||
private long _connectionLatency;
|
||||
private bool _connectionTested;
|
||||
private bool _isTestingConnection;
|
||||
private List<string>? _diagnosticsResults;
|
||||
private bool _isDiagnosing;
|
||||
private List<Ollama.Model> availableModels = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var currentUser = AuthService.CurrentUser;
|
||||
_isAdmin = currentUser?.Role == UserRole.Administrator;
|
||||
await using var context = await ContextFactory.CreateDbContextAsync();
|
||||
|
||||
users = await SettingsService.GetUsersAsync();
|
||||
var current = await SettingsService.GetCurrentUserAsync();
|
||||
selectedUserId = current.Id;
|
||||
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";
|
||||
|
||||
var pid = SettingsService.CurrentProjectId;
|
||||
if (pid != null)
|
||||
{
|
||||
var project = await ProjectService.GetProjectAsync(pid.Value);
|
||||
currentProjectName = project?.Name;
|
||||
currentProjectTemplate = project?.Template;
|
||||
}
|
||||
|
||||
await LoadUserSettings();
|
||||
|
||||
if (_isAdmin)
|
||||
{
|
||||
try
|
||||
{
|
||||
var models = await OllamaService.GetLocalModelsAsync();
|
||||
|
|
@ -464,214 +125,76 @@
|
|||
availableModels = new();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string?> GetSettingAsync(AppDbContext context, string key)
|
||||
{
|
||||
var setting = await context.Settings.FindAsync(key);
|
||||
return setting?.Value;
|
||||
}
|
||||
|
||||
private async Task LoadUserSettings()
|
||||
private static async Task SaveSettingAsync(AppDbContext context, string key, string value)
|
||||
{
|
||||
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";
|
||||
llamaCppUrl = await SettingsService.GetLlamaCppUrlAsync() ?? "http://localhost:8080";
|
||||
selectedBackend = await SettingsService.GetBackendAsync() ?? "Ollama";
|
||||
lektorPrompt = await SettingsService.GetLektorSystemPromptAsync() ?? string.Empty;
|
||||
factCheckTemplate = await SettingsService.GetFactCheckTemplateAsync() ?? string.Empty;
|
||||
spellingPrompt = await SettingsService.GetSpellingPromptAsync() ?? string.Empty;
|
||||
advocatusDiaboliPrompt = await SettingsService.GetAdvocatusDiaboliPromptAsync() ?? string.Empty;
|
||||
hookFinderPrompt = await SettingsService.GetHookFinderPromptAsync() ?? string.Empty;
|
||||
eli5Prompt = await SettingsService.GetEli5PromptAsync() ?? string.Empty;
|
||||
coAuthorPrompt = await SettingsService.GetCoAuthorBrainstormingPromptAsync() ?? string.Empty;
|
||||
coAuthorTemp = await SettingsService.GetCoAuthorTempAsync();
|
||||
techLastenheftPrompt = await SettingsService.GetTechLastenheftPromptAsync() ?? string.Empty;
|
||||
techPflichtenheftPrompt = await SettingsService.GetTechPflichtenheftPromptAsync() ?? string.Empty;
|
||||
techUseCasePrompt = await SettingsService.GetTechUseCasePromptAsync() ?? string.Empty;
|
||||
techStakeholderPrompt = await SettingsService.GetTechStakeholderPromptAsync() ?? string.Empty;
|
||||
techGlossarPrompt = await SettingsService.GetTechGlossarPromptAsync() ?? string.Empty;
|
||||
techQualitaetPrompt = await SettingsService.GetTechQualitaetPromptAsync() ?? string.Empty;
|
||||
techRisikoPrompt = await SettingsService.GetTechRisikoPromptAsync() ?? string.Empty;
|
||||
techLastenheftTemp = await SettingsService.GetTechLastenheftTempAsync();
|
||||
techPflichtenheftTemp = await SettingsService.GetTechPflichtenheftTempAsync();
|
||||
techUseCaseTemp = await SettingsService.GetTechUseCaseTempAsync();
|
||||
techStakeholderTemp = await SettingsService.GetTechStakeholderTempAsync();
|
||||
techGlossarTemp = await SettingsService.GetTechGlossarTempAsync();
|
||||
techQualitaetTemp = await SettingsService.GetTechQualitaetTempAsync();
|
||||
techRisikoTemp = await SettingsService.GetTechRisikoTempAsync();
|
||||
selectedLanguage = await SettingsService.GetLanguageAsync() ?? "de";
|
||||
var existing = await context.Settings.FindAsync(key);
|
||||
if (existing != null)
|
||||
{
|
||||
existing.Value = value;
|
||||
}
|
||||
|
||||
private async Task SwitchUser(ChangeEventArgs e)
|
||||
else
|
||||
{
|
||||
var userId = int.Parse(e.Value?.ToString() ?? "0");
|
||||
selectedUserId = userId;
|
||||
SettingsService.SetCurrentUser(userId);
|
||||
await SettingsService.InitializeAsync();
|
||||
await LoadUserSettings();
|
||||
context.Settings.Add(new SettingEntity { Key = key, Value = value });
|
||||
}
|
||||
|
||||
private async Task AddUser()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(newUserName)) return;
|
||||
|
||||
var user = await SettingsService.CreateUserAsync(newUserName);
|
||||
users.Add(user);
|
||||
newUserName = string.Empty;
|
||||
statusMessage = string.Format(Loc["settings.status.user_created"], user.Name);
|
||||
}
|
||||
|
||||
private async Task SaveSettings()
|
||||
{
|
||||
if (_isAdmin)
|
||||
{
|
||||
await SettingsService.SaveOllamaUrlAsync(ollamaUrl);
|
||||
await SettingsService.SaveLlamaCppUrlAsync(llamaCppUrl);
|
||||
await SettingsService.SaveBackendAsync(selectedBackend);
|
||||
OllamaService.Reconfigure(ollamaUrl);
|
||||
await using var context = await ContextFactory.CreateDbContextAsync();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ragModel))
|
||||
{
|
||||
var ok = await OllamaService.TestEmbeddingAsync(ragModel);
|
||||
if (!ok)
|
||||
{
|
||||
statusMessage = selectedLanguage == "en"
|
||||
? $"Model '{ragModel}' does not support embeddings. RAG model not saved."
|
||||
: $"Modell '{ragModel}' unterstützt keine Embeddings. RAG-Modell nicht gespeichert.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
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);
|
||||
await SettingsService.SaveAdvocatusDiaboliPromptAsync(advocatusDiaboliPrompt);
|
||||
await SettingsService.SaveHookFinderPromptAsync(hookFinderPrompt);
|
||||
await SettingsService.SaveEli5PromptAsync(eli5Prompt);
|
||||
await SettingsService.SaveCoAuthorBrainstormingPromptAsync(coAuthorPrompt);
|
||||
await SettingsService.SaveTechLastenheftPromptAsync(techLastenheftPrompt);
|
||||
await SettingsService.SaveTechPflichtenheftPromptAsync(techPflichtenheftPrompt);
|
||||
await SettingsService.SaveTechUseCasePromptAsync(techUseCasePrompt);
|
||||
await SettingsService.SaveTechStakeholderPromptAsync(techStakeholderPrompt);
|
||||
await SettingsService.SaveTechGlossarPromptAsync(techGlossarPrompt);
|
||||
await SettingsService.SaveTechQualitaetPromptAsync(techQualitaetPrompt);
|
||||
await SettingsService.SaveTechRisikoPromptAsync(techRisikoPrompt);
|
||||
await SettingsService.SaveLektorTempAsync(lektorTemp);
|
||||
await SettingsService.SaveFactCheckTempAsync(factCheckTemp);
|
||||
await SettingsService.SaveSpellingTempAsync(spellingTemp);
|
||||
await SettingsService.SaveAdvocatusDiaboliTempAsync(advocatusDiaboliTemp);
|
||||
await SettingsService.SaveHookFinderTempAsync(hookFinderTemp);
|
||||
await SettingsService.SaveEli5TempAsync(eli5Temp);
|
||||
await SettingsService.SaveCoAuthorTempAsync(coAuthorTemp);
|
||||
await SettingsService.SaveTechLastenheftTempAsync(techLastenheftTemp);
|
||||
await SettingsService.SaveTechPflichtenheftTempAsync(techPflichtenheftTemp);
|
||||
await SettingsService.SaveTechUseCaseTempAsync(techUseCaseTemp);
|
||||
await SettingsService.SaveTechStakeholderTempAsync(techStakeholderTemp);
|
||||
await SettingsService.SaveTechGlossarTempAsync(techGlossarTemp);
|
||||
await SettingsService.SaveTechQualitaetTempAsync(techQualitaetTemp);
|
||||
await SettingsService.SaveTechRisikoTempAsync(techRisikoTemp);
|
||||
await SettingsService.SaveLanguageAsync(selectedLanguage);
|
||||
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);
|
||||
|
||||
if (selectedLanguage != Loc.CurrentLanguage)
|
||||
await Loc.SetLanguageAsync(selectedLanguage);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
statusMessage = Loc["settings.status.saved"];
|
||||
await ThemeService.ApplyTheme(selectedTheme);
|
||||
OllamaConfig.OllamaUrl = ollamaUrl;
|
||||
OllamaConfig.ChatModel = ragModel;
|
||||
OllamaConfig.SpellingModel = spellingModel;
|
||||
await OllamaService.ReconfigureAsync(ollamaUrl);
|
||||
|
||||
statusMessage = "Einstellungen gespeichert.";
|
||||
await ApplyTheme(selectedTheme);
|
||||
}
|
||||
|
||||
private string GetTempCategory(double temp)
|
||||
private async Task CreateProject()
|
||||
{
|
||||
if (temp <= 0.2) return Loc["settings.temperature.category.precise"];
|
||||
if (temp <= 0.5) return Loc["settings.temperature.category.balanced"];
|
||||
if (temp <= 1.2) return Loc["settings.temperature.category.creative"];
|
||||
return Loc["settings.temperature.category.chaos"];
|
||||
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 string GetTempDescription(double temp)
|
||||
private async Task ApplyTheme(string theme)
|
||||
{
|
||||
if (temp <= 0.2) return Loc["settings.temperature.desc.precise"];
|
||||
if (temp <= 0.5) return Loc["settings.temperature.desc.balanced"];
|
||||
if (temp <= 1.2) return Loc["settings.temperature.desc.creative"];
|
||||
return Loc["settings.temperature.desc.chaos"];
|
||||
}
|
||||
|
||||
private string Fmt(double v) => v.ToString("F1", System.Globalization.CultureInfo.InvariantCulture);
|
||||
|
||||
private void SetTemp(ChangeEventArgs e, Action<double> setter)
|
||||
{
|
||||
if (e.Value is string s && double.TryParse(s, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var v))
|
||||
setter(Math.Round(v, 1));
|
||||
}
|
||||
|
||||
private async Task TestConnection()
|
||||
{
|
||||
_isTestingConnection = true;
|
||||
_connectionTested = false;
|
||||
StateHasChanged();
|
||||
|
||||
OllamaService.Reconfigure(ollamaUrl);
|
||||
var result = await OllamaService.TestConnectionAsync();
|
||||
_connectionOk = result.Success;
|
||||
_connectionLatency = result.LatencyMs;
|
||||
_connectionTested = true;
|
||||
_isTestingConnection = false;
|
||||
}
|
||||
|
||||
private async Task RunAllDiagnostics()
|
||||
{
|
||||
_isDiagnosing = true;
|
||||
_diagnosticsResults = new();
|
||||
StateHasChanged();
|
||||
|
||||
OllamaService.Reconfigure(ollamaUrl);
|
||||
var lang = selectedLanguage;
|
||||
|
||||
var results = new List<string>();
|
||||
|
||||
var conn = await OllamaService.TestConnectionAsync();
|
||||
if (conn.Success)
|
||||
results.Add(lang == "en" ? $"✓ Connection: {conn.LatencyMs} ms" : $"✓ Verbindung: {conn.LatencyMs} ms");
|
||||
else
|
||||
{
|
||||
results.Add(lang == "en" ? "✗ Connection failed" : "✗ Verbindung fehlgeschlagen");
|
||||
_diagnosticsResults = results;
|
||||
_isDiagnosing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var models = new[] {
|
||||
(Loc["settings.models.label.style"] ?? "Style", styleModel),
|
||||
(Loc["settings.models.label.rag"] ?? "RAG", ragModel),
|
||||
(Loc["settings.models.label.spellcheck"] ?? "Spelling", spellingModel)
|
||||
};
|
||||
|
||||
foreach (var (label, model) in models)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(model))
|
||||
{
|
||||
results.Add($"? {label}: {(!string.IsNullOrEmpty(lang) && lang == "en" ? "not configured" : "nicht konfiguriert")}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var exists = await OllamaService.ModelExistsAsync(model);
|
||||
if (!exists)
|
||||
{
|
||||
results.Add($"✗ {label} ({model}): {(!string.IsNullOrEmpty(lang) && lang == "en" ? "not found" : "nicht gefunden")}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var latency = await OllamaService.MeasureLatencyAsync(model);
|
||||
var latencyStr = latency >= 0 ? $"{latency} ms" : "—";
|
||||
|
||||
var ctx = await OllamaService.GetModelContextWindowAsync(model);
|
||||
var ctxStr = ctx.Success ? $"{ctx.ContextWindow}" : "?";
|
||||
|
||||
results.Add($"✓ {label} ({model}): {latencyStr}, {(!string.IsNullOrEmpty(lang) && lang == "en" ? "Context" : "Kontext")}: {ctxStr}");
|
||||
}
|
||||
|
||||
_diagnosticsResults = results;
|
||||
_isDiagnosing = false;
|
||||
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);");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,196 +0,0 @@
|
|||
@page "/setup-2fa"
|
||||
@inject IAuthService AuthService
|
||||
@inject NavigationManager Navigation
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject ILocalizationService Loc
|
||||
|
||||
@rendermode InteractiveServer
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
|
||||
<PageTitle>@Loc["page.title.setup2fa"]</PageTitle>
|
||||
|
||||
<div class="auth-page">
|
||||
<div class="auth-card">
|
||||
<div class="auth-logo">
|
||||
<img src="/Assets/logo.jpg" alt="Author Buddy" />
|
||||
</div>
|
||||
<h2>@Loc["setup2fa.heading"]</h2>
|
||||
|
||||
@if (isEnabled)
|
||||
{
|
||||
<div class="auth-success">
|
||||
<p>@Loc["setup2fa.status.enabled"]</p>
|
||||
<button @onclick="Disable2FA" disabled="@isBusy">@Loc["setup2fa.button.disable"]</button>
|
||||
</div>
|
||||
}
|
||||
else if (showSecret)
|
||||
{
|
||||
<p>@Loc["setup2fa.instruction"]</p>
|
||||
|
||||
<div class="qr-container">
|
||||
<img src="@qrCodeDataUri" alt="QR Code" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>@Loc["setup2fa.label.secret_key"]</label>
|
||||
<div class="secret-display">
|
||||
<code>@secret</code>
|
||||
<button @onclick="CopySecret">@Loc["common.copy"]</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="code">@Loc["setup2fa.label.verification_code"]</label>
|
||||
<input id="code" @bind="verificationCode" maxlength="6" @onkeydown="HandleKeyDown" />
|
||||
</div>
|
||||
|
||||
<div class="auth-actions">
|
||||
<button @onclick="Enable2FA" disabled="@isBusy || string.IsNullOrWhiteSpace(verificationCode)">@Loc["setup2fa.button.enable"]</button>
|
||||
<button @onclick="Cancel">@Loc["common.cancel"]</button>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>@Loc["setup2fa.description"]</p>
|
||||
<div class="auth-actions">
|
||||
<button @onclick="StartSetup">@Loc["setup2fa.button.setup"]</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
<div class="auth-error">@errorMessage</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string secret = string.Empty;
|
||||
private string verificationCode = string.Empty;
|
||||
private string errorMessage = string.Empty;
|
||||
private string? qrCodeDataUri;
|
||||
private bool showSecret;
|
||||
private bool isEnabled;
|
||||
private bool isBusy;
|
||||
private string? username;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
isEnabled = await AuthService.Is2FAEnabledAsync();
|
||||
var user = await AuthService.GetCurrentUserAsync();
|
||||
username = user?.Name;
|
||||
}
|
||||
|
||||
private async Task HandleKeyDown(KeyboardEventArgs e)
|
||||
{
|
||||
if (e.Key == "Enter")
|
||||
await Enable2FA();
|
||||
}
|
||||
|
||||
private async Task StartSetup()
|
||||
{
|
||||
isBusy = true;
|
||||
try
|
||||
{
|
||||
secret = await AuthService.Generate2FASecretAsync();
|
||||
var uri = AuthService.Get2FAProvisioningUri(secret, username ?? "user");
|
||||
qrCodeDataUri = await GenerateQRCodeDataUri(uri);
|
||||
showSecret = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"{Loc["common.error"]} {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Enable2FA()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(verificationCode) || verificationCode.Length != 6)
|
||||
{
|
||||
errorMessage = Loc["setup2fa.error.invalid_code"];
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TotpHelper.VerifyCode(secret, verificationCode))
|
||||
{
|
||||
errorMessage = Loc["setup2fa.error.code_invalid"];
|
||||
return;
|
||||
}
|
||||
|
||||
isBusy = true;
|
||||
errorMessage = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
var result = await AuthService.Enable2FAAsync(secret);
|
||||
if (result)
|
||||
{
|
||||
isEnabled = true;
|
||||
showSecret = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = Loc["setup2fa.error.enable_failed"];
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"{Loc["common.error"]} {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Disable2FA()
|
||||
{
|
||||
isBusy = true;
|
||||
try
|
||||
{
|
||||
var result = await AuthService.Disable2FAAsync();
|
||||
if (result)
|
||||
{
|
||||
isEnabled = false;
|
||||
showSecret = false;
|
||||
secret = string.Empty;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"{Loc["common.error"]} {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
isBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string?> GenerateQRCodeDataUri(string uri)
|
||||
{
|
||||
try
|
||||
{
|
||||
var encoded = System.Web.HttpUtility.UrlEncode(uri);
|
||||
return $"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data={encoded}";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CopySecret()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(secret))
|
||||
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", secret);
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
Navigation.NavigateTo("/");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
@page "/user-management"
|
||||
@attribute [Authorize]
|
||||
@inject IAuthService AuthService
|
||||
@inject ILocalizationService Loc
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<PageTitle>@Loc["usermanagement.title"] - Author Buddy</PageTitle>
|
||||
|
||||
<div class="usermanagement-page">
|
||||
<h3>@Loc["usermanagement.title"]</h3>
|
||||
|
||||
<table class="usermanagement-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>@Loc["usermanagement.col.name"]</th>
|
||||
<th>@Loc["usermanagement.col.role"]</th>
|
||||
<th>@Loc["usermanagement.col.status"]</th>
|
||||
<th>@Loc["usermanagement.col.actions"]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var u in users)
|
||||
{
|
||||
<tr class="@(u.Id == currentUserId ? "usermanagement-current" : "") @(!u.IsActive ? "usermanagement-inactive" : "")">
|
||||
<td>@u.Name @(u.Id == currentUserId ? "(Du)" : "")</td>
|
||||
<td>@(u.Role == UserRole.Administrator ? Loc["settings.user.role.admin"] : Loc["settings.user.role.author"])</td>
|
||||
<td>
|
||||
@if (u.IsActive)
|
||||
{
|
||||
<span class="usermanagement-status-active">@Loc["usermanagement.status.active"]</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="usermanagement-status-inactive">@Loc["usermanagement.status.inactive"]</span>
|
||||
}
|
||||
</td>
|
||||
<td class="usermanagement-actions">
|
||||
@if (u.Id != currentUserId)
|
||||
{
|
||||
@if (u.IsActive)
|
||||
{
|
||||
<button class="btn btn-sm" @onclick="() => DeactivateUser(u.Id)" title="@Loc["usermanagement.action.deactivate"]">@Loc["usermanagement.action.deactivate"]</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn btn-sm btn-primary" @onclick="() => ActivateUser(u.Id)" title="@Loc["usermanagement.action.activate"]">@Loc["usermanagement.action.activate"]</button>
|
||||
}
|
||||
<button class="btn btn-sm btn-danger" @onclick="() => DeleteUser(u.Id)" title="@Loc["usermanagement.action.delete"]">@Loc["usermanagement.action.delete"]</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span style="font-size:11px;color:var(--border-color);">@Loc["usermanagement.cannot_self"]</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@if (!string.IsNullOrEmpty(statusMessage))
|
||||
{
|
||||
<div class="usermanagement-status">@statusMessage</div>
|
||||
}
|
||||
|
||||
<button class="btn btn-secondary" @onclick='() => Navigation.NavigateTo("/settings")' style="margin-top:12px;">@Loc["common.back"]</button>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<UserEntity> users = new();
|
||||
private int currentUserId;
|
||||
private string statusMessage = string.Empty;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadUsers();
|
||||
}
|
||||
|
||||
private async Task LoadUsers()
|
||||
{
|
||||
users = await AuthService.GetAllUsersAsync();
|
||||
currentUserId = AuthService.CurrentUser?.Id ?? 0;
|
||||
}
|
||||
|
||||
private async Task ActivateUser(int userId)
|
||||
{
|
||||
var ok = await AuthService.SetUserActiveAsync(userId, true);
|
||||
statusMessage = ok ? Loc["usermanagement.status.activated"] : Loc["common.error"];
|
||||
await LoadUsers();
|
||||
}
|
||||
|
||||
private async Task DeactivateUser(int userId)
|
||||
{
|
||||
var ok = await AuthService.SetUserActiveAsync(userId, false);
|
||||
statusMessage = ok ? Loc["usermanagement.status.deactivated"] : Loc["common.error"];
|
||||
await LoadUsers();
|
||||
}
|
||||
|
||||
private async Task DeleteUser(int userId)
|
||||
{
|
||||
var ok = await AuthService.DeleteUserAsync(userId);
|
||||
statusMessage = ok ? Loc["usermanagement.status.deleted"] : Loc["common.error"];
|
||||
await LoadUsers();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
@inject NavigationManager Navigation
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
|
||||
@rendermode InteractiveServer
|
||||
|
||||
@code {
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var state = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
if (state.User.Identity?.IsAuthenticated == true)
|
||||
return;
|
||||
Navigation.NavigateTo("/login");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,6 @@
|
|||
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
|
||||
<NotAuthorized>
|
||||
<RedirectToLogin />
|
||||
</NotAuthorized>
|
||||
</AuthorizeRouteView>
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@
|
|||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.JSInterop
|
||||
@using AuthorBuddy.Web
|
||||
@using AuthorBuddy.Web.Components
|
||||
|
|
@ -14,3 +12,4 @@
|
|||
@using AuthorBuddy.Web.Models
|
||||
@using AuthorBuddy.Web.Services
|
||||
@using AuthorBuddy.Web.Data
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
|
|
|
|||
472
DOCUMENTATION.md
472
DOCUMENTATION.md
|
|
@ -1,472 +0,0 @@
|
|||
# AuthorBuddy.Web – Dokumentation
|
||||
|
||||
## Inhaltsverzeichnis
|
||||
|
||||
1. [Überblick](#1-überblick)
|
||||
2. [Funktionsübersicht](#2-funktionsübersicht)
|
||||
3. [Systemarchitektur](#3-systemarchitektur)
|
||||
4. [Installation & Einrichtung](#4-installation--einrichtung)
|
||||
5. [Bedienung](#5-bedienung)
|
||||
- [Projektverwaltung](#51-projektverwaltung)
|
||||
- [Editor](#52-editor)
|
||||
- [Assistent](#53-assistent)
|
||||
- [Einstellungen](#54-einstellungen)
|
||||
- [Modellverwaltung](#55-modellverwaltung)
|
||||
- [Benutzerverwaltung](#56-benutzerverwaltung)
|
||||
- [Datenbank-Administration](#57-datenbank-administration)
|
||||
6. [Datenbank](#6-datenbank)
|
||||
7. [Themes & Lokalisierung](#7-themes--lokalisierung)
|
||||
8. [LLM-Backends](#8-llm-backends)
|
||||
9. [Technische Referenz](#9-technische-referenz)
|
||||
|
||||
---
|
||||
|
||||
## 1. Überblick
|
||||
|
||||
**AuthorBuddy.Web** ist eine webbasierte Schreibumgebung für Autoren mit KI-Unterstützung. Die Anwendung läuft als Blazor Interactive Server (.NET 10.0) und nutzt lokale Large Language Models (LLMs) über **Ollama** oder **llama.cpp** für Textanalyse, Lektorat, Faktenabfrage und kreative Zusammenarbeit.
|
||||
|
||||
Daten werden in einer eingebetteten **LiteDB**-Datenbank (NoSQL) gespeichert – es wird kein externes Datenbanksystem benötigt.
|
||||
|
||||
### Zielgruppen
|
||||
|
||||
- **Autoren & Schriftsteller** – Romanvorlage mit Kapitelverwaltung, Exposé, Charakterentwicklung
|
||||
- **Requirements Engineers** – Technische Vorlage für Lastenheft, Pflichtenheft, Use Cases uvm.
|
||||
- **Administratoren** – Benutzerverwaltung mit 2FA, Datenbank-Administration
|
||||
|
||||
---
|
||||
|
||||
## 2. Funktionsübersicht
|
||||
|
||||
| Funktion | Beschreibung |
|
||||
|----------|-------------|
|
||||
| **Projektverwaltung** | Zwei Vorlagen: Roman (mit Drafts, Fakten, Charakteren, Archiv) und Technisch (mit Anforderungen, Use Cases, Architektur, Glossar) |
|
||||
| **Markdown-Editor** | Vollbild-Editor mit Autosave, Versionierung, Diff-Ansicht |
|
||||
| **KI-Assistent** | Mehrere Modi: Lektorat, Advocatus Diaboli, Hook-Finder, ELI5, Co-Author, RAG-Faktenabfrage |
|
||||
| **Technische Assistenten** | Lastenheft, Pflichtenheft, Use Cases, Stakeholder-Analyse, Glossar, Qualitätsanforderungen, Risikoanalyse |
|
||||
| **Rechtschreibprüfung** | KI-gestützte Korrektur über LLM |
|
||||
| **Stilanalyse** | KI-gestütztes Lektorat |
|
||||
| **RAG (Retrieval-Augmented Generation)** | Volltext-Suche über Projektdateien mit Embedding-Vektoren |
|
||||
| **Wikipedia-Recherche** | Integrierte Wikipedia-Suche (DE/EN) mit „Als Fakt speichern" |
|
||||
| **Datei-Kategorien** | Markdown-Dateien können als Charakter, Exposé, Fakt oder Dokument getaggt werden |
|
||||
| **Benutzerverwaltung** | Multi-User mit Passwort-Hashing und TOTP-2FA |
|
||||
| **Modellverwaltung** | LLM-Modelle via Ollama herunterladen und verwalten |
|
||||
| **Zwei Backends** | Ollama (vollständig) und llama.cpp (OpenAI-kompatibel) |
|
||||
| **Mehrere Themes** | Hell, Dunkel, Windows 95 Retro |
|
||||
| **Zweisprachig** | Deutsch (Standard) und Englisch |
|
||||
| **DB-Admin** | Datenbank-Tabellen einsehen, bearbeiten und löschen (nur Admin) |
|
||||
|
||||
---
|
||||
|
||||
## 3. Systemarchitektur
|
||||
|
||||
### 3.1 Technologie-Stack
|
||||
|
||||
| Komponente | Technologie |
|
||||
|------------|-------------|
|
||||
| **Framework** | .NET 10.0 – Blazor Interactive Server |
|
||||
| **Sprache** | C# 13 |
|
||||
| **UI** | Razor Components, CSS Custom Properties |
|
||||
| **Datenbank** | LiteDB 5.0 (eingebettetes NoSQL-Dokumenten-DB) |
|
||||
| **Text-Diff** | DiffPlex 1.9 |
|
||||
| **Authentifizierung** | Cookie-basiert mit ClaimsPrincipal |
|
||||
| **2FA** | TOTP (Time-based One-Time Password) |
|
||||
|
||||
### 3.2 Schichtenarchitektur
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Pages (.razor) │
|
||||
│ Editor, Assistant, Settings, Models, ... │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ Shared Components │
|
||||
│ CategorySelector, CopyCreateToolbar, │
|
||||
│ DiffView, RedirectToLogin │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ Services │
|
||||
│ AuthService │ OllamaService │
|
||||
│ SettingsService │ ProjectService │
|
||||
│ RagService │ WikipediaService │
|
||||
│ ThemeService │ LocalizationService │
|
||||
│ DbAdminService │ FileScannerService │
|
||||
│ OllamaSetupService │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ Backends (ILLMBackend) │
|
||||
│ OllamaBackend │ LlamaCppBackend │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ Daten (LiteDB) │
|
||||
│ UserEntity, ProjectEntity, SettingEntity, │
|
||||
│ FileCategoryEntity, DocumentVector, │
|
||||
│ DocumentMetaEntity │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.3 Abhängigkeits-Injektion (DI)
|
||||
|
||||
Die Anwendung verwendet das integrierte DI-System von ASP.NET Core:
|
||||
|
||||
- **Singleton** (`AddSingleton`): `ILiteDatabase`, `OllamaSettings`, `IOllamaService`, `ILLMBackend` (beide), `IProjectService`, `IRagService`, `IWikipediaService`, `IOllamaSetupService`, `IFileScannerService`, `OperationState`, `IDbAdminService`
|
||||
- **Scoped** (`AddScoped`): `ISettingsService`, `IThemeService`, `IAuthService`, `ILocalizationService`, `CustomAuthStateProvider`
|
||||
|
||||
---
|
||||
|
||||
## 4. Installation & Einrichtung
|
||||
|
||||
### 4.1 Voraussetzungen
|
||||
|
||||
- **.NET 10.0 SDK** oder höher
|
||||
- **Ollama** (empfohlen) oder **llama.cpp** für LLM-Unterstützung
|
||||
- Ein moderner Webbrowser (Chrome, Edge, Firefox)
|
||||
|
||||
### 4.2 Schnellstart
|
||||
|
||||
```bash
|
||||
# Repository klonen
|
||||
git clone <repository-url>
|
||||
cd AuthorBuddy.Web
|
||||
|
||||
# Bauen
|
||||
dotnet build
|
||||
|
||||
# Starten
|
||||
dotnet run
|
||||
```
|
||||
|
||||
Die Anwendung ist standardmäßig unter `http://localhost:5000` erreichbar.
|
||||
|
||||
### 4.3 LLM-Modelle einrichten
|
||||
|
||||
1. Ollama installieren (https://ollama.com) oder llama.cpp konfigurieren
|
||||
2. Modelle pullen, z.B.: `ollama pull llama3.2`, `ollama pull nomic-embed-text`
|
||||
3. In der Anwendung unter **Einstellungen → Modelle** die Modelle auswählen
|
||||
4. Verbindung testen mit dem **Test**-Button
|
||||
|
||||
### 4.4 Erstmaliger Start
|
||||
|
||||
Beim ersten Start wird automatisch ein Admin-Benutzer erstellt. Über den *Setup-Assistenten* (`/setup-assistant`) können die empfohlenen Einstellungen übernommen werden.
|
||||
|
||||
---
|
||||
|
||||
## 5. Bedienung
|
||||
|
||||
### 5.1 Projektverwaltung
|
||||
|
||||
**Neues Projekt anlegen:**
|
||||
|
||||
1. Klicke auf **Datei → Neues Projekt** in der Seitenleiste
|
||||
2. Wähle eine Vorlage:
|
||||
- **📖 Roman** – Erzeugt Ordner für Drafts, Fakten, Charaktere, Archiv sowie Exposé- und Ideen-Vorlagen
|
||||
- **⚙️ Technische Vorlage** – Erzeugt Ordner für Anforderungen, Use Cases, Architektur, Glossar sowie Lastenheft- und Pflichtenheft-Vorlagen
|
||||
3. Vergib einen Projektnamen und wähle einen Speicherort
|
||||
4. Klicke auf **Erstellen**
|
||||
|
||||
**Projekt öffnen:**
|
||||
|
||||
In der Seitenleiste unter **Projekte** werden alle vorhandenen Projekte aufgelistet. Ein Klick öffnet das Projekt im Editor.
|
||||
|
||||
**Projekt kopieren:**
|
||||
|
||||
Rechtsklick (oder Kontextmenü) auf ein Projekt → **Projekt kopieren** – erstellt eine vollständige Kopie inkl. Dateistruktur, Kategorien und projektspezifischer Einstellungen.
|
||||
|
||||
### 5.2 Editor
|
||||
|
||||
Der Editor (`/editor`) ist der zentrale Arbeitsbereich.
|
||||
|
||||
**Funktionen:**
|
||||
|
||||
| Funktion | Beschreibung |
|
||||
|----------|-------------|
|
||||
| **Text eingeben** | Markdown-Editor im Vollbildmodus |
|
||||
| **Autosave** | Speichert automatisch alle 2 Sekunden |
|
||||
| **Manuell speichern** | `Strg+S` oder Klick auf **Speichern** |
|
||||
| **Versionierung** | Klick auf **Version** erstellt einen Snapshot im Unterordner `_versions/` |
|
||||
| **Versionen durchsuchen** | **Versionen** → Liste aller Snapshots mit Kommentar |
|
||||
| **Diff-Ansicht** | **Vergleichen** zeigt Side-by-Side-Diff zwischen Versionen |
|
||||
| **Version wiederherstellen** | **Wiederherstellen** lädt eine alte Version in den Editor |
|
||||
| **Stil prüfen** | KI-Analyse des Schreibstils über das Stil-Modell |
|
||||
| **Rechtschreibung prüfen** | KI-Korrektur über das Spelling-Modell |
|
||||
| **RAG-Suche** | Durchsucht indexierte Projektinhalte nach ähnlichen Passagen |
|
||||
| **Wikipedia-Suche** | Durchsucht Wikipedia (DE+EN) und kann Ergebnisse als Fakten speichern |
|
||||
| **Assistent** | Öffnet den KI-Assistenten mit dem aktuellen Text als Eingabe |
|
||||
| **Projekt indexieren** | Baut den Vektorindex für RAG auf (nach Projektänderungen nötig) |
|
||||
|
||||
**Datei-Kategorien:**
|
||||
|
||||
Im Dateibaum können `.md`-Dateien mit Kategorien versehen werden:
|
||||
|
||||
- 👤 **Charakter** – Personenprofil
|
||||
- 📋 **Exposé** – Handlungsübersicht (nur eine Datei pro Projekt)
|
||||
- 📌 **Fakt** – Hintergrundinformation
|
||||
- 📄 **Dokument** – Allgemeines Dokument
|
||||
|
||||
Kategorien werden im Dateibaum als Badge angezeigt und können über das 🏷-Symbol bearbeitet werden.
|
||||
|
||||
### 5.3 Assistent
|
||||
|
||||
Der KI-Assistent (`/assistant`) bietet mehrere Arbeitsmodi:
|
||||
|
||||
**Roman-Modi:** (immer verfügbar)
|
||||
|
||||
| Modus | Beschreibung | Temp. |
|
||||
|-------|-------------|-------|
|
||||
| **Lektorat** | Stilistische Textoptimierung – lebendiger, anschaulicher Stil | 0.7 |
|
||||
| **Advocatus Diaboli** | Radikale Gegenposition – Schwachstellen in der Argumentation | 0.9 |
|
||||
| **Hook-Finder** | 5 emotionale Einstiegssätze für den Text | 0.9 |
|
||||
| **ELI5** | Vereinfachung komplexer Passagen | 0.5 |
|
||||
| **Co-Author** | Kreative Weiterentwicklung von Handlung/Charakteren | 1.0 |
|
||||
| **RAG** | Faktenabfrage aus dem projekteigenen Index | 0.3 |
|
||||
|
||||
**Technische Modi:** (nur bei technischer Projektvorlage sichtbar)
|
||||
|
||||
| Modus | Beschreibung | Temp. |
|
||||
|-------|-------------|-------|
|
||||
| **📋 Lastenheft** | Erstellung eines Lastenhefts nach IREB-Standard | 0.3 |
|
||||
| **⚙️ Pflichtenheft** | Detailliertes Pflichtenheft mit technischer Umsetzung | 0.3 |
|
||||
| **🔄 Use Cases** | UML-Use-Case-Modellierung mit Akteuren und Abläufen | 0.4 |
|
||||
| **👥 Stakeholder-Analyse** | Identifikation und Analyse aller Projekt-Stakeholder | 0.5 |
|
||||
| **📖 Glossar** | Fachglossar mit Definitionen und Synonymen | 0.2 |
|
||||
| **✅ Qualitätsanforderungen** | Qualitätsanforderungen nach ISO 25010 | 0.3 |
|
||||
| **⚠️ Risikoanalyse** | Identifikation und Bewertung von Projektrisiken | 0.5 |
|
||||
|
||||
**Prompt-Editor:**
|
||||
|
||||
Jeder Modus hat einen konfigurierbaren System-Prompt. Klicke auf **Prompt bearbeiten**, um den Prompt für die aktuelle Session anzupassen. Mit **Zurücksetzen** wird der Standard-Prompt aus den Einstellungen geladen.
|
||||
|
||||
**Temperatur:**
|
||||
|
||||
Der Temperatur-Regler beeinflusst die Kreativität des Modells:
|
||||
- **0.0–0.2**: Präzise, deterministisch
|
||||
- **0.3–0.5**: Ausgewogen, faktennah
|
||||
- **0.6–1.2**: Kreativ, überraschende Wendungen
|
||||
- **1.3–2.0**: Chaotisch, experimentell
|
||||
|
||||
**Exposé-Kontext:**
|
||||
|
||||
In den Modi Lektorat, Co-Author und Hook-Finder wird automatisch das per Kategorie markierte Exposé als Kontext in den Prompt eingefügt.
|
||||
|
||||
### 5.4 Einstellungen
|
||||
|
||||
Die Einstellungsseite (`/settings`) ist in vier Spalten organisiert:
|
||||
|
||||
| Spalte | Inhalt |
|
||||
|--------|--------|
|
||||
| Links (Admin) | Modelle, Ollama-Server, Diagnose, Design, Sprache |
|
||||
| Mitte-Links | Lektor-System-Prompt, Fact-Check-Vorlage, Rechtschreibungs-Prompt |
|
||||
| Mitte-Rechts | Advocatus Diaboli, Hook-Finder, ELI5, Co-Author |
|
||||
| Rechts | Alle 7 technischen Prompts mit Temperatur |
|
||||
|
||||
**Projektkontext:**
|
||||
|
||||
Wenn ein Projekt geladen ist, werden die Einstellungen für dieses Projekt gespeichert. Ohne Projekt gelten die benutzerspezifischen Einstellungen.
|
||||
|
||||
### 5.5 Modellverwaltung
|
||||
|
||||
Die Modellverwaltung (`/models`) zeigt alle lokal vorhandenen Ollama-Modelle mit Größe und Änderungsdatum an. Neue Modelle können direkt heruntergeladen werden:
|
||||
|
||||
1. Modellname eingeben (z.B. `llama3.2`, `mistral`, `gemma4`)
|
||||
2. Auf **Herunterladen** klicken
|
||||
3. Der Fortschritt wird in Echtzeit angezeigt
|
||||
|
||||
### 5.6 Benutzerverwaltung
|
||||
|
||||
Die Benutzerverwaltung (`/user-management`) ist nur für Administratoren zugänglich:
|
||||
|
||||
- Liste aller Benutzer mit Rolle und Status
|
||||
- Benutzer aktivieren/deaktivieren
|
||||
- Benutzer löschen (der eigene Account kann nicht gelöscht werden)
|
||||
|
||||
**Zwei-Faktor-Authentifizierung (2FA):**
|
||||
|
||||
Jeder Benutzer kann 2FA einrichten:
|
||||
1. Unter **Einstellungen → 2FA einrichten** QR-Code scannen
|
||||
2. Geheimen Schlüssel notieren
|
||||
3. Code zur Bestätigung eingeben
|
||||
|
||||
### 5.7 Datenbank-Administration
|
||||
|
||||
Die DB-Admin-Seite (`/db-admin`) ist Administratoren vorbehalten und erlaubt:
|
||||
|
||||
- Auswahl der Datenbank-Tabelle (users, projects, settings, fileCategories, documentMeta, vectors)
|
||||
- Anzeige aller Datensätze in einer Tabelle
|
||||
- Bearbeiten von Feldwerten (Öffnet Bearbeitungs-Modal)
|
||||
- Löschen von Datensätzen (mit Bestätigungsdialog)
|
||||
|
||||
> **Achtung:** Hier können kritische Daten verändert oder gelöscht werden. Nur durchführen, wenn du genau weißt, was du tust.
|
||||
|
||||
---
|
||||
|
||||
## 6. Datenbank
|
||||
|
||||
Die Anwendung verwendet **LiteDB** – eine eingebettete NoSQL-Dokumentendatenbank. Die Datenbankdatei (`author_brain.db`) liegt im Ausführungsverzeichnis der Anwendung.
|
||||
|
||||
### 6.1 Collections
|
||||
|
||||
| Collection | Entity | Zweck |
|
||||
|------------|--------|-------|
|
||||
| `users` | `UserEntity` | Benutzerkonten mit Passwort-Hash und 2FA-Secret |
|
||||
| `projects` | `ProjectEntity` | Projekte mit Name, Pfad, Besitzer und Vorlagentyp |
|
||||
| `settings` | `SettingEntity` | Key-Value-Einstellungen (global/benutzer-/projektspezifisch) |
|
||||
| `fileCategories` | `FileCategoryEntity` | Datei-Kategorie-Zuordnungen |
|
||||
| `documentMeta` | `DocumentMetaEntity` | Metadaten indexierter Dokumente (Hash, Änderungsdatum) |
|
||||
| `vectors` | `DocumentVector` | Embedding-Vektoren für die RAG-Suche |
|
||||
|
||||
### 6.2 Einstellungshierarchie
|
||||
|
||||
Einstellungen werden mit absteigender Priorität aufgelöst:
|
||||
1. **Projektspezifisch** (`UserId` + `ProjectId` gesetzt)
|
||||
2. **Benutzerspezifisch** (`UserId` gesetzt, `ProjectId` null)
|
||||
3. **Global** (beide null)
|
||||
|
||||
---
|
||||
|
||||
## 7. Themes & Lokalisierung
|
||||
|
||||
### 7.1 Themes
|
||||
|
||||
Drei CSS-Themes stehen zur Verfügung:
|
||||
|
||||
| Theme | Beschreibung |
|
||||
|-------|-------------|
|
||||
| **Hell** (Standard) | Helles Farbschema mit blauen Akzenten |
|
||||
| **Dunkel** | Dark Mode, inspiriert von VS Code |
|
||||
| **W95** | Windows 95 Retro-Stil |
|
||||
|
||||
Die Theme-Umschaltung erfolgt durch dynamisches Einfügen eines `<link>`-Elements in den Seitenkopf.
|
||||
|
||||
### 7.2 Lokalisierung
|
||||
|
||||
Unterstützte Sprachen:
|
||||
- **Deutsch** (`de`) – Standard
|
||||
- **Englisch** (`en`)
|
||||
|
||||
Die Übersetzungen liegen als JSON-Dateien im Ordner `Resources/`. Die Sprachumschaltung erfolgt in den Einstellungen und wird sofort wirksam. Für nicht gefundene Schlüssel gibt es eingebaute deutsche Fallback-Texte.
|
||||
|
||||
---
|
||||
|
||||
## 8. LLM-Backends
|
||||
|
||||
### 8.1 Ollama (Standard)
|
||||
|
||||
- **Status**: Voll unterstützt
|
||||
- **Modellverwaltung**: Ja (Pull, Delete)
|
||||
- **API**: REST (`/api/generate`, `/api/embeddings`, `/api/tags`, `/api/pull`, `/api/delete`, `/api/show`)
|
||||
- **Standard-URL**: `http://localhost:11434`
|
||||
|
||||
### 8.2 llama.cpp
|
||||
|
||||
- **Status**: Basisfunktionen unterstützt
|
||||
- **Modellverwaltung**: Nein
|
||||
- **API**: OpenAI-kompatibel (`/v1/chat/completions`, `/v1/models`) + `/embedding`, `/slots`
|
||||
- **Standard-URL**: `http://localhost:8080`
|
||||
|
||||
Voreingestellte Modelle (in `OllamaSettings`):
|
||||
|
||||
| Modell | Zweck |
|
||||
|--------|-------|
|
||||
| `gemma4:e4b` | Stil-Analyse (Lektorat) |
|
||||
| `nomic-embed-text` | Embeddings (RAG) |
|
||||
| `phi3:mini` | Rechtschreibprüfung |
|
||||
|
||||
---
|
||||
|
||||
## 9. Technische Referenz
|
||||
|
||||
### 9.1 Routen
|
||||
|
||||
| Route | Seite | Zugriff |
|
||||
|-------|-------|---------|
|
||||
| `/` | Dashboard | Authentifiziert |
|
||||
| `/login` | Anmeldung | Öffentlich |
|
||||
| `/register` | Registrierung | Öffentlich |
|
||||
| `/editor` | Editor | Authentifiziert |
|
||||
| `/editor/index/{id}` | Editor mit Projekt | Authentifiziert |
|
||||
| `/editor/newproject` | Editor mit Erstell-Dialog | Authentifiziert |
|
||||
| `/assistant` | KI-Assistent | Authentifiziert |
|
||||
| `/settings` | Einstellungen | Authentifiziert |
|
||||
| `/models` | Modellverwaltung | Authentifiziert |
|
||||
| `/user-management` | Benutzerverwaltung | Authentifiziert |
|
||||
| `/setup-2fa` | 2FA einrichten | Authentifiziert |
|
||||
| `/setup-assistant` | Setup-Assistent | Authentifiziert |
|
||||
| `/db-admin` | DB-Administration | Authentifiziert (Admin) |
|
||||
| `/not-found` | 404 | Öffentlich |
|
||||
| `/Error` | 500 | Öffentlich |
|
||||
|
||||
### 9.2 NuGet-Pakete
|
||||
|
||||
- **DiffPlex** 1.9.0 – Text-Differencing für die Diff-Ansicht
|
||||
- **LiteDB** 5.0.21 – Eingebettete NoSQL-Dokumentendatenbank
|
||||
|
||||
### 9.3 Schlüssel-Klassen
|
||||
|
||||
| Namespace | Klasse | Zweck |
|
||||
|-----------|--------|-------|
|
||||
| `Models` | `Constants` | App-Konstanten (DB-Name, Ordnerstruktur) |
|
||||
| `Models` | `OllamaSettings` | Zentrale Konfiguration (Singleton) |
|
||||
| `Models` | `ProjectTemplate` | Vorlagentyp (Novel, Technical) |
|
||||
| `Models` | `FileCategory` | Datei-Kategorien als Flags-Enum |
|
||||
| `Models` | `FileTreeItem` | Baumstruktur für Projektdateien |
|
||||
| `Data` | `UserEntity` | Benutzer-DB-Entity |
|
||||
| `Data` | `ProjectEntity` | Projekt-DB-Entity |
|
||||
| `Data` | `SettingEntity` | Einstellungs-DB-Entity |
|
||||
| `Data` | `DocumentVector` | Embedding-Vektor-DB-Entity |
|
||||
| `Services` | `AuthService` | Authentifizierung mit 2FA |
|
||||
| `Services` | `SettingsService` | Einstellungs-Verwaltung |
|
||||
| `Services` | `ProjectService` | Projekt-CRUD |
|
||||
| `Services` | `OllamaService` | LLM-Kommunikation (Fassade) |
|
||||
| `Services` | `RagService` | RAG-Indexierung und -Suche |
|
||||
| `Services` | `DbAdminService` | DB-Administration |
|
||||
|
||||
### 9.4 Wichtige Konstanten
|
||||
|
||||
```csharp
|
||||
Constants.DbFile // "author_brain.db"
|
||||
Constants.SubFolders // Standard-Ordnerstruktur (Referenz)
|
||||
```
|
||||
|
||||
Projekt-Ordnerstruktur bei **Roman**-Vorlage:
|
||||
```
|
||||
{rootPath}/
|
||||
├── 00_Drafts/
|
||||
├── 01_Fakten/
|
||||
├── 02_Characters/
|
||||
├── 03_Archive/
|
||||
├── 00_Projekt_Info.md
|
||||
├── Die Handlung (Das Exposé).md
|
||||
└── Ideen Sammlung.md
|
||||
```
|
||||
|
||||
Projekt-Ordnerstruktur bei **Technischer** Vorlage:
|
||||
```
|
||||
{rootPath}/
|
||||
├── 00_Anforderungen/
|
||||
├── 01_UseCases/
|
||||
├── 02_Architektur/
|
||||
├── 03_Glossar/
|
||||
├── 00_Projekt_Info.md
|
||||
├── Lastenheft.md
|
||||
└── Pflichtenheft.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Entwicklung & Erweiterung
|
||||
|
||||
### 10.1 Neuen Assistenten-Modus hinzufügen
|
||||
|
||||
1. **Prompt-Default** in `Models/OllamaSettings.cs` (DE + EN + Temp)
|
||||
2. **Settings-Key** in `Services/SettingsService.cs` (Key-Konstante + Getter/Setter)
|
||||
3. **Interface-Methoden** in `Services/ISettingsService.cs`
|
||||
4. **Settings-UI** in `Components/Pages/Settings.razor`
|
||||
5. **Mode-Dropdown** + `LoadPromptForMode` in `Components/Pages/OllamaAssistant.razor`
|
||||
6. **Lokalisierung** in `Resources/de.json` + `Resources/en.json`
|
||||
7. **DB-Seeding** in `SettingsService.InitializeAsync()` + `LoadProjectSettingsAsync()`
|
||||
|
||||
### 10.2 Neues Daten-Modell hinzufügen
|
||||
|
||||
1. Entity-Klasse in `Data/` anlegen
|
||||
2. Collection-Namen in `Services/DbAdminService.KnownCollections` aufnehmen
|
||||
3. Service-Methoden für CRUD-Operationen schreiben
|
||||
4. Bei Bedarf UI-Komponente erstellen
|
||||
|
||||
---
|
||||
|
||||
*Dokumentation erstellt am 31.05.2026*
|
||||
89
Data/AppDbContext.cs
Normal file
89
Data/AppDbContext.cs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace AuthorBuddy.Web.Data;
|
||||
|
||||
public class AppDbContext : DbContext
|
||||
{
|
||||
public DbSet<ProjectEntity> Projects => Set<ProjectEntity>();
|
||||
public DbSet<SettingEntity> Settings => Set<SettingEntity>();
|
||||
public DbSet<DocumentMetaEntity> DocumentMeta => Set<DocumentMetaEntity>();
|
||||
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<ProjectEntity>(entity =>
|
||||
{
|
||||
entity.ToTable("Projects");
|
||||
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");
|
||||
});
|
||||
|
||||
modelBuilder.Entity<SettingEntity>(entity =>
|
||||
{
|
||||
entity.ToTable("Settings");
|
||||
entity.HasKey(e => e.Key);
|
||||
entity.Property(e => e.Key).HasColumnName("key");
|
||||
entity.Property(e => e.Value).HasColumnName("value").IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<DocumentMetaEntity>(entity =>
|
||||
{
|
||||
entity.ToTable("DocumentMeta");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.FilePath).HasColumnName("file_path").IsRequired();
|
||||
entity.Property(e => e.LastModified).HasColumnName("last_modified");
|
||||
entity.Property(e => e.FileType).HasColumnName("file_type");
|
||||
entity.Property(e => e.ProjectId).HasColumnName("project_id");
|
||||
});
|
||||
}
|
||||
|
||||
public static void InitializeDatabase(string connectionString)
|
||||
{
|
||||
using var connection = new SqliteConnection(connectionString);
|
||||
connection.Open();
|
||||
connection.EnableExtensions(true);
|
||||
|
||||
try
|
||||
{
|
||||
connection.LoadExtension("vec0");
|
||||
using var testCmd = connection.CreateCommand();
|
||||
testCmd.CommandText = "SELECT vec_version();";
|
||||
var version = testCmd.ExecuteScalar();
|
||||
Console.WriteLine($"sqlite-vec Version: {version}");
|
||||
}
|
||||
catch (SqliteException ex)
|
||||
{
|
||||
Console.WriteLine($"Fehler beim Laden von vec0: {ex.Message}");
|
||||
}
|
||||
|
||||
using var createVecCmd = connection.CreateCommand();
|
||||
createVecCmd.CommandText = @"
|
||||
CREATE TABLE IF NOT EXISTS Projects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
root_path TEXT NOT NULL,
|
||||
last_opened DATETIME
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS Settings (
|
||||
key TEXT NOT NULL UNIQUE,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS DocumentMeta (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id INTEGER,
|
||||
file_path TEXT UNIQUE,
|
||||
last_modified DATETIME,
|
||||
hash TEXT,
|
||||
file_type TEXT,
|
||||
FOREIGN KEY(project_id) REFERENCES Projects(id)
|
||||
);
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS vec_documents USING vec0(
|
||||
embedding float[1024]
|
||||
);";
|
||||
createVecCmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,23 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace AuthorBuddy.Web.Data;
|
||||
|
||||
[Table("DocumentMeta")]
|
||||
public class DocumentMetaEntity
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public int Id { get; set; }
|
||||
|
||||
public int? ProjectId { get; set; }
|
||||
|
||||
[Required]
|
||||
public string FilePath { get; set; } = string.Empty;
|
||||
|
||||
public DateTime? LastModified { get; set; }
|
||||
|
||||
public string? Hash { get; set; }
|
||||
|
||||
public string? FileType { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
namespace AuthorBuddy.Web.Data;
|
||||
|
||||
public class DocumentVector
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int DocumentMetaId { get; set; }
|
||||
public int ProjectId { get; set; }
|
||||
public string Text { get; set; } = string.Empty;
|
||||
public string Hash { get; set; } = string.Empty;
|
||||
public float[] Embedding { get; set; } = Array.Empty<float>();
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
using AuthorBuddy.Web.Models;
|
||||
|
||||
namespace AuthorBuddy.Web.Data;
|
||||
|
||||
public class FileCategoryEntity
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int ProjectId { get; set; }
|
||||
public string FilePath { get; set; } = string.Empty;
|
||||
public FileCategory Category { get; set; } = FileCategory.None;
|
||||
}
|
||||
|
|
@ -1,13 +1,20 @@
|
|||
using AuthorBuddy.Web.Models;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace AuthorBuddy.Web.Data;
|
||||
|
||||
[Table("Projects")]
|
||||
public class ProjectEntity
|
||||
{
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string RootPath { get; set; } = string.Empty;
|
||||
public int UserId { get; set; }
|
||||
|
||||
public DateTime? LastOpened { get; set; }
|
||||
public ProjectTemplate Template { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
using LiteDB;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace AuthorBuddy.Web.Data;
|
||||
|
||||
[Table("Settings")]
|
||||
public class SettingEntity
|
||||
{
|
||||
public int Id { get; set; }
|
||||
[Key]
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string Value { get; set; } = string.Empty;
|
||||
public int? UserId { get; set; }
|
||||
public int? ProjectId { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
namespace AuthorBuddy.Web.Data;
|
||||
|
||||
public enum UserRole
|
||||
{
|
||||
Author,
|
||||
Administrator
|
||||
}
|
||||
|
||||
public class UserEntity
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string PasswordHash { get; set; } = string.Empty;
|
||||
public string TwoFactorSecret { get; set; } = string.Empty;
|
||||
public bool TwoFactorEnabled { get; set; }
|
||||
public UserRole Role { get; set; } = UserRole.Author;
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
259
INSTALL.md
259
INSTALL.md
|
|
@ -1,259 +0,0 @@
|
|||
# AuthorBuddy – Installation auf Ubuntu VPS
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Ubuntu 22.04 oder 24.04 LTS
|
||||
- Root-Zugriff oder `sudo`
|
||||
- Mindestens 8 GB RAM, 4 Kerne (empfohlen für lokale LLMs)
|
||||
- ca. 15 GB freier Speicher für Ollama + Modelle
|
||||
|
||||
---
|
||||
|
||||
## 1. System aktualisieren & Abhängigkeiten
|
||||
|
||||
```bash
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
sudo apt install -y curl git ufw
|
||||
```
|
||||
|
||||
## 2. .NET 10 SDK / Runtime installieren
|
||||
|
||||
```bash
|
||||
# Microsoft-Paketquelle registrieren
|
||||
sudo apt install -y wget
|
||||
wget https://dot.net/v1/dotnet-install.sh -O /tmp/dotnet-install.sh
|
||||
chmod +x /tmp/dotnet-install.sh
|
||||
|
||||
# .NET 10 SDK (zum Build)
|
||||
sudo /tmp/dotnet-install.sh --channel 10.0 --install-dir /usr/share/dotnet
|
||||
|
||||
# Symlink für globale Nutzung
|
||||
sudo ln -sf /usr/share/dotnet/dotnet /usr/local/bin/dotnet
|
||||
|
||||
# Prüfen
|
||||
dotnet --version
|
||||
# => 10.0.xxx
|
||||
```
|
||||
|
||||
> Falls nur Runtime benötigt wird: `--channel 10.0 --runtime aspnetcore`
|
||||
|
||||
## 3. Ollama installieren
|
||||
|
||||
```bash
|
||||
curl -fsSL https://ollama.com/install.sh | sh
|
||||
|
||||
# Dienst starten
|
||||
sudo systemctl enable ollama
|
||||
sudo systemctl start ollama
|
||||
|
||||
# Prüfen
|
||||
ollama --version
|
||||
|
||||
# Port prüfen (default 11434)
|
||||
ss -tlnp | grep 11434
|
||||
```
|
||||
|
||||
### Modelle pullen
|
||||
|
||||
Für den Betrieb werden mindestens 2 Modelle empfohlen:
|
||||
|
||||
```bash
|
||||
# Standard-Modell für Lektorat, Assistent, Stilanalyse
|
||||
ollama pull llama3.2:3b
|
||||
|
||||
# Modell für Rechtschreibprüfung (kleiner, schneller)
|
||||
ollama pull llama3.2:1b
|
||||
```
|
||||
|
||||
Größere Modelle (7B–13B) liefern bessere Texte. Beispiele:
|
||||
|
||||
```bash
|
||||
ollama pull llama3.1:8b
|
||||
ollama pull dolphin-mistral:7b
|
||||
```
|
||||
|
||||
**Übersicht Speicherverbrauch:**
|
||||
| Modell | RAM/VRAM |
|
||||
|--------|----------|
|
||||
| llama3.2:1b | ~1 GB |
|
||||
| llama3.2:3b | ~2,5 GB |
|
||||
| llama3.1:8b | ~5,5 GB |
|
||||
| dolphin-mistral:7b | ~5 GB |
|
||||
|
||||
---
|
||||
|
||||
## 4. App bauen & deployen
|
||||
|
||||
```bash
|
||||
# Quellcode auf den Server kopieren (z.B. via SCP oder Git)
|
||||
git clone https://github.com/dein-repo/AuthorBuddy.Web /opt/AuthorBuddy.Web
|
||||
# ODER: scp -r . ubuntu@deine-vps:/opt/AuthorBuddy.Web
|
||||
|
||||
cd /opt/AuthorBuddy.Web
|
||||
|
||||
# Restore & Build (Release)
|
||||
dotnet restore
|
||||
dotnet publish -c Release -o /opt/AuthorBuddy.Web/publish
|
||||
|
||||
# Daten-Verzeichnis für Projekte anlegen
|
||||
mkdir -p /home/ubuntu/Documents/AuthorBuddy
|
||||
```
|
||||
|
||||
### Datenbankpfad prüfen
|
||||
|
||||
LiteDB erzeugt die Datenbank `author_brain.db` standardmäßig im `bin/`-Verzeichnis.
|
||||
Besser: Ein festes Verzeichnis setzen → Umgebungsvariable oder Code-Anpassung in `Program.cs`:
|
||||
|
||||
```csharp
|
||||
// Program.cs (optional – falls gewünscht)
|
||||
var dbDir = "/opt/AuthorBuddy.Web/data";
|
||||
Directory.CreateDirectory(dbDir);
|
||||
var dbPath = Path.Combine(dbDir, "author_brain.db");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Systemd-Service einrichten
|
||||
|
||||
```bash
|
||||
sudo nano /etc/systemd/system/authorbuddy.service
|
||||
```
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=AuthorBuddy Web
|
||||
After=network.target ollama.service
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/opt/AuthorBuddy.Web/publish
|
||||
ExecStart=/usr/share/dotnet/dotnet /opt/AuthorBuddy.Web/publish/AuthorBuddy.Web.dll
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
KillSignal=SIGINT
|
||||
SyslogIdentifier=authorbuddy
|
||||
User=ubuntu
|
||||
Environment=ASPNETCORE_URLS=http://localhost:5000
|
||||
Environment=ASPNETCORE_ENVIRONMENT=Production
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable authorbuddy
|
||||
sudo systemctl start authorbuddy
|
||||
|
||||
# Status prüfen
|
||||
sudo systemctl status authorbuddy
|
||||
|
||||
# Logs live
|
||||
sudo journalctl -u authorbuddy -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Reverse Proxy mit Nginx
|
||||
|
||||
```bash
|
||||
sudo apt install -y nginx
|
||||
```
|
||||
|
||||
`/etc/nginx/sites-available/authorbuddy`:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name deine-domain.de;
|
||||
|
||||
# (Optional: später mit Certbot auf HTTPS upgraden)
|
||||
# return 301 https://$host$request_uri;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:5000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Blazor Server benötigt lange Timeouts für Streaming
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 86400s;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/authorbuddy /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
### HTTPS (Let's Encrypt)
|
||||
|
||||
```bash
|
||||
sudo apt install -y certbot python3-certbot-nginx
|
||||
sudo certbot --nginx -d deine-domain.de
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Firewall
|
||||
|
||||
```bash
|
||||
sudo ufw allow OpenSSH
|
||||
sudo ufw allow 'Nginx Full'
|
||||
sudo ufw enable
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Einrichtung in der App
|
||||
|
||||
Nach dem ersten Start:
|
||||
|
||||
1. **Im Browser öffnen:** `http://deine-domain.de`
|
||||
2. **Ersten Benutzer registrieren** (wird automatisch Admin)
|
||||
3. **Settings → Ollama Server:** `http://localhost:11434`
|
||||
4. **Test** – Verbindung prüfen
|
||||
5. **Modelle zuweisen:**
|
||||
- RAG-Modell: `llama3.2:3b` (oder größer)
|
||||
- Stil-Modell: `llama3.2:3b`
|
||||
- Rechtschreib-Modell: `llama3.2:1b` (klein = schnell)
|
||||
6. **Prompts prüfen** – Standard-Prompts sind vorbelegt
|
||||
|
||||
---
|
||||
|
||||
## 9. Nützliche Befehle
|
||||
|
||||
| Aktion | Befehl |
|
||||
|--------|--------|
|
||||
| App neustarten | `sudo systemctl restart authorbuddy` |
|
||||
| Logs ansehen | `sudo journalctl -u authorbuddy -f` |
|
||||
| Ollama neustarten | `sudo systemctl restart ollama` |
|
||||
| Verfügbare Modelle | `ollama list` |
|
||||
| Modell laufend testen | `ollama run llama3.2:3b` |
|
||||
| App stoppen | `sudo systemctl stop authorbuddy` |
|
||||
|
||||
---
|
||||
|
||||
## 10. Backup
|
||||
|
||||
```bash
|
||||
# Datenbank + Projekte sichern
|
||||
sudo tar czf /root/backup-authorbuddy-$(date +%Y%m%d).tar.gz \
|
||||
/opt/AuthorBuddy.Web/data \
|
||||
/home/ubuntu/Documents/AuthorBuddy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
- **502 Bad Gateway** → App-Logs prüfen: `journalctl -u authorbuddy -f`
|
||||
- **Ollama nicht erreichbar** → `curl http://localhost:11434` testen
|
||||
- **Port bereits belegt** → `ASPNETCORE_URLS` in der Service-Datei ändern
|
||||
- **Blazor SignalR disconnect** → `proxy_read_timeout` in Nginx erhöhen (86400s)
|
||||
73
LICENSE
73
LICENSE
|
|
@ -1,73 +0,0 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
|
||||
|
||||
Copyright 2026 adminroot
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
|
@ -10,5 +10,5 @@ public static class Constants
|
|||
"Exports"
|
||||
};
|
||||
|
||||
public const string DbFile = "author_brain.db";
|
||||
public const string SQLiteConnection = "Data Source=author_brain.db";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
namespace AuthorBuddy.Web.Models;
|
||||
|
||||
[Flags]
|
||||
public enum FileCategory
|
||||
{
|
||||
None = 0,
|
||||
Character = 1,
|
||||
Exposee = 2,
|
||||
Fact = 4,
|
||||
Document = 8
|
||||
}
|
||||
|
||||
public static class FileCategoryHelper
|
||||
{
|
||||
public static bool IsValid(FileCategory value)
|
||||
{
|
||||
if (value == FileCategory.None) return true;
|
||||
|
||||
// Exposee and Document must be alone
|
||||
bool hasExposee = value.HasFlag(FileCategory.Exposee);
|
||||
bool hasDocument = value.HasFlag(FileCategory.Document);
|
||||
bool hasCharacter = value.HasFlag(FileCategory.Character);
|
||||
bool hasFact = value.HasFlag(FileCategory.Fact);
|
||||
|
||||
if (hasExposee && value != FileCategory.Exposee) return false;
|
||||
if (hasDocument && value != FileCategory.Document) return false;
|
||||
|
||||
// Charakter + Fakt is the only valid multi-category combination
|
||||
if (hasCharacter && hasFact && value == (FileCategory.Character | FileCategory.Fact)) return true;
|
||||
|
||||
// Single categories are valid
|
||||
if (value is FileCategory.Character or FileCategory.Fact or FileCategory.Exposee or FileCategory.Document) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static string DisplayName(FileCategory category) => category switch
|
||||
{
|
||||
FileCategory.Character => "Charakter",
|
||||
FileCategory.Exposee => "Exposé",
|
||||
FileCategory.Fact => "Fakt",
|
||||
FileCategory.Document => "Dokument",
|
||||
FileCategory.Character | FileCategory.Fact => "Charakter + Fakt",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
public static string DisplayIcon(FileCategory category) => category switch
|
||||
{
|
||||
FileCategory.Character => "👤",
|
||||
FileCategory.Exposee => "📋",
|
||||
FileCategory.Fact => "📌",
|
||||
FileCategory.Document => "📄",
|
||||
FileCategory.Character | FileCategory.Fact => "👤📌",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
public static string CssClass(FileCategory category) => category switch
|
||||
{
|
||||
FileCategory.Character => "category-character",
|
||||
FileCategory.Exposee => "category-exposee",
|
||||
FileCategory.Fact => "category-fact",
|
||||
FileCategory.Document => "category-document",
|
||||
FileCategory.Character | FileCategory.Fact => "category-character-fact",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
namespace AuthorBuddy.Web.Models;
|
||||
|
||||
public class FileTreeItem
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string FullPath { get; set; } = string.Empty;
|
||||
public bool IsDirectory { get; set; }
|
||||
public FileCategory Category { get; set; }
|
||||
public List<FileTreeItem> Children { get; set; } = new();
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
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 LLM_Model(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public long Size { get; set; }
|
||||
public DateTime? ModifiedAt { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
using Ollama;
|
||||
|
||||
namespace AuthorBuddy.Web.Models;
|
||||
|
||||
//namespace AuthorBuddy.Web.Models;
|
||||
public class ModelStatus
|
||||
{
|
||||
public string Status { get; private set; } = "Unknown";
|
||||
public double Progress { get; private set; }
|
||||
|
||||
//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";
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,110 +2,26 @@ namespace AuthorBuddy.Web.Models;
|
|||
|
||||
public class OllamaSettings
|
||||
{
|
||||
public string StyleModel { get; set; } = "gemma4:e4b";
|
||||
public string EmbedModel { get; set; } = "nomic-embed-text";
|
||||
public string ChatModel { 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 LlamaCppUrl { get; set; } = "http://localhost:8080";
|
||||
public string Backend { get; set; } = "Ollama";
|
||||
public string Theme { get; set; } = "light";
|
||||
public string Language { get; set; } = "de";
|
||||
|
||||
public string LektorSystemPrompt { get; set; } = "Du bist ein erfahrener Lektor. Analysiere den Text des Autors. " +
|
||||
public string LektorSystemPrompt { get; private 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.";
|
||||
|
||||
public string TechLastenheftPrompt { get; set; } = "Du bist ein erfahrener Requirements Engineer nach IREB-Standard. Erstelle ein Lastenheft aus den gegebenen Informationen. Gliedere es in: 1. Zielbestimmung, 2. Produkteinsatz, 3. Produktumgebung, 4. Funktionale Anforderungen, 5. Nicht-funktionale Anforderungen, 6. Lieferumfang. Formuliere alle Anforderungen eindeutig, nachvollziehbar und überprüfbar.";
|
||||
|
||||
public string TechPflichtenheftPrompt { get; set; } = "Du bist ein erfahrener Systemarchitekt. Erstelle aus dem Lastenheft ein detailliertes Pflichtenheft. Beschreibe: 1. Systemarchitektur, 2. Komponenten, 3. Schnittstellen, 4. Datenmodell, 5. Technische Umsetzung. Verwende präzise technische Sprache und vermeide Mehrdeutigkeiten.";
|
||||
|
||||
public string TechUseCasePrompt { get; set; } = "Du bist ein Experte für Use-Case-Modellierung nach UML-Standard. Definiere die wichtigsten Anwendungsfälle. Jeder Use Case enthält: 1. Akteure, 2. Vorbedingungen, 3. Nachbedingungen, 4. Normaler Ablauf (Schritt für Schritt), 5. Alternative Abläufe, 6. Ausnahmen.";
|
||||
|
||||
public string TechStakeholderPrompt { get; set; } = "Du bist ein erfahrener Stakeholder-Analyst. Identifiziere alle relevanten Stakeholder für das Projekt. Für jeden Stakeholder beschreibe: 1. Rolle und Interesse, 2. Erwartungen und Ziele, 3. Einfluss und Priorität, 4. Kommunikationsbedarf. Berücksichtige auch indirekte und versteckte Stakeholder.";
|
||||
|
||||
public string TechGlossarPrompt { get; set; } = "Du bist ein Terminologie-Manager. Erstelle ein fachliches Glossar mit klaren Definitionen aller wichtigen Begriffe, Abkürzungen und Akronyme aus dem Projektkontext. Strukturiere alphabetisch. Jeder Eintrag enthält: Begriff, Definition, Synonyme, Kontext.";
|
||||
|
||||
public string TechQualitaetPrompt { get; set; } = "Du bist ein Qualitätsmanager nach ISO 25010. Definiere die Qualitätsanforderungen in folgende Kategorien: 1. Funktionale Eignung, 2. Zuverlässigkeit, 3. Benutzbarkeit, 4. Effizienz, 5. Wartbarkeit, 6. Portabilität. Formuliere messbare Kriterien.";
|
||||
|
||||
public string TechRisikoPrompt { get; set; } = "Du bist ein Risikomanager. Identifiziere technische und fachliche Risiken des Projekts. Für jedes Risiko beschreibe: 1. Beschreibung, 2. Eintrittswahrscheinlichkeit (niedrig/mittel/hoch), 3. Auswirkung (niedrig/mittel/hoch), 4. Risikobewertung, 5. Maßnahmen zur Risikominimierung.";
|
||||
|
||||
public string FactCheckTemplate { get; set; } = "FAKTEN AUS DER DATENBANK:\n{0}\n\n" +
|
||||
private 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.";
|
||||
|
||||
public string SpellingPrompt { get; set; } = "Du bist ein Rechtschreibprüfer. Korrigiere NUR Fehler (Rechtschreibung/Grammatik). " +
|
||||
"Ändere den Stil nicht. Gib nur den korrigierten Text zurück.";
|
||||
|
||||
public string AdvocatusDiaboliPrompt { get; set; } = "Nimm eine radikale Gegenposition zu meinem Text ein. Welche Schwachstellen in meiner Argumentation fallen dir auf?";
|
||||
|
||||
public string HookFinderPrompt { get; set; } = "Generiere mir 5 emotionale, packende Einstiegssätze (Hooks) für diesen Text, die den Leser sofort fesseln.";
|
||||
|
||||
public string Eli5Prompt { get; set; } = "Wo ist der Text zu kompliziert geschrieben? Liefere mir konkrete Vorschläge, wie ich komplexe Absätze einfacher formulieren kann, ohne den Kern zu verlieren.";
|
||||
|
||||
public string CoAuthorBrainstormingPrompt { get; set; } = "Du bist ein kreativer Co-Autor. Der Nutzer gibt dir eine Idee, einen Charakter oder eine Situation. " +
|
||||
"Hilf ihm, die Handlung oder Personen weiterzuentwickeln. Stelle kluge Fragen, schlage alternative Wendungen vor, " +
|
||||
"denke über Motivationen, Konflikte und emotionale Tiefe nach. Sei inspirierend, aber bleib beim Thema.";
|
||||
|
||||
public string LektorSystemPromptEn { get; } = "You are an experienced editor. Analyze the author's text. " +
|
||||
"Optimize it for a vivid, descriptive, and subjective style. " +
|
||||
"Use strong verbs, avoid unnecessary adjectives, and follow 'Show, don't tell'. " +
|
||||
"Respond directly with the improved text or concrete suggestions.";
|
||||
|
||||
public string FactCheckTemplateEn { get; } = "FACTS FROM DATABASE:\n{0}\n\n" +
|
||||
"QUESTION / CONTEXT:\n{1}\n\n" +
|
||||
"Based on the facts, write a short, vivid addition or correction.";
|
||||
|
||||
public string SpellingPromptEn { get; } = "You are a spell checker. Correct ONLY errors (spelling/grammar). " +
|
||||
"Do not change the style. Return only the corrected text.";
|
||||
|
||||
public string AdvocatusDiaboliPromptEn { get; } = "Take a radical opposing position to my text. What weaknesses in my argumentation do you notice?";
|
||||
|
||||
public string HookFinderPromptEn { get; } = "Generate 5 emotional, captivating opening sentences (hooks) for this text that immediately grab the reader's attention.";
|
||||
|
||||
public string Eli5PromptEn { get; } = "Where is the text too complicated? Give me concrete suggestions on how to simplify complex paragraphs without losing the core message.";
|
||||
|
||||
public string CoAuthorBrainstormingPromptEn { get; } = "You are a creative co-author. The user gives you an idea, a character, or a situation. " +
|
||||
"Help them develop the plot or characters further. Ask smart questions, suggest alternative twists, " +
|
||||
"think about motivations, conflicts, and emotional depth. Be inspiring but stay on topic.";
|
||||
|
||||
public string TechLastenheftPromptEn { get; } = "You are an experienced Requirements Engineer following IREB standards. Create a requirements specification from the given information. Structure it into: 1. Objectives, 2. Product application, 3. Product environment, 4. Functional requirements, 5. Non-functional requirements, 6. Deliverables. Formulate all requirements clearly, traceably, and verifiably.";
|
||||
|
||||
public string TechPflichtenheftPromptEn { get; } = "You are an experienced system architect. Create a detailed functional specification from the requirements document. Describe: 1. System architecture, 2. Components, 3. Interfaces, 4. Data model, 5. Technical implementation. Use precise technical language and avoid ambiguity.";
|
||||
|
||||
public string TechUseCasePromptEn { get; } = "You are a UML use case modeling expert. Define the most important use cases. Each use case includes: 1. Actors, 2. Preconditions, 3. Postconditions, 4. Normal flow (step by step), 5. Alternative flows, 6. Exceptions.";
|
||||
|
||||
public string TechStakeholderPromptEn { get; } = "You are an experienced stakeholder analyst. Identify all relevant stakeholders for the project. For each stakeholder describe: 1. Role and interest, 2. Expectations and goals, 3. Influence and priority, 4. Communication needs. Also consider indirect and hidden stakeholders.";
|
||||
|
||||
public string TechGlossarPromptEn { get; } = "You are a terminology manager. Create a technical glossary with clear definitions of all important terms, abbreviations, and acronyms from the project context. Structure alphabetically. Each entry contains: term, definition, synonyms, context.";
|
||||
|
||||
public string TechQualitaetPromptEn { get; } = "You are a quality manager following ISO 25010. Define the quality requirements in the following categories: 1. Functional suitability, 2. Reliability, 3. Usability, 4. Efficiency, 5. Maintainability, 6. Portability. Formulate measurable criteria.";
|
||||
|
||||
public string TechRisikoPromptEn { get; } = "You are a risk manager. Identify technical and business risks for the project. For each risk describe: 1. Description, 2. Probability (low/medium/high), 3. Impact (low/medium/high), 4. Risk assessment, 5. Risk mitigation measures.";
|
||||
|
||||
public double LektorTemp { get; set; } = 0.7;
|
||||
public double FactCheckTemp { get; set; } = 0.3;
|
||||
public double SpellingTemp { get; set; } = 0.1;
|
||||
public double AdvocatusDiaboliTemp { get; set; } = 0.9;
|
||||
public double HookFinderTemp { get; set; } = 0.9;
|
||||
public double Eli5Temp { get; set; } = 0.5;
|
||||
public double CoAuthorTemp { get; set; } = 1.0;
|
||||
|
||||
public double TechLastenheftTemp { get; set; } = 0.3;
|
||||
public double TechPflichtenheftTemp { get; set; } = 0.3;
|
||||
public double TechUseCaseTemp { get; set; } = 0.4;
|
||||
public double TechStakeholderTemp { get; set; } = 0.5;
|
||||
public double TechGlossarTemp { get; set; } = 0.2;
|
||||
public double TechQualitaetTemp { get; set; } = 0.3;
|
||||
public double TechRisikoTemp { get; set; } = 0.5;
|
||||
|
||||
public string GetDefaultPrompt(string dePrompt, string enPrompt, string currentLanguage)
|
||||
{
|
||||
return currentLanguage == "en" ? enPrompt : dePrompt;
|
||||
}
|
||||
|
||||
public string GetFactCheckPrompt(string question, string facts)
|
||||
{
|
||||
return string.Format(FactCheckTemplate, facts, question);
|
||||
return string.Format(FactCheckTemplate, question, facts);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,4 @@ public class Project
|
|||
public int Id { get; internal set; }
|
||||
public string Name { get; internal set; } = string.Empty;
|
||||
public string RootPath { get; internal set; } = string.Empty;
|
||||
public ProjectTemplate Template { get; internal set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
namespace AuthorBuddy.Web.Models;
|
||||
|
||||
public static class ProjectPathHelper
|
||||
{
|
||||
public static string AllowedBasePath =>
|
||||
Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
|
||||
"AuthorBuddy"
|
||||
);
|
||||
|
||||
public static bool IsPathAllowed(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path)) return false;
|
||||
var full = Path.GetFullPath(path);
|
||||
var allowed = Path.GetFullPath(AllowedBasePath);
|
||||
return full.StartsWith(allowed + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(full, allowed, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static string SuggestPath(string projectName)
|
||||
{
|
||||
var safeName = Sanitize(projectName);
|
||||
return Path.Combine(AllowedBasePath, safeName);
|
||||
}
|
||||
|
||||
private static string Sanitize(string name)
|
||||
{
|
||||
var invalid = Path.GetInvalidFileNameChars();
|
||||
var result = new string(name.Where(c => !invalid.Contains(c)).ToArray());
|
||||
return string.IsNullOrWhiteSpace(result) ? "Projekt" : result.Trim();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
namespace AuthorBuddy.Web.Models;
|
||||
|
||||
public enum ProjectTemplate
|
||||
{
|
||||
Novel = 0,
|
||||
Technical = 1
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
namespace AuthorBuddy.Web.Models;
|
||||
|
||||
public class WikipediaResult
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Snippet { get; set; } = string.Empty;
|
||||
public string Url { get; set; } = string.Empty;
|
||||
public string Language { get; set; } = "de";
|
||||
public string? Extract { get; set; }
|
||||
}
|
||||
|
||||
public class WikipediaSearchRequest
|
||||
{
|
||||
public string Query { get; set; } = string.Empty;
|
||||
public bool SearchGerman { get; set; } = true;
|
||||
public bool SearchEnglish { get; set; } = true;
|
||||
public int MaxResults { get; set; } = 3;
|
||||
}
|
||||
55
Program.cs
55
Program.cs
|
|
@ -1,78 +1,35 @@
|
|||
using AuthorBuddy.Web.Components;
|
||||
using AuthorBuddy.Web.Data;
|
||||
using AuthorBuddy.Web.Models;
|
||||
using AuthorBuddy.Web.Services;
|
||||
using LiteDB;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
|
||||
var dbDir = OperatingSystem.IsLinux()
|
||||
? "/opt/AuthorBuddy.Web/data"
|
||||
: AppContext.BaseDirectory;
|
||||
Directory.CreateDirectory(dbDir);
|
||||
var dbPath = Path.Combine(dbDir, Constants.DbFile);
|
||||
builder.Services.AddSingleton<ILiteDatabase>(new LiteDatabase($"Filename={dbPath};Connection=direct"));
|
||||
|
||||
builder.Services.AddSingleton<OllamaSettings>();
|
||||
builder.Services.AddSingleton<IOllamaService, OllamaService>();
|
||||
builder.Services.AddSingleton<ILLMBackend, OllamaBackend>();
|
||||
builder.Services.AddSingleton<ILLMBackend, LlamaCppBackend>();
|
||||
builder.Services.AddSingleton<IOllamaSetupService, OllamaSetupService>();
|
||||
builder.Services.AddSingleton<IFileScannerService, FileScannerService>();
|
||||
builder.Services.AddScoped<ISettingsService, SettingsService>();
|
||||
builder.Services.AddScoped<IThemeService, ThemeService>();
|
||||
builder.Services.AddSingleton<IProjectService, ProjectService>();
|
||||
builder.Services.AddSingleton<IExportService, ExportService>();
|
||||
builder.Services.AddSingleton<IRagService, RagService>();
|
||||
builder.Services.AddSingleton<IWikipediaService, WikipediaService>();
|
||||
builder.Services.AddSingleton<OperationState>();
|
||||
builder.Services.AddSingleton<IDbAdminService, DbAdminService>();
|
||||
builder.Services.AddScoped<IAuthService, AuthService>();
|
||||
builder.Services.AddScoped<ILocalizationService, LocalizationService>();
|
||||
builder.Services.AddScoped<CustomAuthStateProvider>();
|
||||
builder.Services.AddScoped<AuthenticationStateProvider>(sp =>
|
||||
sp.GetRequiredService<CustomAuthStateProvider>());
|
||||
builder.Services.AddAuthorizationCore();
|
||||
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
.AddCookie(options =>
|
||||
{
|
||||
options.LoginPath = "/login";
|
||||
options.Cookie.HttpOnly = true;
|
||||
options.ExpireTimeSpan = TimeSpan.FromDays(30);
|
||||
});
|
||||
|
||||
builder.Services.AddDbContextFactory<AppDbContext>(options =>
|
||||
options.UseSqlite(Constants.SQLiteConnection));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
AppDbContext.InitializeDatabase(Constants.SQLiteConnection);
|
||||
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
|
||||
app.UseAntiforgery();
|
||||
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
FileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "Assets")),
|
||||
RequestPath = "/Assets"
|
||||
});
|
||||
|
||||
app.MapStaticAssets();
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
|
||||
app.Run();
|
||||
|
||||
public partial class Program
|
||||
{
|
||||
public const bool DebugAutoLogin = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
# AuthorBuddy
|
||||
|
||||
|
|
@ -1,293 +0,0 @@
|
|||
{
|
||||
"common.status.ready": "Bereit",
|
||||
"common.cancel": "Abbrechen",
|
||||
"common.error": "Fehler:",
|
||||
"common.copy": "Kopieren",
|
||||
"common.status.error": "Fehler",
|
||||
"page.title.login": "Anmelden - Author Buddy",
|
||||
"page.title.register": "Registrieren - Author Buddy",
|
||||
"page.title.setup2fa": "2FA einrichten - Author Buddy",
|
||||
"page.title.settings": "Einstellungen",
|
||||
"page.title.editor": "Editor - Author Buddy",
|
||||
"page.title.assistant": "Assistent - Author Buddy",
|
||||
"page.title.models": "Modelle - Author Buddy",
|
||||
"page.title.home": "Dashboard - Author Buddy",
|
||||
"page.title.error": "Fehler",
|
||||
"page.title.not_found": "Seite nicht gefunden",
|
||||
"page.title.help": "Hilfe",
|
||||
"nav.menu.extras.help": "Hilfe",
|
||||
"nav.menu.file": "Datei",
|
||||
"nav.menu.file.new_project": "Neues Projekt",
|
||||
"nav.menu.file.open_editor": "Editor öffnen",
|
||||
"nav.menu.file.exit": "Beenden",
|
||||
"nav.menu.extras": "Extras",
|
||||
"nav.menu.extras.settings": "Einstellungen",
|
||||
"nav.menu.extras.manage_models": "Modelle verwalten",
|
||||
"nav.menu.extras.assistant": "Assistent",
|
||||
"nav.menu.extras.user_management": "Benutzerverwaltung",
|
||||
"nav.menu.projects": "Projekte",
|
||||
"nav.projects.list_title": "Liste vorhandener Projekte",
|
||||
"nav.projects.loading": "Lade Projekte...",
|
||||
"nav.projects.empty": "Keine Projekte vorhanden.",
|
||||
"nav.projects.create": "Projekt erstellen",
|
||||
"nav.projects.files": "Projektdateien",
|
||||
"nav.projects.refresh_tree": "Dateibaum aktualisieren",
|
||||
"nav.projects.file.new": "Neue Datei",
|
||||
"nav.projects.file.rename": "Umbenennen",
|
||||
"nav.rename.title": "Datei umbenennen",
|
||||
"nav.rename.label.new_name": "Neuer Name",
|
||||
"nav.rename.placeholder": "neuer_name.md",
|
||||
"nav.rename.button.confirm": "Umbenennen",
|
||||
"nav.newfile.title": "Neue Datei",
|
||||
"nav.newfile.label.filename": "Dateiname",
|
||||
"nav.newfile.placeholder": "meine_datei.md",
|
||||
"nav.newfile.button.create": "Erstellen",
|
||||
"nav.user.logged_in_as": "Angemeldet als:",
|
||||
"nav.user.manage_2fa": "2FA verwalten",
|
||||
"nav.user.logout": "Abmelden",
|
||||
"nav.error.file_exists": "Datei existiert bereits.",
|
||||
"nav.projects.copy": "Projekt kopieren",
|
||||
"nav.copy_project.title": "Projekt kopieren",
|
||||
"nav.copy_project.label.name": "Neuer Projektname:",
|
||||
"nav.copy_project.placeholder.name": "z.B. Mein Roman (Kopie)",
|
||||
"nav.copy_project.label.path": "Zielpfad:",
|
||||
"nav.copy_project.placeholder.path": "z.B. C:\\Projekte\\MeinRomanKopie",
|
||||
"nav.copy_project.button.copy": "Kopieren",
|
||||
"nav.status.project_copied": "Projekt kopiert:",
|
||||
"nav.status.renamed_to": "Umbenannt zu:",
|
||||
"nav.status.created": "Erstellt:",
|
||||
"login.heading": "Anmelden",
|
||||
"login.label.username": "Benutzername:",
|
||||
"login.label.password": "Passwort:",
|
||||
"login.button.submit": "Anmelden",
|
||||
"login.link.register": "Registrieren",
|
||||
"login.2fa.instruction": "Gib den 6-stelligen Code aus deiner Authenticator-App ein:",
|
||||
"login.2fa.label.code": "2FA-Code:",
|
||||
"login.2fa.button.verify": "Bestätigen",
|
||||
"login.error.empty_fields": "Bitte Benutzername und Passwort eingeben.",
|
||||
"login.error.invalid_credentials": "Benutzername oder Passwort falsch.",
|
||||
"login.2fa.error.invalid_code": "Bitte gib einen gültigen 6-stelligen Code ein.",
|
||||
"login.2fa.error.code_failed": "Ungültiger Code. Bitte versuche es erneut.",
|
||||
"register.heading": "Registrieren",
|
||||
"register.label.username": "Benutzername:",
|
||||
"register.label.password": "Passwort:",
|
||||
"register.label.confirm_password": "Passwort bestätigen:",
|
||||
"register.button.submit": "Registrieren",
|
||||
"register.link.back_to_login": "Zurück zum Login",
|
||||
"register.error.no_username": "Bitte Benutzername eingeben.",
|
||||
"register.error.no_password": "Bitte Passwort eingeben.",
|
||||
"register.error.password_mismatch": "Passwörter stimmen nicht überein.",
|
||||
"register.error.password_too_short": "Passwort muss mindestens 4 Zeichen lang sein.",
|
||||
"register.error.username_taken": "Benutzername ist bereits vergeben.",
|
||||
"setup2fa.heading": "Zwei-Faktor-Authentifizierung",
|
||||
"setup2fa.status.enabled": "2FA ist aktiviert.",
|
||||
"setup2fa.button.disable": "2FA deaktivieren",
|
||||
"setup2fa.instruction": "Scanne den folgenden QR-Code mit deiner Authenticator-App (z.B. Google Authenticator, Microsoft Authenticator) oder gib den geheimen Schlüssel manuell ein:",
|
||||
"setup2fa.label.secret_key": "Geheimer Schlüssel:",
|
||||
"setup2fa.label.verification_code": "Code zur Bestätigung eingeben:",
|
||||
"setup2fa.button.enable": "2FA aktivieren",
|
||||
"setup2fa.description": "Richte die Zwei-Faktor-Authentifizierung ein, um dein Konto zusätzlich zu schützen.",
|
||||
"setup2fa.button.setup": "2FA einrichten",
|
||||
"setup2fa.error.invalid_code": "Bitte gib einen gültigen 6-stelligen Code ein.",
|
||||
"setup2fa.error.code_invalid": "Code ungültig. Bitte versuche es erneut.",
|
||||
"setup2fa.error.enable_failed": "Fehler beim Aktivieren von 2FA.",
|
||||
"settings.context.project": "Projekt-Einstellungen für",
|
||||
"settings.context.user": "Allgemeine Benutzer-Einstellungen",
|
||||
"settings.section.user": "Benutzer",
|
||||
"settings.user.label.active": "Aktiver Benutzer:",
|
||||
"settings.user.label.new": "Neuer Benutzer:",
|
||||
"settings.user.placeholder.name": "Name",
|
||||
"settings.user.button.add": "Hinzufügen",
|
||||
"settings.user.role.admin": "Administrator",
|
||||
"settings.user.role.author": "Autor",
|
||||
"settings.user.link.2fa": "2FA einrichten",
|
||||
"settings.section.models": "Modelle",
|
||||
"settings.models.label.rag": "RAG-Modell (Faktenabfrage):",
|
||||
"settings.models.empty": "Keine Modelle gefunden",
|
||||
"settings.models.label.style": "Stil-Modell (Lektorat):",
|
||||
"settings.models.label.spellcheck": "Rechtschreibprüfungs-Modell:",
|
||||
"settings.section.ollama_server": "Ollama Server",
|
||||
"settings.ollama.label.url": "Server-URL:",
|
||||
"settings.ollama.hint.url": "Adresse des Ollama-Servers",
|
||||
"settings.ollama.test": "Test",
|
||||
"settings.ollama.online": "Online",
|
||||
"settings.ollama.offline": "Keine Verbindung",
|
||||
"settings.section.diagnostics": "Ollama Diagnose",
|
||||
"settings.diagnostics.run": "Diagnose ausführen",
|
||||
"settings.section.appearance": "Design",
|
||||
"settings.appearance.label.theme": "Theme:",
|
||||
"settings.appearance.theme.light": "Hell",
|
||||
"settings.appearance.theme.dark": "Dunkel",
|
||||
"settings.appearance.theme.retro": "(Retro)",
|
||||
"settings.section.language": "Sprache",
|
||||
"settings.language.label": "Sprache:",
|
||||
"settings.section.editor_prompt": "Lektor-System-Prompt",
|
||||
"settings.section.factcheck_template": "Fact-Check-Vorlage",
|
||||
"settings.section.spellcheck_prompt": "Rechtschreibprüfungs-Prompt",
|
||||
"settings.section.advocatus_diaboli": "Advocatus Diaboli",
|
||||
"settings.section.hook_finder": "Hook-Finder",
|
||||
"settings.section.eli5": "ELI5-Vereinfachung",
|
||||
"settings.section.coauthor_brainstorming": "Co-Author Brainstorming",
|
||||
"settings.section.tech_lastenheft": "📋 Lastenheft",
|
||||
"settings.section.tech_pflichtenheft": "⚙️ Pflichtenheft",
|
||||
"settings.section.tech_usecase": "🔄 Use Cases",
|
||||
"settings.section.tech_stakeholder": "👥 Stakeholder-Analyse",
|
||||
"settings.section.tech_glossar": "📖 Glossar",
|
||||
"settings.section.tech_qualitaet": "✅ Qualitätsanforderungen",
|
||||
"settings.section.tech_risiko": "⚠️ Risikoanalyse",
|
||||
"settings.button.save": "Speichern",
|
||||
"settings.status.user_created": "Benutzer '{0}' erstellt.",
|
||||
"settings.status.saved": "Einstellungen gespeichert.",
|
||||
"assistant.mode.lektorat": "Lektorat",
|
||||
"assistant.mode.advocatus": "Advocatus Diaboli",
|
||||
"assistant.mode.hookfinder": "Hook-Finder",
|
||||
"assistant.mode.eli5": "ELI5-Vereinfachung",
|
||||
"assistant.button.execute": "Ausführen",
|
||||
"assistant.button.send_to_editor": "An Editor übermitteln",
|
||||
"assistant.button.edit_prompt": "Prompt bearbeiten",
|
||||
"assistant.button.hide_prompt": "Prompt ausblenden",
|
||||
"assistant.button.reset_prompt": "Zurücksetzen",
|
||||
"assistant.label.system_prompt": "System-Prompt",
|
||||
"assistant.status.starting": "Starte Abfrage...",
|
||||
"assistant.status.searching": "Suche Fakten...",
|
||||
"assistant.label.input_text": "Eingabetext",
|
||||
"assistant.placeholder.input": "Füge hier deinen Text ein...",
|
||||
"assistant.label.result": "Ergebnis",
|
||||
"assistant.hint.empty_result": "Ergebnis erscheint hier nach der Ausführung.",
|
||||
"assistant.status.processing": "Verarbeite...",
|
||||
"assistant.mode.coauthor": "Co-Author Brainstorming",
|
||||
"assistant.mode.rag": "RAG-Faktenabfrage",
|
||||
"assistant.mode.tech_lastenheft": "📋 Lastenheft",
|
||||
"assistant.mode.tech_pflichtenheft": "⚙️ Pflichtenheft",
|
||||
"assistant.mode.tech_usecase": "🔄 Use Cases",
|
||||
"assistant.mode.tech_stakeholder": "👥 Stakeholder-Analyse",
|
||||
"assistant.mode.tech_glossar": "📖 Glossar",
|
||||
"assistant.mode.tech_qualitaet": "✅ Qualitätsanforderungen",
|
||||
"assistant.mode.tech_risiko": "⚠️ Risikoanalyse",
|
||||
"assistant.error.no_prompt": "Kein System-Prompt für diesen Modus konfiguriert. Bitte in den Einstellungen hinterlegen.",
|
||||
"assistant.error.no_project": "Kein Projekt geladen. Bitte zuerst ein Projekt im Editor öffnen.",
|
||||
"assistant.error.no_facts": "Keine relevanten Fakten im Projektindex gefunden. Bitte zuerst das Projekt indexieren.",
|
||||
"assistant.status.done": "Fertig.",
|
||||
"assistant.exposee.context": "Berücksichtige folgendes Exposé der Geschichte:",
|
||||
"models.heading": "Ollama Modelle verwalten",
|
||||
"models.button.refresh": "Aktualisieren",
|
||||
"models.status.loading": "Lade Modelle...",
|
||||
"models.label.size": "Größe:",
|
||||
"models.label.modified": "Geändert:",
|
||||
"models.pull.heading": "Neues Modell herunterladen",
|
||||
"models.pull.label.name": "Modellname:",
|
||||
"models.pull.placeholder": "z.B. llama3.2",
|
||||
"models.pull.button.download": "Herunterladen",
|
||||
"models.error.loading": "Fehler beim Laden der Modelle:",
|
||||
"models.pull.status.starting": "Starte Download...",
|
||||
"models.pull.status.model": "Modell",
|
||||
"models.pull.status.success": "erfolgreich heruntergeladen.",
|
||||
"editor.status.autosaved": "Automatisch gespeichert.",
|
||||
"editor.status.corrections_applied": "Korrekturen übernommen.",
|
||||
"editor.spellcheck.button.apply": "Korrekturen übernehmen",
|
||||
"editor.version.button.browse": "Versionen",
|
||||
"editor.version.button.diff": "Vergleichen",
|
||||
"editor.version.button.restore": "Wiederherstellen",
|
||||
"editor.version.list_title": "Vorhandene Versionen",
|
||||
"editor.version.diff_title": "Diff: Version vom",
|
||||
"editor.version.empty": "Keine Versionen vorhanden.",
|
||||
"editor.placeholder.content": "Schreibe hier deinen Text...",
|
||||
"editor.button.style_check": "Stil prüfen",
|
||||
"editor.button.spell_check": "Rechtschreibung prüfen",
|
||||
"editor.button.save": "Speichern",
|
||||
"editor.button.version": "Version",
|
||||
"editor.button.assistant": "Assistent",
|
||||
"editor.button.index_project": "Projekt indexieren",
|
||||
"editor.version.title": "Version speichern",
|
||||
"editor.version.label.comment": "Kommentar (optional)",
|
||||
"editor.version.placeholder.comment": "Was hat sich geändert?",
|
||||
"editor.version.button.save": "Version speichern",
|
||||
"editor.search.placeholder": "Suchbegriff für Fakten-/Wikipedia-Recherche...",
|
||||
"editor.button.search_rag": "Fakten suchen (RAG)",
|
||||
"editor.button.search_wikipedia": "Wikipedia-Recherche",
|
||||
"editor.spellcheck.heading": "Rechtschreibprüfung - Änderungen:",
|
||||
"editor.rag.heading": "Gefundene Fakten (RAG):",
|
||||
"editor.rag.label.similarity": "Ähnlichkeit:",
|
||||
"editor.wikipedia.heading": "Wikipedia-Recherche:",
|
||||
"editor.wikipedia.status.loaded": "✓ Geladen",
|
||||
"editor.wikipedia.button.load": "Vollständigen Artikel laden",
|
||||
"editor.wikipedia.button.save_as_fact": "Als Fakt speichern",
|
||||
"editor.result.heading": "Ergebnis:",
|
||||
"editor.create_project.title": "Neues Projekt erstellen",
|
||||
"editor.create_project.label.name": "Projektname",
|
||||
"editor.create_project.placeholder.name": "z.B. Mein Roman",
|
||||
"editor.create_project.label.path": "Projektpfad",
|
||||
"editor.create_project.placeholder.path": "z.B. C:\\Projekte\\MeinRoman",
|
||||
"editor.button.create": "Erstellen",
|
||||
"editor.status.imported_from_assistant": "Text aus Assistent übernommen.",
|
||||
"editor.status.loaded": "Geladen:",
|
||||
"editor.warning.context": "Datei ist sehr lang – Kontextfenster könnte überschritten werden",
|
||||
"settings.temperature.label": "Kreativität (Temperatur):",
|
||||
"settings.temperature.category.precise": "Der Buchhalter (Präzise & Streng)",
|
||||
"settings.temperature.category.balanced": "Der Lektor (Ausgewogen)",
|
||||
"settings.temperature.category.creative": "Der Co-Autor (Kreativ)",
|
||||
"settings.temperature.category.chaos": "Der Betrunkene (Chaos)",
|
||||
"settings.temperature.desc.precise": "Deterministisch, wiederholbar, logisch, trocken",
|
||||
"settings.temperature.desc.balanced": "Natürlicher Satzbau, bleibt faktennah",
|
||||
"settings.temperature.desc.creative": "Unerwartete Metaphern, Wendungen, kreative Ideen",
|
||||
"settings.temperature.desc.chaos": "Unbrauchbar für produktives Schreiben",
|
||||
"editor.status.analyzing_style": "Ollama analysiert Stil...",
|
||||
"editor.status.checking_spelling": "Ollama prüft Rechtschreibung...",
|
||||
"editor.status.searching_rag": "Suche verwandte Fakten...",
|
||||
"editor.status.searching_wikipedia": "Suche auf Wikipedia...",
|
||||
"editor.status.loading_article": "Lade Artikel:",
|
||||
"editor.status.saving_fact": "Speichere als Fakt...",
|
||||
"editor.status.creating_project": "Erstelle Projekt...",
|
||||
"editor.status.indexing": "Indexiere Projekt...",
|
||||
"editor.status.saved": "Gespeichert:",
|
||||
"editor.status.version_saved": "Version gespeichert:",
|
||||
"editor.status.saved_as": "Gespeichert als:",
|
||||
"editor.rag.error.no_project": "Kein Projekt geladen. Öffne zuerst ein Projekt über die Sidebar.",
|
||||
"editor.rag.error.no_facts": "Keine relevanten Fakten gefunden. Indexiere das Projekt ggf. über 'Projekt indexieren'.",
|
||||
"editor.rag.error.generic": "RAG-Fehler:",
|
||||
"editor.wikipedia.error.not_found": "Keine Wikipedia-Artikel gefunden.",
|
||||
"editor.wikipedia.error.generic": "Wikipedia-Fehler:",
|
||||
"editor.wikipedia.error.load_failed": "Konnte Artikel nicht laden.",
|
||||
"editor.error.project_not_found": "Projekt nicht gefunden.",
|
||||
"editor.error.save_failed": "Fehler beim Speichern:",
|
||||
"editor.error.create_failed": "Fehler beim Erstellen:",
|
||||
"editor.index.error.no_project": "Kein Projekt geladen. Öffne zuerst ein Projekt über die Sidebar.",
|
||||
"editor.index.error.not_found": "Projekt nicht gefunden.",
|
||||
"editor.index.status.success": "Projekt erfolgreich indexiert.",
|
||||
"editor.index.error.generic": "Indexierungsfehler:",
|
||||
"editor.version.status_loaded_prefix": "Version vom",
|
||||
"editor.version.status_loaded_suffix": "geladen.",
|
||||
"home.tab.rag_facts": "Gefundene Fakten (RAG)",
|
||||
"home.tab.style_analysis": "Stil-Analyse",
|
||||
"home.rag.empty": "Keine Fakten gefunden. Öffne ein Projekt und scanne die Dateien.",
|
||||
"error.heading": "Ein Fehler ist aufgetreten",
|
||||
"error.message": "Entschuldigung, es ist ein unerwarteter Fehler aufgetreten.",
|
||||
"error.link.back_to_home": "Zurück zur Startseite",
|
||||
"notfound.heading": "404 - Seite nicht gefunden",
|
||||
"notfound.message": "Die angeforderte Seite existiert nicht.",
|
||||
"notfound.link.back_to_home": "Zurück zur Startseite",
|
||||
"usermanagement.title": "Benutzerverwaltung",
|
||||
"usermanagement.col.name": "Name",
|
||||
"usermanagement.col.role": "Rolle",
|
||||
"usermanagement.col.status": "Status",
|
||||
"usermanagement.col.actions": "Aktionen",
|
||||
"usermanagement.status.active": "Aktiv",
|
||||
"usermanagement.status.inactive": "Deaktiviert",
|
||||
"usermanagement.action.activate": "Freischalten",
|
||||
"usermanagement.action.deactivate": "Deaktivieren",
|
||||
"usermanagement.action.delete": "Löschen",
|
||||
"usermanagement.cannot_self": "Kann sich nicht selbst verwalten",
|
||||
"usermanagement.status.activated": "Benutzer freigeschaltet.",
|
||||
"usermanagement.status.deactivated": "Benutzer deaktiviert.",
|
||||
"usermanagement.status.deleted": "Benutzer gelöscht.",
|
||||
"common.back": "Zurück",
|
||||
"export.title": "Dokumente exportieren",
|
||||
"export.file_count": "Ausgewählte Dateien",
|
||||
"export.label.filename": "Dateiname",
|
||||
"export.placeholder.filename": "z.B. MeinExport",
|
||||
"export.hint.filename": "Die Datei wird im Browser-Download-Ordner gespeichert.",
|
||||
"export.label.format": "Format",
|
||||
"export.format.markdown": "Markdown (Einzeldatei)",
|
||||
"export.format.zip": "Markdown (ZIP-Archiv)",
|
||||
"export.button.export": "Exportieren"
|
||||
}
|
||||
|
|
@ -1,293 +0,0 @@
|
|||
{
|
||||
"common.status.ready": "Ready",
|
||||
"common.cancel": "Cancel",
|
||||
"common.error": "Error:",
|
||||
"common.copy": "Copy",
|
||||
"common.status.error": "Error",
|
||||
"page.title.login": "Login - Author Buddy",
|
||||
"page.title.register": "Register - Author Buddy",
|
||||
"page.title.setup2fa": "2FA setup - Author Buddy",
|
||||
"page.title.settings": "Settings",
|
||||
"page.title.editor": "Editor - Author Buddy",
|
||||
"page.title.assistant": "Assistant - Author Buddy",
|
||||
"page.title.models": "Models - Author Buddy",
|
||||
"page.title.home": "Dashboard - Author Buddy",
|
||||
"page.title.error": "Error",
|
||||
"page.title.not_found": "Page not found",
|
||||
"page.title.help": "Help",
|
||||
"nav.menu.extras.help": "Help",
|
||||
"nav.menu.file": "File",
|
||||
"nav.menu.file.new_project": "New project",
|
||||
"nav.menu.file.open_editor": "Open editor",
|
||||
"nav.menu.file.exit": "Exit",
|
||||
"nav.menu.extras": "Extras",
|
||||
"nav.menu.extras.settings": "Settings",
|
||||
"nav.menu.extras.manage_models": "Manage models",
|
||||
"nav.menu.extras.assistant": "Assistant",
|
||||
"nav.menu.extras.user_management": "User management",
|
||||
"nav.menu.projects": "Projects",
|
||||
"nav.projects.list_title": "List existing projects",
|
||||
"nav.projects.loading": "Loading projects...",
|
||||
"nav.projects.empty": "No projects available.",
|
||||
"nav.projects.create": "Create project",
|
||||
"nav.projects.files": "Project files",
|
||||
"nav.projects.refresh_tree": "Refresh file tree",
|
||||
"nav.projects.file.new": "New file",
|
||||
"nav.projects.file.rename": "Rename",
|
||||
"nav.rename.title": "Rename file",
|
||||
"nav.rename.label.new_name": "New name",
|
||||
"nav.rename.placeholder": "new_name.md",
|
||||
"nav.rename.button.confirm": "Rename",
|
||||
"nav.newfile.title": "New file",
|
||||
"nav.newfile.label.filename": "Filename",
|
||||
"nav.newfile.placeholder": "my_file.md",
|
||||
"nav.newfile.button.create": "Create",
|
||||
"nav.user.logged_in_as": "Logged in as:",
|
||||
"nav.user.manage_2fa": "Manage 2FA",
|
||||
"nav.user.logout": "Logout",
|
||||
"nav.error.file_exists": "File already exists.",
|
||||
"nav.projects.copy": "Copy project",
|
||||
"nav.copy_project.title": "Copy project",
|
||||
"nav.copy_project.label.name": "New project name:",
|
||||
"nav.copy_project.placeholder.name": "e.g. My novel (Copy)",
|
||||
"nav.copy_project.label.path": "Destination path:",
|
||||
"nav.copy_project.placeholder.path": "e.g. C:\\Projects\\MyNovelCopy",
|
||||
"nav.copy_project.button.copy": "Copy",
|
||||
"nav.status.project_copied": "Project copied:",
|
||||
"nav.status.renamed_to": "Renamed to:",
|
||||
"nav.status.created": "Created:",
|
||||
"login.heading": "Login",
|
||||
"login.label.username": "Username:",
|
||||
"login.label.password": "Password:",
|
||||
"login.button.submit": "Login",
|
||||
"login.link.register": "Register",
|
||||
"login.2fa.instruction": "Enter the 6-digit code from your authenticator app:",
|
||||
"login.2fa.label.code": "2FA code:",
|
||||
"login.2fa.button.verify": "Verify",
|
||||
"login.error.empty_fields": "Please enter username and password.",
|
||||
"login.error.invalid_credentials": "Invalid username or password.",
|
||||
"login.2fa.error.invalid_code": "Please enter a valid 6-digit code.",
|
||||
"login.2fa.error.code_failed": "Invalid code. Please try again.",
|
||||
"register.heading": "Register",
|
||||
"register.label.username": "Username:",
|
||||
"register.label.password": "Password:",
|
||||
"register.label.confirm_password": "Confirm password:",
|
||||
"register.button.submit": "Register",
|
||||
"register.link.back_to_login": "Back to login",
|
||||
"register.error.no_username": "Please enter a username.",
|
||||
"register.error.no_password": "Please enter a password.",
|
||||
"register.error.password_mismatch": "Passwords do not match.",
|
||||
"register.error.password_too_short": "Password must be at least 4 characters long.",
|
||||
"register.error.username_taken": "Username is already taken.",
|
||||
"setup2fa.heading": "Two-Factor Authentication",
|
||||
"setup2fa.status.enabled": "2FA is enabled.",
|
||||
"setup2fa.button.disable": "Disable 2FA",
|
||||
"setup2fa.instruction": "Scan the QR code below with your authenticator app (e.g. Google Authenticator, Microsoft Authenticator) or enter the secret key manually:",
|
||||
"setup2fa.label.secret_key": "Secret key:",
|
||||
"setup2fa.label.verification_code": "Enter verification code:",
|
||||
"setup2fa.button.enable": "Enable 2FA",
|
||||
"setup2fa.description": "Set up two-factor authentication to add an extra layer of security to your account.",
|
||||
"setup2fa.button.setup": "Set up 2FA",
|
||||
"setup2fa.error.invalid_code": "Please enter a valid 6-digit code.",
|
||||
"setup2fa.error.code_invalid": "Invalid code. Please try again.",
|
||||
"setup2fa.error.enable_failed": "Failed to enable 2FA.",
|
||||
"settings.context.project": "Project settings for",
|
||||
"settings.context.user": "General user settings",
|
||||
"settings.section.user": "User",
|
||||
"settings.user.label.active": "Active user:",
|
||||
"settings.user.label.new": "New user:",
|
||||
"settings.user.placeholder.name": "Name",
|
||||
"settings.user.button.add": "Add",
|
||||
"settings.user.role.admin": "Administrator",
|
||||
"settings.user.role.author": "Author",
|
||||
"settings.user.link.2fa": "Set up 2FA",
|
||||
"settings.section.models": "Models",
|
||||
"settings.models.label.rag": "RAG model (facts):",
|
||||
"settings.models.empty": "No models found",
|
||||
"settings.models.label.style": "Style model (editing):",
|
||||
"settings.models.label.spellcheck": "Spell check model:",
|
||||
"settings.section.ollama_server": "Ollama Server",
|
||||
"settings.ollama.label.url": "Server URL:",
|
||||
"settings.ollama.hint.url": "Ollama server address",
|
||||
"settings.ollama.test": "Test",
|
||||
"settings.ollama.online": "Online",
|
||||
"settings.ollama.offline": "No connection",
|
||||
"settings.section.diagnostics": "Ollama Diagnostics",
|
||||
"settings.diagnostics.run": "Run diagnostics",
|
||||
"settings.section.appearance": "Appearance",
|
||||
"settings.appearance.label.theme": "Theme:",
|
||||
"settings.appearance.theme.light": "Light",
|
||||
"settings.appearance.theme.dark": "Dark",
|
||||
"settings.appearance.theme.retro": "(Retro)",
|
||||
"settings.section.language": "Language",
|
||||
"settings.language.label": "Language:",
|
||||
"settings.section.editor_prompt": "Editor system prompt",
|
||||
"settings.section.factcheck_template": "Fact check template",
|
||||
"settings.section.spellcheck_prompt": "Spell check prompt",
|
||||
"settings.section.advocatus_diaboli": "Advocatus Diaboli",
|
||||
"settings.section.hook_finder": "Hook finder",
|
||||
"settings.section.eli5": "ELI5-Simplification",
|
||||
"settings.section.coauthor_brainstorming": "Co-Author Brainstorming",
|
||||
"settings.section.tech_lastenheft": "📋 Requirements Spec",
|
||||
"settings.section.tech_pflichtenheft": "⚙️ Functional Spec",
|
||||
"settings.section.tech_usecase": "🔄 Use Cases",
|
||||
"settings.section.tech_stakeholder": "👥 Stakeholder Analysis",
|
||||
"settings.section.tech_glossar": "📖 Glossary",
|
||||
"settings.section.tech_qualitaet": "✅ Quality Requirements",
|
||||
"settings.section.tech_risiko": "⚠️ Risk Analysis",
|
||||
"settings.button.save": "Save",
|
||||
"settings.status.user_created": "User '{0}' created.",
|
||||
"settings.status.saved": "Settings saved.",
|
||||
"assistant.mode.lektorat": "Editing",
|
||||
"assistant.mode.advocatus": "Advocatus Diaboli",
|
||||
"assistant.mode.hookfinder": "Hook finder",
|
||||
"assistant.mode.eli5": "ELI5 simplification",
|
||||
"assistant.button.execute": "Execute",
|
||||
"assistant.button.send_to_editor": "Send to editor",
|
||||
"assistant.button.edit_prompt": "Edit prompt",
|
||||
"assistant.button.hide_prompt": "Hide prompt",
|
||||
"assistant.button.reset_prompt": "Reset",
|
||||
"assistant.label.system_prompt": "System prompt",
|
||||
"assistant.status.starting": "Starting...",
|
||||
"assistant.status.searching": "Searching facts...",
|
||||
"assistant.label.input_text": "Input text",
|
||||
"assistant.placeholder.input": "Paste your text here...",
|
||||
"assistant.label.result": "Result",
|
||||
"assistant.hint.empty_result": "Result appears here after execution.",
|
||||
"assistant.status.processing": "Processing...",
|
||||
"assistant.mode.coauthor": "Co-Author Brainstorming",
|
||||
"assistant.mode.rag": "RAG fact query",
|
||||
"assistant.mode.tech_lastenheft": "📋 Requirements Spec",
|
||||
"assistant.mode.tech_pflichtenheft": "⚙️ Functional Spec",
|
||||
"assistant.mode.tech_usecase": "🔄 Use Cases",
|
||||
"assistant.mode.tech_stakeholder": "👥 Stakeholder Analysis",
|
||||
"assistant.mode.tech_glossar": "📖 Glossary",
|
||||
"assistant.mode.tech_qualitaet": "✅ Quality Requirements",
|
||||
"assistant.mode.tech_risiko": "⚠️ Risk Analysis",
|
||||
"assistant.error.no_prompt": "No system prompt configured for this mode. Please configure in settings.",
|
||||
"assistant.error.no_project": "No project loaded. Please open a project in the editor first.",
|
||||
"assistant.error.no_facts": "No relevant facts found in the project index. Please index the project first.",
|
||||
"assistant.status.done": "Done.",
|
||||
"assistant.exposee.context": "Consider the following story outline (Exposé):",
|
||||
"models.heading": "Manage Ollama models",
|
||||
"models.button.refresh": "Refresh",
|
||||
"models.status.loading": "Loading models...",
|
||||
"models.label.size": "Size:",
|
||||
"models.label.modified": "Modified:",
|
||||
"models.pull.heading": "Download new model",
|
||||
"models.pull.label.name": "Model name:",
|
||||
"models.pull.placeholder": "e.g. llama3.2",
|
||||
"models.pull.button.download": "Download",
|
||||
"models.error.loading": "Error loading models:",
|
||||
"models.pull.status.starting": "Starting download...",
|
||||
"models.pull.status.model": "Model",
|
||||
"models.pull.status.success": "downloaded successfully.",
|
||||
"editor.status.autosaved": "Auto-saved.",
|
||||
"editor.status.corrections_applied": "Corrections applied.",
|
||||
"editor.spellcheck.button.apply": "Apply corrections",
|
||||
"editor.version.button.browse": "Versions",
|
||||
"editor.version.button.diff": "Compare",
|
||||
"editor.version.button.restore": "Restore",
|
||||
"editor.version.list_title": "Available versions",
|
||||
"editor.version.diff_title": "Diff: version from",
|
||||
"editor.version.empty": "No versions available.",
|
||||
"editor.placeholder.content": "Write your text here...",
|
||||
"editor.button.style_check": "Check style",
|
||||
"editor.button.spell_check": "Spell check",
|
||||
"editor.button.save": "Save",
|
||||
"editor.button.version": "Version",
|
||||
"editor.button.assistant": "Assistant",
|
||||
"editor.button.index_project": "Index project",
|
||||
"editor.version.title": "Save version",
|
||||
"editor.version.label.comment": "Comment (optional)",
|
||||
"editor.version.placeholder.comment": "What changed?",
|
||||
"editor.version.button.save": "Save version",
|
||||
"editor.search.placeholder": "Search term for fact/Wikipedia research...",
|
||||
"editor.button.search_rag": "Search facts (RAG)",
|
||||
"editor.button.search_wikipedia": "Wikipedia research",
|
||||
"editor.spellcheck.heading": "Spell check - Changes:",
|
||||
"editor.rag.heading": "Found facts (RAG):",
|
||||
"editor.rag.label.similarity": "Similarity:",
|
||||
"editor.wikipedia.heading": "Wikipedia research:",
|
||||
"editor.wikipedia.status.loaded": "\u2713 Loaded",
|
||||
"editor.wikipedia.button.load": "Load full article",
|
||||
"editor.wikipedia.button.save_as_fact": "Save as fact",
|
||||
"editor.result.heading": "Result:",
|
||||
"editor.create_project.title": "Create new project",
|
||||
"editor.create_project.label.name": "Project name",
|
||||
"editor.create_project.placeholder.name": "e.g. My novel",
|
||||
"editor.create_project.label.path": "Project path",
|
||||
"editor.create_project.placeholder.path": "e.g. C:\\Projects\\MyNovel",
|
||||
"editor.button.create": "Create",
|
||||
"editor.status.imported_from_assistant": "Text imported from assistant.",
|
||||
"editor.status.loaded": "Loaded:",
|
||||
"editor.warning.context": "File is very long – context window may be exceeded",
|
||||
"settings.temperature.label": "Creativity (Temperature):",
|
||||
"settings.temperature.category.precise": "The Accountant (Precise & Strict)",
|
||||
"settings.temperature.category.balanced": "The Editor (Balanced)",
|
||||
"settings.temperature.category.creative": "The Co-Author (Creative)",
|
||||
"settings.temperature.category.chaos": "The Drunk (Chaos)",
|
||||
"settings.temperature.desc.precise": "Deterministic, repeatable, logical, dry",
|
||||
"settings.temperature.desc.balanced": "Natural phrasing, stays close to facts",
|
||||
"settings.temperature.desc.creative": "Unexpected metaphors, twists, creative ideas",
|
||||
"settings.temperature.desc.chaos": "Unusable for productive writing",
|
||||
"editor.status.analyzing_style": "Ollama analyzing style...",
|
||||
"editor.status.checking_spelling": "Ollama checking spelling...",
|
||||
"editor.status.searching_rag": "Searching related facts...",
|
||||
"editor.status.searching_wikipedia": "Searching Wikipedia...",
|
||||
"editor.status.loading_article": "Loading article:",
|
||||
"editor.status.saving_fact": "Saving as fact...",
|
||||
"editor.status.creating_project": "Creating project...",
|
||||
"editor.status.indexing": "Indexing project...",
|
||||
"editor.status.saved": "Saved:",
|
||||
"editor.status.version_saved": "Version saved:",
|
||||
"editor.status.saved_as": "Saved as:",
|
||||
"editor.rag.error.no_project": "No project loaded. Open a project via the sidebar first.",
|
||||
"editor.rag.error.no_facts": "No relevant facts found. Index the project if needed via 'Index project'.",
|
||||
"editor.rag.error.generic": "RAG error:",
|
||||
"editor.wikipedia.error.not_found": "No Wikipedia articles found.",
|
||||
"editor.wikipedia.error.generic": "Wikipedia error:",
|
||||
"editor.wikipedia.error.load_failed": "Could not load article.",
|
||||
"editor.error.project_not_found": "Project not found.",
|
||||
"editor.error.save_failed": "Error saving:",
|
||||
"editor.error.create_failed": "Error creating:",
|
||||
"editor.index.error.no_project": "No project loaded. Open a project via the sidebar first.",
|
||||
"editor.index.error.not_found": "Project not found.",
|
||||
"editor.index.status.success": "Project indexed successfully.",
|
||||
"editor.index.error.generic": "Indexing error:",
|
||||
"editor.version.status_loaded_prefix": "Version from",
|
||||
"editor.version.status_loaded_suffix": "loaded.",
|
||||
"home.tab.rag_facts": "Found facts (RAG)",
|
||||
"home.tab.style_analysis": "Style analysis",
|
||||
"home.rag.empty": "No facts found. Open a project and scan the files.",
|
||||
"error.heading": "An error occurred",
|
||||
"error.message": "Sorry, an unexpected error occurred.",
|
||||
"error.link.back_to_home": "Back to home",
|
||||
"notfound.heading": "404 - Page not found",
|
||||
"notfound.message": "The requested page does not exist.",
|
||||
"notfound.link.back_to_home": "Back to home",
|
||||
"usermanagement.title": "User management",
|
||||
"usermanagement.col.name": "Name",
|
||||
"usermanagement.col.role": "Role",
|
||||
"usermanagement.col.status": "Status",
|
||||
"usermanagement.col.actions": "Actions",
|
||||
"usermanagement.status.active": "Active",
|
||||
"usermanagement.status.inactive": "Disabled",
|
||||
"usermanagement.action.activate": "Activate",
|
||||
"usermanagement.action.deactivate": "Deactivate",
|
||||
"usermanagement.action.delete": "Delete",
|
||||
"usermanagement.cannot_self": "Cannot manage yourself",
|
||||
"usermanagement.status.activated": "User activated.",
|
||||
"usermanagement.status.deactivated": "User deactivated.",
|
||||
"usermanagement.status.deleted": "User deleted.",
|
||||
"common.back": "Back",
|
||||
"export.title": "Export documents",
|
||||
"export.file_count": "Selected files",
|
||||
"export.label.filename": "Filename",
|
||||
"export.placeholder.filename": "e.g. MyExport",
|
||||
"export.hint.filename": "The file will be saved in the browser download folder.",
|
||||
"export.label.format": "Format",
|
||||
"export.format.markdown": "Markdown (single file)",
|
||||
"export.format.zip": "Markdown (ZIP archive)",
|
||||
"export.button.export": "Export"
|
||||
}
|
||||
|
|
@ -1,210 +0,0 @@
|
|||
using AuthorBuddy.Web.Data;
|
||||
using LiteDB;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public class AuthService : IAuthService
|
||||
{
|
||||
private readonly ILiteDatabase _db;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ILocalizationService _loc;
|
||||
private UserEntity? _currentUser;
|
||||
private string? _pending2FASecret;
|
||||
private bool _requires2FA;
|
||||
|
||||
public AuthService(ILiteDatabase db, ISettingsService settingsService, ILocalizationService loc)
|
||||
{
|
||||
_db = db;
|
||||
_settingsService = settingsService;
|
||||
_loc = loc;
|
||||
}
|
||||
|
||||
public bool IsLoggedIn => _currentUser != null;
|
||||
public bool Is2FARequired => _requires2FA;
|
||||
public UserEntity? CurrentUser => _currentUser;
|
||||
|
||||
public async Task<bool> LoginAsync(string username, string password)
|
||||
{
|
||||
var col = _db.GetCollection<UserEntity>("users");
|
||||
var user = col.FindOne(u => u.Name == username);
|
||||
|
||||
if (user == null || string.IsNullOrEmpty(user.PasswordHash))
|
||||
return false;
|
||||
|
||||
if (!user.IsActive)
|
||||
return false;
|
||||
|
||||
if (!TotpHelper.VerifyPassword(password, user.PasswordHash))
|
||||
return false;
|
||||
|
||||
if (user.TwoFactorEnabled)
|
||||
{
|
||||
_pending2FASecret = user.TwoFactorSecret;
|
||||
_requires2FA = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
await SetAuthenticatedUser(user);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task LogoutAsync()
|
||||
{
|
||||
_currentUser = null;
|
||||
_requires2FA = false;
|
||||
_pending2FASecret = null;
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task AdminBypassLoginAsync(int userId)
|
||||
{
|
||||
var col = _db.GetCollection<UserEntity>("users");
|
||||
var user = col.FindById(userId);
|
||||
if (user != null)
|
||||
await SetAuthenticatedUser(user);
|
||||
}
|
||||
|
||||
public async Task<bool> RegisterAsync(string username, string password)
|
||||
{
|
||||
var col = _db.GetCollection<UserEntity>("users");
|
||||
var existing = col.FindOne(u => u.Name == username);
|
||||
if (existing != null) return false;
|
||||
|
||||
var hasUsers = col.FindAll().Any();
|
||||
var user = new UserEntity
|
||||
{
|
||||
Name = username,
|
||||
PasswordHash = TotpHelper.HashPassword(password),
|
||||
TwoFactorEnabled = false,
|
||||
TwoFactorSecret = string.Empty,
|
||||
Role = hasUsers ? UserRole.Author : UserRole.Administrator
|
||||
};
|
||||
col.Insert(user);
|
||||
await SetAuthenticatedUser(user);
|
||||
return true;
|
||||
}
|
||||
|
||||
public Task<UserEntity?> GetCurrentUserAsync()
|
||||
{
|
||||
return Task.FromResult(_currentUser);
|
||||
}
|
||||
|
||||
public async Task<bool> Verify2FAAsync(string code)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_pending2FASecret)) return false;
|
||||
|
||||
if (!TotpHelper.VerifyCode(_pending2FASecret, code))
|
||||
return false;
|
||||
|
||||
var col = _db.GetCollection<UserEntity>("users");
|
||||
var user = col.FindOne(u => u.TwoFactorSecret == _pending2FASecret);
|
||||
if (user == null) return false;
|
||||
|
||||
await SetAuthenticatedUser(user);
|
||||
return true;
|
||||
}
|
||||
|
||||
public Task<string> Generate2FASecretAsync()
|
||||
{
|
||||
return Task.FromResult(TotpHelper.GenerateSecret());
|
||||
}
|
||||
|
||||
public string Get2FAProvisioningUri(string secret, string username)
|
||||
{
|
||||
return TotpHelper.GetProvisioningUri(secret, username);
|
||||
}
|
||||
|
||||
public async Task<bool> Enable2FAAsync(string secret)
|
||||
{
|
||||
if (_currentUser == null) return false;
|
||||
|
||||
var col = _db.GetCollection<UserEntity>("users");
|
||||
var user = col.FindById(_currentUser.Id);
|
||||
if (user == null) return false;
|
||||
|
||||
user.TwoFactorSecret = secret;
|
||||
user.TwoFactorEnabled = true;
|
||||
col.Update(user);
|
||||
_currentUser.TwoFactorSecret = secret;
|
||||
_currentUser.TwoFactorEnabled = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> Disable2FAAsync()
|
||||
{
|
||||
if (_currentUser == null) return false;
|
||||
|
||||
var col = _db.GetCollection<UserEntity>("users");
|
||||
var user = col.FindById(_currentUser.Id);
|
||||
if (user == null) return false;
|
||||
|
||||
user.TwoFactorSecret = string.Empty;
|
||||
user.TwoFactorEnabled = false;
|
||||
col.Update(user);
|
||||
_currentUser.TwoFactorSecret = string.Empty;
|
||||
_currentUser.TwoFactorEnabled = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> Is2FAEnabledAsync()
|
||||
{
|
||||
if (_currentUser == null) return false;
|
||||
var col = _db.GetCollection<UserEntity>("users");
|
||||
var user = col.FindById(_currentUser.Id);
|
||||
return user?.TwoFactorEnabled ?? false;
|
||||
}
|
||||
|
||||
public async Task<string?> GetCurrent2FASecretAsync()
|
||||
{
|
||||
if (_currentUser == null) return null;
|
||||
var col = _db.GetCollection<UserEntity>("users");
|
||||
var user = col.FindById(_currentUser.Id);
|
||||
return user?.TwoFactorSecret;
|
||||
}
|
||||
|
||||
private async Task SetAuthenticatedUser(UserEntity user)
|
||||
{
|
||||
_currentUser = user;
|
||||
_requires2FA = false;
|
||||
_pending2FASecret = null;
|
||||
_settingsService.CurrentProjectId = null;
|
||||
_settingsService.CurrentFilePath = null;
|
||||
_settingsService.SetCurrentUser(user.Id);
|
||||
|
||||
await _settingsService.InitializeAsync();
|
||||
await _loc.InitAsync();
|
||||
}
|
||||
|
||||
public Task<List<UserEntity>> GetAllUsersAsync()
|
||||
{
|
||||
var col = _db.GetCollection<UserEntity>("users");
|
||||
return Task.FromResult(col.FindAll().OrderBy(u => u.Name).ToList());
|
||||
}
|
||||
|
||||
public async Task<bool> SetUserActiveAsync(int userId, bool active)
|
||||
{
|
||||
if (_currentUser != null && userId == _currentUser.Id)
|
||||
return false;
|
||||
|
||||
var col = _db.GetCollection<UserEntity>("users");
|
||||
var user = col.FindById(userId);
|
||||
if (user == null) return false;
|
||||
|
||||
user.IsActive = active;
|
||||
col.Update(user);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteUserAsync(int userId)
|
||||
{
|
||||
if (_currentUser != null && userId == _currentUser.Id)
|
||||
return false;
|
||||
|
||||
var col = _db.GetCollection<UserEntity>("users");
|
||||
var user = col.FindById(userId);
|
||||
if (user == null) return false;
|
||||
|
||||
col.Delete(userId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public class CustomAuthStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
private readonly IAuthService _authService;
|
||||
private AuthenticationState _anonymous;
|
||||
|
||||
public CustomAuthStateProvider(IAuthService authService)
|
||||
{
|
||||
_authService = authService;
|
||||
_anonymous = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
||||
}
|
||||
|
||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
{
|
||||
var user = await _authService.GetCurrentUserAsync();
|
||||
if (user != null)
|
||||
{
|
||||
var identity = new ClaimsIdentity(new[]
|
||||
{
|
||||
new Claim(ClaimTypes.Name, user.Name),
|
||||
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||||
}, "custom");
|
||||
return new AuthenticationState(new ClaimsPrincipal(identity));
|
||||
}
|
||||
return _anonymous;
|
||||
}
|
||||
|
||||
public void NotifyStateChanged()
|
||||
{
|
||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
using LiteDB;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public class DbAdminService : IDbAdminService
|
||||
{
|
||||
private readonly ILiteDatabase _db;
|
||||
|
||||
private static readonly string[] KnownCollections =
|
||||
{
|
||||
"users", "projects", "settings", "fileCategories", "documentMeta", "vectors"
|
||||
};
|
||||
|
||||
public DbAdminService(ILiteDatabase db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public List<DbCollectionInfo> GetCollections()
|
||||
{
|
||||
var result = new List<DbCollectionInfo>();
|
||||
foreach (var name in KnownCollections)
|
||||
{
|
||||
var col = _db.GetCollection(name);
|
||||
result.Add(new DbCollectionInfo
|
||||
{
|
||||
Name = name,
|
||||
Count = col.Count()
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<DbRecord> GetAll(string collectionName)
|
||||
{
|
||||
var col = _db.GetCollection(collectionName);
|
||||
var docs = col.FindAll().ToList();
|
||||
var result = new List<DbRecord>();
|
||||
|
||||
foreach (var doc in docs)
|
||||
{
|
||||
var id = doc["_id"].AsInt32;
|
||||
var fields = new Dictionary<string, string?>();
|
||||
foreach (var key in doc.Keys)
|
||||
{
|
||||
if (key == "_id") continue;
|
||||
fields[key] = BsonToDisplay(doc[key]);
|
||||
}
|
||||
result.Add(new DbRecord { Id = id, Fields = fields });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool Update(string collectionName, int id, Dictionary<string, string?> values)
|
||||
{
|
||||
try
|
||||
{
|
||||
var col = _db.GetCollection(collectionName);
|
||||
var doc = col.FindById(new BsonValue(id));
|
||||
if (doc == null) return false;
|
||||
|
||||
foreach (var kv in values)
|
||||
{
|
||||
if (kv.Key == "_id") continue;
|
||||
doc[kv.Key] = ParseBsonValue(kv.Value);
|
||||
}
|
||||
return col.Update(doc);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Delete(string collectionName, int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var col = _db.GetCollection(collectionName);
|
||||
return col.Delete(new BsonValue(id));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? BsonToDisplay(BsonValue val)
|
||||
{
|
||||
if (val.IsNull) return null;
|
||||
if (val.IsDateTime) return val.AsDateTime.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
if (val.IsBoolean) return val.AsBoolean.ToString();
|
||||
if (val.IsDouble) return val.AsDouble.ToString("G");
|
||||
if (val.IsInt32) return val.AsInt32.ToString();
|
||||
if (val.IsInt64) return val.AsInt64.ToString();
|
||||
if (val.IsString) return val.AsString;
|
||||
if (val.IsArray) return $"[Array ({val.AsArray.Count} items)]";
|
||||
if (val.IsDocument) return $"[Object]";
|
||||
return val.RawValue?.ToString();
|
||||
}
|
||||
|
||||
private static BsonValue ParseBsonValue(string? value)
|
||||
{
|
||||
if (value == null) return BsonValue.Null;
|
||||
if (int.TryParse(value, out var i)) return new BsonValue(i);
|
||||
if (long.TryParse(value, out var l)) return new BsonValue(l);
|
||||
if (double.TryParse(value, out var d)) return new BsonValue(d);
|
||||
if (bool.TryParse(value, out var b)) return new BsonValue(b);
|
||||
return new BsonValue(value);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,425 +0,0 @@
|
|||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using AuthorBuddy.Web.Models;
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Wordprocessing;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public partial class ExportService : IExportService
|
||||
{
|
||||
private readonly IProjectService _projectService;
|
||||
|
||||
public ExportService(IProjectService projectService)
|
||||
{
|
||||
_projectService = projectService;
|
||||
QuestPDF.Settings.License = LicenseType.Community;
|
||||
}
|
||||
|
||||
public async Task<byte[]> ExportAsync(int projectId, List<string> filePaths, ExportFormat format)
|
||||
{
|
||||
var project = await _projectService.GetProjectAsync(projectId);
|
||||
var projectName = project?.Name ?? "Projekt";
|
||||
|
||||
var files = new List<(string name, string content)>();
|
||||
foreach (var path in filePaths)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
var content = await File.ReadAllTextAsync(path, Encoding.UTF8);
|
||||
var name = Path.GetFileName(path);
|
||||
files.Add((name, content));
|
||||
}
|
||||
}
|
||||
|
||||
return format switch
|
||||
{
|
||||
ExportFormat.Html => GenerateHtml(projectName, files),
|
||||
ExportFormat.Markdown => GenerateMarkdown(projectName, files),
|
||||
ExportFormat.Zip => GenerateZip(files),
|
||||
ExportFormat.Docx => GenerateDocx(projectName, files),
|
||||
ExportFormat.Pdf => GeneratePdf(projectName, files),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(format))
|
||||
};
|
||||
}
|
||||
|
||||
private static byte[] GenerateHtml(string projectName, List<(string name, string content)> files)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("<!DOCTYPE html>");
|
||||
sb.AppendLine("<html lang=\"de\">");
|
||||
sb.AppendLine("<head>");
|
||||
sb.AppendLine("<meta charset=\"utf-8\">");
|
||||
sb.AppendLine("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
|
||||
sb.AppendLine($"<title>{EscapeHtml(projectName)} – Export</title>");
|
||||
sb.AppendLine("<style>");
|
||||
sb.AppendLine(GetHtmlCss());
|
||||
sb.AppendLine("</style>");
|
||||
sb.AppendLine("</head>");
|
||||
sb.AppendLine("<body>");
|
||||
sb.AppendLine($"<header class=\"doc-header\"><h1>{EscapeHtml(projectName)}</h1><p class=\"doc-date\">Exportiert: {DateTime.Now:dd.MM.yyyy HH:mm}</p></header>");
|
||||
|
||||
foreach (var (name, content) in files)
|
||||
{
|
||||
sb.AppendLine($"<section class=\"doc-section\"><h2>{EscapeHtml(name)}</h2><div class=\"doc-content\">{RenderMarkdownAsHtml(content)}</div></section>");
|
||||
}
|
||||
|
||||
sb.AppendLine("</body>");
|
||||
sb.AppendLine("</html>");
|
||||
return Encoding.UTF8.GetBytes(sb.ToString());
|
||||
}
|
||||
|
||||
private static byte[] GenerateMarkdown(string projectName, List<(string name, string content)> files)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"# {projectName}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"> Exportiert: {DateTime.Now:dd.MM.yyyy HH:mm}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("---");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var (name, content) in files)
|
||||
{
|
||||
sb.AppendLine($"## {name}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(content.TrimEnd());
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("---");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetBytes(sb.ToString());
|
||||
}
|
||||
|
||||
private static byte[] GenerateZip(List<(string name, string content)> files)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, true))
|
||||
{
|
||||
foreach (var (name, content) in files)
|
||||
{
|
||||
var entry = archive.CreateEntry(name, CompressionLevel.Optimal);
|
||||
using var writer = new StreamWriter(entry.Open(), Encoding.UTF8);
|
||||
writer.Write(content);
|
||||
}
|
||||
}
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
private static byte[] GenerateDocx(string projectName, List<(string name, string content)> files)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
using var doc = WordprocessingDocument.Create(ms, WordprocessingDocumentType.Document);
|
||||
var mainPart = doc.AddMainDocumentPart();
|
||||
mainPart.Document = new DocumentFormat.OpenXml.Wordprocessing.Document();
|
||||
var body = mainPart.Document.AppendChild(new Body());
|
||||
|
||||
body.AppendChild(new Paragraph(
|
||||
new Run(new RunProperties(new Bold()), new Text(projectName)))
|
||||
{ ParagraphProperties = new ParagraphProperties(new Justification { Val = JustificationValues.Center }) });
|
||||
|
||||
body.AppendChild(new Paragraph(
|
||||
new Run(new RunProperties(new DocumentFormat.OpenXml.Wordprocessing.Color { Val = "666666" }, new FontSize { Val = "20" }),
|
||||
new Text($"Exportiert: {DateTime.Now:dd.MM.yyyy HH:mm}"))));
|
||||
|
||||
foreach (var (name, content) in files)
|
||||
{
|
||||
body.AppendChild(new Paragraph(
|
||||
new Run(new RunProperties(new Bold(), new FontSize { Val = "32" }), new Text(name))
|
||||
));
|
||||
|
||||
var lines = content.Split('\n');
|
||||
foreach (var raw in lines)
|
||||
{
|
||||
var line = raw.TrimEnd('\r');
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
body.AppendChild(new Paragraph());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("### "))
|
||||
{
|
||||
body.AppendChild(new Paragraph(
|
||||
new Run(new RunProperties(new Bold(), new FontSize { Val = "28" }), new Text(line[4..]))
|
||||
));
|
||||
}
|
||||
else if (line.StartsWith("## "))
|
||||
{
|
||||
body.AppendChild(new Paragraph(
|
||||
new Run(new RunProperties(new Bold(), new FontSize { Val = "32" }), new Text(line[3..]))
|
||||
));
|
||||
}
|
||||
else if (line.StartsWith("# "))
|
||||
{
|
||||
body.AppendChild(new Paragraph(
|
||||
new Run(new RunProperties(new Bold(), new FontSize { Val = "36" }), new Text(line[2..]))
|
||||
));
|
||||
}
|
||||
else if (line.StartsWith("- ") || line.StartsWith("* "))
|
||||
{
|
||||
body.AppendChild(new Paragraph(
|
||||
new Run(new Text($" • {line[2..]}"))
|
||||
));
|
||||
}
|
||||
else if (line.Length > 2 && char.IsDigit(line[0]) && line[1] == '.' && line[2] == ' ')
|
||||
{
|
||||
body.AppendChild(new Paragraph(
|
||||
new Run(new Text($" {line}"))
|
||||
));
|
||||
}
|
||||
else
|
||||
{
|
||||
body.AppendChild(new Paragraph(
|
||||
new Run(new Text(line))
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doc.Save();
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
private static byte[] GeneratePdf(string projectName, List<(string name, string content)> files)
|
||||
{
|
||||
var doc = QuestPDF.Fluent.Document.Create(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A4);
|
||||
page.Margin(40);
|
||||
page.DefaultTextStyle(x => x.FontSize(11).FontFamily("Times New Roman"));
|
||||
|
||||
page.Header().Column(col =>
|
||||
{
|
||||
col.Item().AlignCenter().Text(projectName).Bold().FontSize(22);
|
||||
col.Item().AlignCenter().Text($"Exportiert: {DateTime.Now:dd.MM.yyyy HH:mm}").FontSize(10).FontColor(Colors.Grey.Medium);
|
||||
});
|
||||
|
||||
page.Content().Column(col =>
|
||||
{
|
||||
foreach (var (name, content) in files)
|
||||
{
|
||||
col.Item().PaddingTop(12).Text(name).Bold().FontSize(16);
|
||||
col.Item().PaddingLeft(4).Element(c => RenderMarkdownToPdf(c, content));
|
||||
}
|
||||
});
|
||||
|
||||
page.Footer().AlignCenter().Text(x =>
|
||||
{
|
||||
x.Span("Seite ").FontSize(9);
|
||||
x.CurrentPageNumber().FontSize(9);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
doc.GeneratePdf(ms);
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
private static void RenderMarkdownToPdf(IContainer container, string md)
|
||||
{
|
||||
var lines = md.Split('\n');
|
||||
container.Column(col =>
|
||||
{
|
||||
foreach (var raw in lines)
|
||||
{
|
||||
var line = raw.TrimEnd('\r');
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
col.Item().Height(8);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("### "))
|
||||
{
|
||||
col.Item().PaddingTop(6).Text(line[4..]).Bold().FontSize(13);
|
||||
}
|
||||
else if (line.StartsWith("## "))
|
||||
{
|
||||
col.Item().PaddingTop(8).Text(line[3..]).Bold().FontSize(15);
|
||||
}
|
||||
else if (line.StartsWith("# "))
|
||||
{
|
||||
col.Item().PaddingTop(10).Text(line[2..]).Bold().FontSize(17);
|
||||
}
|
||||
else if (line.StartsWith("- ") || line.StartsWith("* "))
|
||||
{
|
||||
col.Item().PaddingLeft(12).Text($" • {line[2..]}").FontSize(11);
|
||||
}
|
||||
else if (line is "---" or "***" or "___")
|
||||
{
|
||||
col.Item().Height(1).Background(Colors.Grey.Lighten2);
|
||||
}
|
||||
else if (line.Length > 2 && char.IsDigit(line[0]) && line[1] == '.' && line[2] == ' ')
|
||||
{
|
||||
col.Item().PaddingLeft(12).Text($" {line}").FontSize(11);
|
||||
}
|
||||
else
|
||||
{
|
||||
col.Item().Text(line).FontSize(11);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static string EscapeHtml(string text)
|
||||
=> text.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace("\"", """);
|
||||
|
||||
private static string RenderMarkdownAsHtml(string md)
|
||||
{
|
||||
var lines = md.Split('\n');
|
||||
var sb = new StringBuilder();
|
||||
var inList = false;
|
||||
|
||||
foreach (var raw in lines)
|
||||
{
|
||||
var line = raw.TrimEnd('\r');
|
||||
|
||||
if (line.StartsWith("###### "))
|
||||
{
|
||||
CloseList(ref inList, sb);
|
||||
sb.AppendLine($"<h6>{EscapeHtml(line[7..])}</h6>");
|
||||
continue;
|
||||
}
|
||||
if (line.StartsWith("##### "))
|
||||
{
|
||||
CloseList(ref inList, sb);
|
||||
sb.AppendLine($"<h5>{EscapeHtml(line[6..])}</h5>");
|
||||
continue;
|
||||
}
|
||||
if (line.StartsWith("#### "))
|
||||
{
|
||||
CloseList(ref inList, sb);
|
||||
sb.AppendLine($"<h4>{EscapeHtml(line[5..])}</h4>");
|
||||
continue;
|
||||
}
|
||||
if (line.StartsWith("### "))
|
||||
{
|
||||
CloseList(ref inList, sb);
|
||||
sb.AppendLine($"<h3>{EscapeHtml(line[4..])}</h3>");
|
||||
continue;
|
||||
}
|
||||
if (line.StartsWith("## "))
|
||||
{
|
||||
CloseList(ref inList, sb);
|
||||
sb.AppendLine($"<h2>{EscapeHtml(line[3..])}</h2>");
|
||||
continue;
|
||||
}
|
||||
if (line.StartsWith("# "))
|
||||
{
|
||||
CloseList(ref inList, sb);
|
||||
sb.AppendLine($"<h1>{EscapeHtml(line[2..])}</h1>");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("- ") || line.StartsWith("* "))
|
||||
{
|
||||
if (!inList)
|
||||
{
|
||||
inList = true;
|
||||
sb.AppendLine("<ul>");
|
||||
}
|
||||
sb.AppendLine($"<li>{RenderInline(line[2..])}</li>");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.Length > 2 && char.IsDigit(line[0]) && (line[1] == '.' || line[1] == ')') && line[2] == ' ')
|
||||
{
|
||||
if (!inList)
|
||||
{
|
||||
inList = true;
|
||||
sb.AppendLine("<ol>");
|
||||
}
|
||||
sb.AppendLine($"<li>{RenderInline(line[3..])}</li>");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line is "---" or "***" or "___")
|
||||
{
|
||||
CloseList(ref inList, sb);
|
||||
sb.AppendLine("<hr>");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
CloseList(ref inList, sb);
|
||||
continue;
|
||||
}
|
||||
|
||||
CloseList(ref inList, sb);
|
||||
sb.AppendLine($"<p>{RenderInline(line)}</p>");
|
||||
}
|
||||
|
||||
CloseList(ref inList, sb);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static void CloseList(ref bool inList, StringBuilder sb)
|
||||
{
|
||||
if (inList)
|
||||
{
|
||||
sb.AppendLine(sb[sb.Length - 1] == '>' ? "</ol>" : "</ul>");
|
||||
inList = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string RenderInline(string text)
|
||||
{
|
||||
var result = EscapeHtml(text);
|
||||
result = BoldRegex().Replace(result, "<strong>$1</strong>");
|
||||
result = ItalicRegex().Replace(result, "<em>$1</em>");
|
||||
result = UnderlineRegex().Replace(result, "<u>$1</u>");
|
||||
result = CodeRegex().Replace(result, "<code>$1</code>");
|
||||
result = LinkRegex().Replace(result, "<a href=\"$2\">$1</a>");
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string GetHtmlCss()
|
||||
{
|
||||
return """
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: Georgia, 'Times New Roman', serif; font-size: 12pt; line-height: 1.6; color: #222; max-width: 210mm; margin: 0 auto; padding: 20mm 30mm; background: #fff; }
|
||||
.doc-header { text-align: center; margin-bottom: 30px; padding-bottom: 15px; border-bottom: 2px solid #333; }
|
||||
.doc-header h1 { font-size: 22pt; margin-bottom: 4px; }
|
||||
.doc-date { font-size: 10pt; color: #666; }
|
||||
.doc-section { margin-bottom: 25px; page-break-before: auto; }
|
||||
.doc-section h2 { font-size: 16pt; margin-bottom: 10px; padding-bottom: 3px; border-bottom: 1px solid #ccc; color: #333; }
|
||||
.doc-content h1 { font-size: 18pt; margin: 15px 0 8px; }
|
||||
.doc-content h2 { font-size: 15pt; margin: 12px 0 6px; }
|
||||
.doc-content h3 { font-size: 13pt; margin: 10px 0 5px; }
|
||||
.doc-content h4 { font-size: 12pt; margin: 8px 0 4px; }
|
||||
.doc-content h5 { font-size: 11pt; margin: 6px 0 3px; }
|
||||
.doc-content h6 { font-size: 10pt; margin: 6px 0 3px; color: #555; }
|
||||
.doc-content p { margin: 6px 0; text-align: justify; }
|
||||
.doc-content ul, .doc-content ol { margin: 6px 0 6px 20px; }
|
||||
.doc-content li { margin: 2px 0; }
|
||||
.doc-content hr { margin: 15px 0; border: none; border-top: 1px solid #ccc; }
|
||||
.doc-content code { font-family: 'Consolas', 'Courier New', monospace; font-size: 10pt; background: #f4f4f4; padding: 1px 4px; border-radius: 2px; }
|
||||
.doc-content a { color: #1a73e8; text-decoration: underline; }
|
||||
.doc-content strong { font-weight: bold; }
|
||||
.doc-content em { font-style: italic; }
|
||||
@media print { body { padding: 0; } .doc-section { page-break-before: always; } .doc-section:first-of-type { page-break-before: auto; } }
|
||||
""";
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"\*\*(.+?)\*\*")]
|
||||
private static partial Regex BoldRegex();
|
||||
[GeneratedRegex(@"\*(.+?)\*")]
|
||||
private static partial Regex ItalicRegex();
|
||||
[GeneratedRegex(@"<u>(.+?)</u>")]
|
||||
private static partial Regex UnderlineRegex();
|
||||
[GeneratedRegex(@"`(.+?)`")]
|
||||
private static partial Regex CodeRegex();
|
||||
[GeneratedRegex(@"\[([^\]]+)\]\(([^)]+)\)")]
|
||||
private static partial Regex LinkRegex();
|
||||
}
|
||||
|
|
@ -1,17 +1,20 @@
|
|||
using AuthorBuddy.Web.Data;
|
||||
using LiteDB;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public class FileScannerService : IFileScannerService
|
||||
{
|
||||
private readonly IOllamaService _ollama;
|
||||
private readonly ILiteDatabase _db;
|
||||
private readonly IDbContextFactory<AppDbContext> _contextFactory;
|
||||
private readonly string _connectionString;
|
||||
|
||||
public FileScannerService(IOllamaService ollama, ILiteDatabase db)
|
||||
public FileScannerService(IOllamaService ollama, IDbContextFactory<AppDbContext> contextFactory)
|
||||
{
|
||||
_ollama = ollama;
|
||||
_db = db;
|
||||
_contextFactory = contextFactory;
|
||||
_connectionString = Models.Constants.SQLiteConnection;
|
||||
}
|
||||
|
||||
public async Task ScanProjectFolderAsync(int projectId, string rootPath, IProgress<string> progress)
|
||||
|
|
@ -22,55 +25,77 @@ public class FileScannerService : IFileScannerService
|
|||
foreach (var file in files)
|
||||
{
|
||||
var fileInfo = new FileInfo(file);
|
||||
bool needsUpdate = CheckIfUpdateRequired(projectId, file, fileInfo.LastWriteTimeUtc);
|
||||
bool needsUpdate = await CheckIfUpdateRequired(projectId, file, fileInfo.LastWriteTimeUtc);
|
||||
|
||||
if (needsUpdate)
|
||||
{
|
||||
progress?.Report($"Verarbeite: {fileInfo.Name}...");
|
||||
await ProcessFile(projectId, file, fileInfo.LastWriteTimeUtc);
|
||||
await ProcessAndVectorizeFile(projectId, file, fileInfo.LastWriteTimeUtc);
|
||||
}
|
||||
}
|
||||
progress?.Report("Scan abgeschlossen.");
|
||||
}
|
||||
|
||||
private bool CheckIfUpdateRequired(int projectId, string file, DateTime lastWriteTimeUtc)
|
||||
private async Task<bool> CheckIfUpdateRequired(int projectId, string file, DateTime lastWriteTimeUtc)
|
||||
{
|
||||
var col = _db.GetCollection<DocumentMetaEntity>("documentMeta");
|
||||
var meta = col.FindOne(m => m.ProjectId == projectId && m.FilePath == file);
|
||||
await using var context = await _contextFactory.CreateDbContextAsync();
|
||||
|
||||
var meta = await context.DocumentMeta
|
||||
.Where(m => m.ProjectId == projectId && m.FilePath == file)
|
||||
.Select(m => m.LastModified)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (meta == null) return true;
|
||||
return meta.LastModified < lastWriteTimeUtc;
|
||||
return meta.Value < lastWriteTimeUtc;
|
||||
}
|
||||
|
||||
private async Task<bool> ProcessFile(int projectId, string path, DateTime lastModified)
|
||||
private async Task<bool> ProcessAndVectorizeFile(int projectId, string path, DateTime lastModified)
|
||||
{
|
||||
string content = await File.ReadAllTextAsync(path);
|
||||
|
||||
var vector = await _ollama.GetEmbeddingAsync(content);
|
||||
|
||||
using var connection = new SqliteConnection(_connectionString);
|
||||
connection.Open();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
|
||||
try
|
||||
{
|
||||
var col = _db.GetCollection<DocumentMetaEntity>("documentMeta");
|
||||
var existing = col.FindOne(m => m.ProjectId == projectId && m.FilePath == path);
|
||||
var cmdMeta = connection.CreateCommand();
|
||||
cmdMeta.CommandText = @"
|
||||
INSERT INTO DocumentMeta (project_id, file_path, last_modified)
|
||||
VALUES (@pid, @path, @mod)
|
||||
ON CONFLICT(file_path) DO UPDATE SET last_modified = @mod
|
||||
RETURNING id;";
|
||||
cmdMeta.Parameters.AddWithValue("@pid", projectId);
|
||||
cmdMeta.Parameters.AddWithValue("@path", path);
|
||||
cmdMeta.Parameters.AddWithValue("@mod", lastModified);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
existing.LastModified = lastModified;
|
||||
col.Update(existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
col.Insert(new DocumentMetaEntity
|
||||
{
|
||||
ProjectId = projectId,
|
||||
FilePath = path,
|
||||
LastModified = lastModified,
|
||||
FileType = ".md"
|
||||
});
|
||||
}
|
||||
var docId = await cmdMeta.ExecuteScalarAsync();
|
||||
|
||||
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;";
|
||||
cmdVec.Parameters.AddWithValue("@id", docId);
|
||||
cmdVec.Parameters.AddWithValue("@vec", blob);
|
||||
await cmdVec.ExecuteNonQueryAsync();
|
||||
|
||||
transaction.Commit();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
transaction.Rollback();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] ConvertVectorToBytes(IList<double> vector)
|
||||
{
|
||||
float[] floatVector = vector.Select(d => (float)d).ToArray();
|
||||
byte[] bytes = new byte[floatVector.Length * sizeof(float)];
|
||||
Buffer.BlockCopy(floatVector, 0, bytes, 0, bytes.Length);
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
using AuthorBuddy.Web.Data;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public interface IAuthService
|
||||
{
|
||||
Task<bool> LoginAsync(string username, string password);
|
||||
Task LogoutAsync();
|
||||
Task<bool> RegisterAsync(string username, string password);
|
||||
Task AdminBypassLoginAsync(int userId);
|
||||
Task<UserEntity?> GetCurrentUserAsync();
|
||||
bool IsLoggedIn { get; }
|
||||
bool Is2FARequired { get; }
|
||||
Task<bool> Verify2FAAsync(string code);
|
||||
Task<string> Generate2FASecretAsync();
|
||||
string Get2FAProvisioningUri(string secret, string username);
|
||||
Task<bool> Enable2FAAsync(string secret);
|
||||
Task<bool> Disable2FAAsync();
|
||||
Task<bool> Is2FAEnabledAsync();
|
||||
Task<string?> GetCurrent2FASecretAsync();
|
||||
Task<List<UserEntity>> GetAllUsersAsync();
|
||||
Task<bool> SetUserActiveAsync(int userId, bool active);
|
||||
Task<bool> DeleteUserAsync(int userId);
|
||||
UserEntity? CurrentUser { get; }
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public class DbCollectionInfo
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public int Count { get; set; }
|
||||
}
|
||||
|
||||
public class DbRecord
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public Dictionary<string, string?> Fields { get; set; } = new();
|
||||
}
|
||||
|
||||
public interface IDbAdminService
|
||||
{
|
||||
List<DbCollectionInfo> GetCollections();
|
||||
List<DbRecord> GetAll(string collectionName);
|
||||
bool Update(string collectionName, int id, Dictionary<string, string?> values);
|
||||
bool Delete(string collectionName, int id);
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public enum ExportFormat
|
||||
{
|
||||
Html,
|
||||
Markdown,
|
||||
Zip,
|
||||
Docx,
|
||||
Pdf
|
||||
}
|
||||
|
||||
public interface IExportService
|
||||
{
|
||||
Task<byte[]> ExportAsync(int projectId, List<string> filePaths, ExportFormat format);
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
using AuthorBuddy.Web.Models;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public interface ILLMBackend
|
||||
{
|
||||
string Name { get; }
|
||||
bool SupportsModelManagement { get; }
|
||||
|
||||
Task<bool> TestConnectionAsync();
|
||||
Task<IEnumerable<LLM_Model>> GetModelsAsync();
|
||||
Task<string> ChatAsync(string model, string prompt, string? systemPrompt = null, double? temperature = null);
|
||||
Task ChatStreamAsync(string model, string prompt, Func<string, Task> onToken, string? systemPrompt = null, double? temperature = null);
|
||||
Task<float[]> GetEmbeddingAsync(string text, string? model = null);
|
||||
Task<bool> PullModelAsync(string name, IProgress<string>? progress = null);
|
||||
Task<bool> DeleteModelAsync(string name);
|
||||
Task<int?> GetContextSizeAsync(string model);
|
||||
Task<long> MeasureLatencyAsync(string model);
|
||||
Task<bool> ModelExistsAsync(string model);
|
||||
Task<(bool Success, int ContextWindow)> GetModelContextWindowAsync(string model);
|
||||
bool TestEmbedding(string model);
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public interface ILocalizationService
|
||||
{
|
||||
string this[string key] { get; }
|
||||
string Get(string key);
|
||||
Task InitAsync();
|
||||
Task SetLanguageAsync(string language);
|
||||
string CurrentLanguage { get; }
|
||||
}
|
||||
|
|
@ -1,22 +1,15 @@
|
|||
|
||||
|
||||
using AuthorBuddy.Web.Models;
|
||||
using Ollama;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public interface IOllamaService
|
||||
{
|
||||
Task<IEnumerable<LLM_Model>> GetLocalModelsAsync();
|
||||
Task<bool> PullModelAsync(string modelName, IProgress<string> progress);
|
||||
Task<string> QuickSpellCheckAsync(string text);
|
||||
Task<float[]> GetEmbeddingAsync(string text);
|
||||
Task<bool> TestEmbeddingAsync(string model);
|
||||
Task<string> Chat(string model, string userMessage, string? systemPrompt = null, double? temperature = null);
|
||||
Task ChatStream(string model, string userMessage, Func<string, Task> onToken, string? systemPrompt = null, double? temperature = null);
|
||||
Task<IEnumerable<Model>> GetLocalModelsAsync();
|
||||
Task PullModelAsync(string modelName, IProgress<ModelStatus> progress);
|
||||
Task<IList<double>> GetEmbeddingAsync(string text);
|
||||
Task<string> StreamStyleCheckAsync(string input);
|
||||
Task<string> AnswerBasedOnFactsAsync(string question, string facts);
|
||||
Task<(bool Success, long LatencyMs)> TestConnectionAsync();
|
||||
Task<bool> ModelExistsAsync(string model);
|
||||
Task<(bool Success, int ContextWindow)> GetModelContextWindowAsync(string model);
|
||||
Task<long> MeasureLatencyAsync(string model);
|
||||
void Reconfigure(string ollamaUrl);
|
||||
Task<string> QuickSpellCheckAsync(string text);
|
||||
Task ReconfigureAsync(string ollamaUrl);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
using AuthorBuddy.Web.Models;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public interface IOllamaSetupService
|
||||
{
|
||||
Task<SetupRecommendation> AnalyzeSettingsAsync();
|
||||
Task<bool> CreateConfigFileAsync(OllamaSettings settings);
|
||||
}
|
||||
|
||||
public class SetupRecommendation
|
||||
{
|
||||
public bool IsCorrect { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public OllamaSettings RecommendedSettings { get; set; } = new();
|
||||
public List<string> Findings { get; set; } = new();
|
||||
}
|
||||
|
|
@ -4,14 +4,7 @@ namespace AuthorBuddy.Web.Services;
|
|||
|
||||
public interface IProjectService
|
||||
{
|
||||
Task<int> CreateNewProjectAsync(string projectName, string rootPath, int userId, ProjectTemplate template = ProjectTemplate.Novel);
|
||||
Task<List<Project>> GetAllProjectsAsync(int? userId = null);
|
||||
Task<Project?> GetProjectAsync(int projectId);
|
||||
Task<List<FileTreeItem>> GetProjectFileTreeAsync(int projectId);
|
||||
Task<int> CreateNewProjectAsync(string projectName, string rootPath);
|
||||
Task<List<Project>> GetAllProjectsAsync();
|
||||
Task SyncProjectFilesAsync(int projectId, string directoryPath, IProgress<string> progress);
|
||||
Task<int> CopyProjectAsync(int sourceProjectId, string newName, string newRootPath, int userId);
|
||||
|
||||
Task<Dictionary<string, FileCategory>> GetFileCategoriesAsync(int projectId);
|
||||
Task<bool> SetFileCategoryAsync(int projectId, string filePath, FileCategory category);
|
||||
Task ClearFileCategoryAsync(int projectId, string filePath);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public class RagSearchResult
|
||||
{
|
||||
public string Text { get; set; } = string.Empty;
|
||||
public string FilePath { get; set; } = string.Empty;
|
||||
public double Similarity { get; set; }
|
||||
}
|
||||
|
||||
public interface IRagService
|
||||
{
|
||||
Task IndexProjectAsync(int projectId, string rootPath, IProgress<string> progress);
|
||||
Task<List<RagSearchResult>> SearchAsync(int projectId, string query, int topN = 5);
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
using AuthorBuddy.Web.Data;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public interface ISettingsService
|
||||
{
|
||||
Task<string?> GetOllamaUrlAsync();
|
||||
Task<string?> GetLlamaCppUrlAsync();
|
||||
Task<string?> GetBackendAsync();
|
||||
Task<string?> GetRagModelAsync();
|
||||
Task<string?> GetStyleModelAsync();
|
||||
Task<string?> GetSpellingModelAsync();
|
||||
Task<string?> GetThemeAsync();
|
||||
Task SaveOllamaUrlAsync(string url);
|
||||
Task SaveLlamaCppUrlAsync(string url);
|
||||
Task SaveBackendAsync(string backend);
|
||||
Task SaveRagModelAsync(string model);
|
||||
Task SaveStyleModelAsync(string model);
|
||||
Task SaveSpellingModelAsync(string model);
|
||||
Task SaveThemeAsync(string theme);
|
||||
|
||||
Task<string?> GetLektorSystemPromptAsync();
|
||||
Task<string?> GetFactCheckTemplateAsync();
|
||||
Task<string?> GetSpellingPromptAsync();
|
||||
Task<string?> GetAdvocatusDiaboliPromptAsync();
|
||||
Task<string?> GetHookFinderPromptAsync();
|
||||
Task<string?> GetEli5PromptAsync();
|
||||
Task<string?> GetCoAuthorBrainstormingPromptAsync();
|
||||
|
||||
Task SaveLektorSystemPromptAsync(string prompt);
|
||||
Task SaveFactCheckTemplateAsync(string prompt);
|
||||
Task SaveSpellingPromptAsync(string prompt);
|
||||
Task SaveAdvocatusDiaboliPromptAsync(string prompt);
|
||||
Task SaveHookFinderPromptAsync(string prompt);
|
||||
Task SaveEli5PromptAsync(string prompt);
|
||||
Task SaveCoAuthorBrainstormingPromptAsync(string prompt);
|
||||
Task<double> GetLektorTempAsync();
|
||||
Task<double> GetFactCheckTempAsync();
|
||||
Task<double> GetSpellingTempAsync();
|
||||
Task<double> GetAdvocatusDiaboliTempAsync();
|
||||
Task<double> GetHookFinderTempAsync();
|
||||
Task<double> GetEli5TempAsync();
|
||||
Task<double> GetCoAuthorTempAsync();
|
||||
Task SaveLektorTempAsync(double temp);
|
||||
Task SaveFactCheckTempAsync(double temp);
|
||||
Task SaveSpellingTempAsync(double temp);
|
||||
Task SaveAdvocatusDiaboliTempAsync(double temp);
|
||||
Task SaveHookFinderTempAsync(double temp);
|
||||
Task SaveEli5TempAsync(double temp);
|
||||
Task SaveCoAuthorTempAsync(double temp);
|
||||
|
||||
Task<string?> GetTechLastenheftPromptAsync();
|
||||
Task<string?> GetTechPflichtenheftPromptAsync();
|
||||
Task<string?> GetTechUseCasePromptAsync();
|
||||
Task<string?> GetTechStakeholderPromptAsync();
|
||||
Task<string?> GetTechGlossarPromptAsync();
|
||||
Task<string?> GetTechQualitaetPromptAsync();
|
||||
Task<string?> GetTechRisikoPromptAsync();
|
||||
Task SaveTechLastenheftPromptAsync(string prompt);
|
||||
Task SaveTechPflichtenheftPromptAsync(string prompt);
|
||||
Task SaveTechUseCasePromptAsync(string prompt);
|
||||
Task SaveTechStakeholderPromptAsync(string prompt);
|
||||
Task SaveTechGlossarPromptAsync(string prompt);
|
||||
Task SaveTechQualitaetPromptAsync(string prompt);
|
||||
Task SaveTechRisikoPromptAsync(string prompt);
|
||||
Task<double> GetTechLastenheftTempAsync();
|
||||
Task<double> GetTechPflichtenheftTempAsync();
|
||||
Task<double> GetTechUseCaseTempAsync();
|
||||
Task<double> GetTechStakeholderTempAsync();
|
||||
Task<double> GetTechGlossarTempAsync();
|
||||
Task<double> GetTechQualitaetTempAsync();
|
||||
Task<double> GetTechRisikoTempAsync();
|
||||
Task SaveTechLastenheftTempAsync(double temp);
|
||||
Task SaveTechPflichtenheftTempAsync(double temp);
|
||||
Task SaveTechUseCaseTempAsync(double temp);
|
||||
Task SaveTechStakeholderTempAsync(double temp);
|
||||
Task SaveTechGlossarTempAsync(double temp);
|
||||
Task SaveTechQualitaetTempAsync(double temp);
|
||||
Task SaveTechRisikoTempAsync(double temp);
|
||||
Task<string?> GetLanguageAsync();
|
||||
Task SaveLanguageAsync(string language);
|
||||
|
||||
Task<List<UserEntity>> GetUsersAsync();
|
||||
Task<UserEntity> GetCurrentUserAsync();
|
||||
void SetCurrentUser(int userId);
|
||||
Task<UserEntity> CreateUserAsync(string name);
|
||||
|
||||
int? CurrentProjectId { get; set; }
|
||||
string? CurrentFilePath { get; set; }
|
||||
string? PendingAssistantInput { get; set; }
|
||||
string? PendingAssistantOutput { get; set; }
|
||||
Task<int?> GetLastProjectIdAsync();
|
||||
Task SaveLastProjectIdAsync(int projectId);
|
||||
Task LoadProjectSettingsAsync();
|
||||
|
||||
Task InitializeAsync();
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public interface IThemeService
|
||||
{
|
||||
Task ApplyTheme(string theme);
|
||||
|
||||
string Theme { get; }
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
using AuthorBuddy.Web.Models;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public interface IWikipediaService
|
||||
{
|
||||
Task<List<WikipediaResult>> SearchAsync(string query, int maxResults = 3);
|
||||
Task<List<WikipediaResult>> SearchAsync(WikipediaSearchRequest request);
|
||||
Task<string?> GetExtractAsync(string title, string language = "de");
|
||||
Task<string?> GetFullArticleAsync(string title, string language = "de");
|
||||
}
|
||||
|
|
@ -1,236 +0,0 @@
|
|||
using System.Text.Json;
|
||||
using AuthorBuddy.Web.Models;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public class LlamaCppBackend : ILLMBackend
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly OllamaSettings _settings;
|
||||
|
||||
public string Name => "llama.cpp";
|
||||
public bool SupportsModelManagement => false;
|
||||
|
||||
public LlamaCppBackend(OllamaSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
_http = new HttpClient { Timeout = TimeSpan.FromSeconds(300) };
|
||||
}
|
||||
|
||||
private string Url => _settings.LlamaCppUrl;
|
||||
|
||||
public async Task<bool> TestConnectionAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _http.GetAsync($"{Url}/health");
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<LLM_Model>> GetModelsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _http.GetAsync($"{Url}/v1/models");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return new List<LLM_Model> { new LLM_Model("llama.cpp") };
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
using var doc = JsonDocument.Parse(content);
|
||||
if (!doc.RootElement.TryGetProperty("data", out var data))
|
||||
return new List<LLM_Model> { new LLM_Model("llama.cpp") };
|
||||
|
||||
var list = new List<LLM_Model>();
|
||||
foreach (var m in data.EnumerateArray())
|
||||
{
|
||||
var id = m.TryGetProperty("id", out var idProp) ? idProp.GetString() ?? "llama.cpp" : "llama.cpp";
|
||||
list.Add(new LLM_Model(id));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
catch { return new List<LLM_Model> { new LLM_Model("llama.cpp") }; }
|
||||
}
|
||||
|
||||
public async Task<string> ChatAsync(string model, string prompt, string? systemPrompt = null, double? temperature = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var messages = new List<object>();
|
||||
if (!string.IsNullOrEmpty(systemPrompt))
|
||||
messages.Add(new { role = "system", content = systemPrompt });
|
||||
messages.Add(new { role = "user", content = prompt });
|
||||
|
||||
var request = new Dictionary<string, object?>
|
||||
{
|
||||
["model"] = model,
|
||||
["messages"] = messages,
|
||||
["stream"] = false
|
||||
};
|
||||
if (temperature.HasValue) request["temperature"] = Math.Clamp(temperature.Value, 0.0, 2.0);
|
||||
|
||||
var response = await _http.PostAsync(
|
||||
$"{Url}/v1/chat/completions",
|
||||
new StringContent(JsonSerializer.Serialize(request), System.Text.Encoding.UTF8, "application/json")
|
||||
);
|
||||
if (!response.IsSuccessStatusCode) return "Fehler bei der Verarbeitung.";
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
using var doc = JsonDocument.Parse(content);
|
||||
var root = doc.RootElement;
|
||||
if (root.TryGetProperty("choices", out var choices) && choices.GetArrayLength() > 0)
|
||||
{
|
||||
var first = choices[0];
|
||||
if (first.TryGetProperty("message", out var msg) && msg.TryGetProperty("content", out var text))
|
||||
return text.GetString() ?? "";
|
||||
}
|
||||
return "Fehler bei der Verarbeitung.";
|
||||
}
|
||||
catch (Exception ex) { return $"Error: {ex.Message}"; }
|
||||
}
|
||||
|
||||
public async Task ChatStreamAsync(string model, string prompt, Func<string, Task> onToken, string? systemPrompt = null, double? temperature = null)
|
||||
{
|
||||
var messages = new List<object>();
|
||||
if (!string.IsNullOrEmpty(systemPrompt))
|
||||
messages.Add(new { role = "system", content = systemPrompt });
|
||||
messages.Add(new { role = "user", content = prompt });
|
||||
|
||||
var request = new Dictionary<string, object?>
|
||||
{
|
||||
["model"] = model,
|
||||
["messages"] = messages,
|
||||
["stream"] = true
|
||||
};
|
||||
if (temperature.HasValue) request["temperature"] = Math.Clamp(temperature.Value, 0.0, 2.0);
|
||||
|
||||
using var response = await _http.PostAsync(
|
||||
$"{Url}/v1/chat/completions",
|
||||
new StringContent(JsonSerializer.Serialize(request), System.Text.Encoding.UTF8, "application/json")
|
||||
);
|
||||
if (!response.IsSuccessStatusCode) return;
|
||||
|
||||
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;
|
||||
if (!line.StartsWith("data: ")) continue;
|
||||
|
||||
var json = line[6..];
|
||||
if (json == "[DONE]") break;
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
if (root.TryGetProperty("choices", out var choices) && choices.GetArrayLength() > 0)
|
||||
{
|
||||
var delta = choices[0].TryGetProperty("delta", out var d) ? d : default;
|
||||
if (delta.ValueKind != JsonValueKind.Undefined && delta.TryGetProperty("content", out var content))
|
||||
{
|
||||
var text = content.GetString();
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
await onToken(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<float[]> GetEmbeddingAsync(string text, string? model = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new { content = text };
|
||||
var response = await _http.PostAsync(
|
||||
$"{Url}/embedding",
|
||||
new StringContent(JsonSerializer.Serialize(request), System.Text.Encoding.UTF8, "application/json")
|
||||
);
|
||||
if (!response.IsSuccessStatusCode) return Array.Empty<float>();
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
using var doc = JsonDocument.Parse(content);
|
||||
if (!doc.RootElement.TryGetProperty("embedding", out var emb)) return Array.Empty<float>();
|
||||
var list = new List<float>();
|
||||
foreach (var v in emb.EnumerateArray()) list.Add(v.GetSingle());
|
||||
return list.ToArray();
|
||||
}
|
||||
catch { return Array.Empty<float>(); }
|
||||
}
|
||||
|
||||
public Task<bool> PullModelAsync(string name, IProgress<string>? progress = null)
|
||||
=> Task.FromResult(false);
|
||||
|
||||
public Task<bool> DeleteModelAsync(string name)
|
||||
=> Task.FromResult(false);
|
||||
|
||||
public async Task<int?> GetContextSizeAsync(string model)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _http.GetAsync($"{Url}/slots");
|
||||
if (!response.IsSuccessStatusCode) return 2048;
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
using var doc = JsonDocument.Parse(content);
|
||||
if (doc.RootElement.GetArrayLength() > 0)
|
||||
{
|
||||
var first = doc.RootElement[0];
|
||||
if (first.TryGetProperty("n_ctx", out var ctx))
|
||||
return ctx.GetInt32();
|
||||
}
|
||||
return 2048;
|
||||
}
|
||||
catch { return 2048; }
|
||||
}
|
||||
|
||||
public async Task<long> MeasureLatencyAsync(string model)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var response = await _http.GetAsync($"{Url}/health");
|
||||
sw.Stop();
|
||||
return response.IsSuccessStatusCode ? sw.ElapsedMilliseconds : -1;
|
||||
}
|
||||
catch { return -1; }
|
||||
}
|
||||
|
||||
public async Task<bool> ModelExistsAsync(string model)
|
||||
{
|
||||
try
|
||||
{
|
||||
var models = await GetModelsAsync();
|
||||
return models.Any();
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
public async Task<(bool Success, int ContextWindow)> GetModelContextWindowAsync(string model)
|
||||
{
|
||||
var ctx = await GetContextSizeAsync(model);
|
||||
return (ctx.HasValue, ctx ?? 2048);
|
||||
}
|
||||
|
||||
public bool TestEmbedding(string model)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = _http.PostAsync(
|
||||
$"{Url}/embedding",
|
||||
new StringContent(JsonSerializer.Serialize(new { content = "test" }), System.Text.Encoding.UTF8, "application/json")
|
||||
).GetAwaiter().GetResult();
|
||||
if (!response.IsSuccessStatusCode) return false;
|
||||
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
|
||||
using var doc = JsonDocument.Parse(content);
|
||||
if (doc.RootElement.TryGetProperty("embedding", out var emb))
|
||||
return emb.EnumerateArray().Any();
|
||||
return false;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
using System.Text.Json;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public class LocalizationService : ILocalizationService
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
private Dictionary<string, string> _translations = new();
|
||||
private string _language = "de";
|
||||
|
||||
public string CurrentLanguage => _language;
|
||||
|
||||
public LocalizationService(ISettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
public async Task InitAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_language = await _settingsService.GetLanguageAsync() ?? "de";
|
||||
}
|
||||
catch
|
||||
{
|
||||
_language = "de";
|
||||
}
|
||||
await LoadLanguageInternalAsync(_language);
|
||||
}
|
||||
|
||||
public string Get(string key)
|
||||
{
|
||||
if (_translations.TryGetValue(key, out var value))
|
||||
return value;
|
||||
return Fallback(key);
|
||||
}
|
||||
|
||||
public string this[string key] => Get(key);
|
||||
|
||||
public async Task SetLanguageAsync(string language)
|
||||
{
|
||||
_language = language;
|
||||
await LoadLanguageInternalAsync(language);
|
||||
}
|
||||
|
||||
private async Task LoadLanguageInternalAsync(string language)
|
||||
{
|
||||
var searched = new List<string>();
|
||||
|
||||
var paths = new[]
|
||||
{
|
||||
Path.Combine(AppContext.BaseDirectory, "Resources", $"{language}.json"),
|
||||
Path.Combine(Directory.GetCurrentDirectory(), "Resources", $"{language}.json"),
|
||||
};
|
||||
|
||||
searched.AddRange(paths);
|
||||
|
||||
foreach (var path in paths)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = await File.ReadAllTextAsync(path);
|
||||
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||
if (dict != null && dict.Count > 0)
|
||||
{
|
||||
_translations = dict;
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// try next path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_translations = new();
|
||||
}
|
||||
|
||||
private static string Fallback(string key)
|
||||
{
|
||||
return key switch
|
||||
{
|
||||
"common.status.ready" => "Bereit",
|
||||
"common.cancel" => "Abbrechen",
|
||||
"common.error" => "Fehler:",
|
||||
"common.copy" => "Kopieren",
|
||||
"common.status.error" => "Fehler",
|
||||
"page.title.login" => "Anmelden - Author Buddy",
|
||||
"page.title.register" => "Registrieren - Author Buddy",
|
||||
"page.title.setup2fa" => "2FA einrichten - Author Buddy",
|
||||
"page.title.settings" => "Einstellungen",
|
||||
"page.title.editor" => "Editor - Author Buddy",
|
||||
"page.title.assistant" => "Assistent - Author Buddy",
|
||||
"page.title.models" => "Modelle - Author Buddy",
|
||||
"page.title.home" => "Dashboard - Author Buddy",
|
||||
"page.title.error" => "Fehler",
|
||||
"page.title.not_found" => "Seite nicht gefunden",
|
||||
"login.heading" => "Anmelden",
|
||||
"login.label.username" => "Benutzername",
|
||||
"login.label.password" => "Passwort",
|
||||
"login.button.submit" => "Anmelden",
|
||||
"login.link.register" => "Registrieren",
|
||||
"login.2fa.instruction" => "Gib deinen 2FA-Code ein:",
|
||||
"login.2fa.label.code" => "2FA-Code",
|
||||
"login.2fa.button.verify" => "Bestätigen",
|
||||
"login.error.empty_fields" => "Bitte Benutzername und Passwort eingeben.",
|
||||
"login.error.invalid_credentials" => "Ungültige Anmeldedaten.",
|
||||
"login.2fa.error.invalid_code" => "Der 2FA-Code muss 6-stellig sein.",
|
||||
"login.2fa.error.code_failed" => "Ungültiger 2FA-Code.",
|
||||
"nav.user.logged_in_as" => "Angemeldet als",
|
||||
"nav.user.manage_2fa" => "2FA verwalten",
|
||||
"nav.user.logout" => "Abmelden",
|
||||
"assistant.exposee.context" => "Berücksichtige folgendes Exposé der Geschichte:",
|
||||
_ => key,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,253 +0,0 @@
|
|||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using AuthorBuddy.Web.Models;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public class OllamaBackend : ILLMBackend
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly OllamaSettings _settings;
|
||||
|
||||
public string Name => "Ollama";
|
||||
public bool SupportsModelManagement => true;
|
||||
|
||||
public OllamaBackend(OllamaSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
_http = new HttpClient { Timeout = TimeSpan.FromSeconds(300) };
|
||||
}
|
||||
|
||||
private string Url => _settings.OllamaUrl;
|
||||
|
||||
public async Task<bool> TestConnectionAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var response = await _http.GetAsync($"{Url}/api/tags");
|
||||
sw.Stop();
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<LLM_Model>> GetModelsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _http.GetAsync($"{Url}/api/tags");
|
||||
if (!response.IsSuccessStatusCode) return Enumerable.Empty<LLM_Model>();
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
using var doc = JsonDocument.Parse(content);
|
||||
if (!doc.RootElement.TryGetProperty("models", out var models))
|
||||
return Enumerable.Empty<LLM_Model>();
|
||||
var list = new List<LLM_Model>();
|
||||
foreach (var m in models.EnumerateArray())
|
||||
list.Add(new LLM_Model(m));
|
||||
return list;
|
||||
}
|
||||
catch { return Enumerable.Empty<LLM_Model>(); }
|
||||
}
|
||||
|
||||
public async Task<string> ChatAsync(string model, string prompt, string? systemPrompt = null, double? temperature = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new Dictionary<string, object?>
|
||||
{
|
||||
["model"] = model, ["prompt"] = prompt, ["stream"] = false
|
||||
};
|
||||
if (!string.IsNullOrEmpty(systemPrompt)) request["system"] = systemPrompt;
|
||||
if (temperature.HasValue) request["temperature"] = Math.Clamp(temperature.Value, 0.0, 2.0);
|
||||
|
||||
var response = await _http.PostAsJsonAsync($"{Url}/api/generate", request);
|
||||
if (!response.IsSuccessStatusCode) return "Fehler bei der Verarbeitung.";
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
using var doc = JsonDocument.Parse(content);
|
||||
if (doc.RootElement.TryGetProperty("response", out var resp))
|
||||
return resp.GetString() ?? "";
|
||||
return "Fehler bei der Verarbeitung.";
|
||||
}
|
||||
catch (Exception ex) { return $"Error: {ex.Message}"; }
|
||||
}
|
||||
|
||||
public async Task ChatStreamAsync(string model, string prompt, Func<string, Task> onToken, string? systemPrompt = null, double? temperature = null)
|
||||
{
|
||||
var request = new Dictionary<string, object?>
|
||||
{
|
||||
["model"] = model, ["prompt"] = prompt, ["stream"] = true
|
||||
};
|
||||
if (!string.IsNullOrEmpty(systemPrompt)) request["system"] = systemPrompt;
|
||||
if (temperature.HasValue) request["temperature"] = Math.Clamp(temperature.Value, 0.0, 2.0);
|
||||
|
||||
using var response = await _http.PostAsync(
|
||||
$"{Url}/api/generate",
|
||||
new StringContent(JsonSerializer.Serialize(request), System.Text.Encoding.UTF8, "application/json")
|
||||
);
|
||||
if (!response.IsSuccessStatusCode) return;
|
||||
|
||||
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))
|
||||
await onToken(text);
|
||||
}
|
||||
}
|
||||
|
||||
private string ExtractResponseText(string jsonLine)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(jsonLine)) return string.Empty;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(jsonLine);
|
||||
if (doc.RootElement.TryGetProperty("response", out var resp))
|
||||
return resp.GetString() ?? string.Empty;
|
||||
}
|
||||
catch { }
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public async Task<float[]> GetEmbeddingAsync(string text, string? model = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var m = model ?? _settings.EmbedModel;
|
||||
var request = new { model = m, prompt = text };
|
||||
var response = await _http.PostAsJsonAsync($"{Url}/api/embeddings", request);
|
||||
if (!response.IsSuccessStatusCode) return Array.Empty<float>();
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
using var doc = JsonDocument.Parse(content);
|
||||
if (!doc.RootElement.TryGetProperty("embedding", out var emb)) return Array.Empty<float>();
|
||||
var list = new List<float>();
|
||||
foreach (var v in emb.EnumerateArray()) list.Add(v.GetSingle());
|
||||
return list.ToArray();
|
||||
}
|
||||
catch { return Array.Empty<float>(); }
|
||||
}
|
||||
|
||||
public async Task<bool> PullModelAsync(string name, IProgress<string>? progress = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new { name, stream = true };
|
||||
var response = await _http.PostAsync(
|
||||
$"{Url}/api/pull",
|
||||
new StringContent(JsonSerializer.Serialize(request), System.Text.Encoding.UTF8, "application/json")
|
||||
);
|
||||
if (!response.IsSuccessStatusCode) return false;
|
||||
|
||||
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 st = status.GetString() ?? "";
|
||||
var msg = st;
|
||||
if (root.TryGetProperty("completed", out var c) && root.TryGetProperty("total", out var t))
|
||||
{
|
||||
var comp = c.GetInt64(); var tot = t.GetInt64();
|
||||
if (tot > 0) msg = $"{st}: {comp}/{tot} ({(int)(comp * 100 / tot)}%)";
|
||||
}
|
||||
progress?.Report(msg);
|
||||
if (st == "success") return true;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteModelAsync(string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
var req = new HttpRequestMessage(HttpMethod.Delete, $"{Url}/api/delete")
|
||||
{
|
||||
Content = new StringContent(JsonSerializer.Serialize(new { name }), System.Text.Encoding.UTF8, "application/json")
|
||||
};
|
||||
var response = await _http.SendAsync(req);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
public async Task<int?> GetContextSizeAsync(string model)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _http.PostAsJsonAsync($"{Url}/api/show", new { model });
|
||||
if (!response.IsSuccessStatusCode) return null;
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
using var doc = JsonDocument.Parse(content);
|
||||
if (doc.RootElement.TryGetProperty("modelfile", out var mf))
|
||||
{
|
||||
var modelfile = mf.GetString() ?? "";
|
||||
var match = Regex.Match(modelfile, @"num_ctx\s+(\d+)");
|
||||
if (match.Success && int.TryParse(match.Groups[1].Value, out var ctx))
|
||||
return ctx;
|
||||
}
|
||||
return 2048;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
public async Task<long> MeasureLatencyAsync(string model)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var request = new { model, prompt = "Hallo", stream = false };
|
||||
var response = await _http.PostAsJsonAsync($"{Url}/api/generate", request);
|
||||
sw.Stop();
|
||||
return response.IsSuccessStatusCode ? sw.ElapsedMilliseconds : -1;
|
||||
}
|
||||
catch { return -1; }
|
||||
}
|
||||
|
||||
public async Task<bool> ModelExistsAsync(string model)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(model)) return false;
|
||||
try
|
||||
{
|
||||
var models = await GetModelsAsync();
|
||||
return models.Any(m => m.Name == model || m.Name.StartsWith(model.Split(':')[0]));
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
public async Task<(bool Success, int ContextWindow)> GetModelContextWindowAsync(string model)
|
||||
{
|
||||
var ctx = await GetContextSizeAsync(model);
|
||||
return (ctx.HasValue, ctx ?? 0);
|
||||
}
|
||||
|
||||
public bool TestEmbedding(string model)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new { model, prompt = "test" };
|
||||
var response = _http.PostAsJsonAsync($"{Url}/api/embeddings", request).GetAwaiter().GetResult();
|
||||
if (!response.IsSuccessStatusCode) return false;
|
||||
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
|
||||
using var doc = JsonDocument.Parse(content);
|
||||
if (doc.RootElement.TryGetProperty("embedding", out var emb))
|
||||
return emb.EnumerateArray().Any();
|
||||
return false;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,115 +1,84 @@
|
|||
using AuthorBuddy.Web.Models;
|
||||
using Ollama;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public class OllamaService : IOllamaService
|
||||
public class OllamaService : IOllamaService, IDisposable
|
||||
{
|
||||
private readonly IEnumerable<ILLMBackend> _backends;
|
||||
private readonly OllamaSettings _settings;
|
||||
private readonly OperationState _opState;
|
||||
private OllamaApiClient? _client;
|
||||
private string _currentUrl = "http://localhost:11434";
|
||||
|
||||
public OllamaService(IEnumerable<ILLMBackend> backends, OllamaSettings settings, OperationState opState)
|
||||
public OllamaService(OllamaSettings settings)
|
||||
{
|
||||
_backends = backends;
|
||||
_settings = settings;
|
||||
_opState = opState;
|
||||
_currentUrl = settings.OllamaUrl;
|
||||
_client = new OllamaApiClient(baseUri: new Uri(_currentUrl));
|
||||
}
|
||||
|
||||
private ILLMBackend ActiveBackend =>
|
||||
_backends.FirstOrDefault(b => b.Name == _settings.Backend) ?? _backends.First();
|
||||
|
||||
public void Reconfigure(string url)
|
||||
public Task ReconfigureAsync(string ollamaUrl)
|
||||
{
|
||||
_settings.OllamaUrl = url;
|
||||
if (_currentUrl != ollamaUrl)
|
||||
{
|
||||
_currentUrl = ollamaUrl;
|
||||
_client?.Dispose();
|
||||
_client = new OllamaApiClient(baseUri: new Uri(ollamaUrl));
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<(bool Success, long LatencyMs)> TestConnectionAsync()
|
||||
private OllamaApiClient Client => _client ??= new OllamaApiClient(baseUri: new Uri(_currentUrl));
|
||||
|
||||
public async Task<IEnumerable<Model>> GetLocalModelsAsync()
|
||||
{
|
||||
_opState.SetThought($"Teste Verbindung zu {ActiveBackend.Name}...");
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var result = await ActiveBackend.TestConnectionAsync();
|
||||
sw.Stop();
|
||||
_opState.AppendThought(result ? " OK" : " Fehlgeschlagen");
|
||||
return (result, result ? sw.ElapsedMilliseconds : 0);
|
||||
var models = await Client.Models.ListModelsAsync();
|
||||
return models.Models ?? Enumerable.Empty<Ollama.Model>();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<LLM_Model>> GetLocalModelsAsync()
|
||||
public async Task PullModelAsync(string modelName, IProgress<ModelStatus> progress)
|
||||
{
|
||||
_opState.SetThought($"Lade Modelle von {ActiveBackend.Name}...");
|
||||
var models = await ActiveBackend.GetModelsAsync();
|
||||
_opState.AppendThought($" {models.Count()} gefunden");
|
||||
return models;
|
||||
var p = new ModelStatus();
|
||||
await foreach (var response in Client.Models.PullModelAsync(modelName))
|
||||
{
|
||||
p.SetModelStatus(response);
|
||||
progress?.Report(p);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> TestEmbeddingAsync(string model)
|
||||
public async Task<IList<double>> GetEmbeddingAsync(string text)
|
||||
{
|
||||
return ActiveBackend.TestEmbedding(model);
|
||||
var response = await Client.Embeddings.GenerateEmbeddingAsync(
|
||||
model: _settings.EmbedModel,
|
||||
prompt: text);
|
||||
return response.Embedding ?? Array.Empty<double>();
|
||||
}
|
||||
|
||||
public async Task<float[]> GetEmbeddingAsync(string text)
|
||||
public async Task<string> StreamStyleCheckAsync(string input)
|
||||
{
|
||||
return await ActiveBackend.GetEmbeddingAsync(text, _settings.EmbedModel);
|
||||
}
|
||||
|
||||
public async Task<string> QuickSpellCheckAsync(string text)
|
||||
{
|
||||
return await Chat(_settings.SpellingModel, text, _settings.SpellingPrompt, _settings.SpellingTemp);
|
||||
}
|
||||
|
||||
public async Task<string> StreamStyleCheckAsync(string text)
|
||||
{
|
||||
return await Chat(_settings.StyleModel, text, _settings.LektorSystemPrompt, _settings.LektorTemp);
|
||||
var systemPrompt = _settings.LektorSystemPrompt;
|
||||
var chat = Client.Chat(_settings.ChatModel, systemPrompt);
|
||||
var message = await chat.SendAsync(input);
|
||||
return message.Content;
|
||||
}
|
||||
|
||||
public async Task<string> AnswerBasedOnFactsAsync(string question, string facts)
|
||||
{
|
||||
var prompt = _settings.GetFactCheckPrompt(question, facts);
|
||||
return await Chat(_settings.EmbedModel, prompt, temperature: _settings.FactCheckTemp);
|
||||
var chat = Client.Chat(_settings.ChatModel);
|
||||
var message = await chat.SendAsync(prompt);
|
||||
return message.Content;
|
||||
}
|
||||
|
||||
public async Task<bool> ModelExistsAsync(string model)
|
||||
public async Task<string> QuickSpellCheckAsync(string text)
|
||||
{
|
||||
return await ActiveBackend.ModelExistsAsync(model);
|
||||
var systemPrompt = _settings.SpellingPrompt;
|
||||
var chat = Client.Chat(_settings.SpellingModel, systemPrompt);
|
||||
var message = await chat.SendAsync(text);
|
||||
return message.Content;
|
||||
}
|
||||
|
||||
public async Task<(bool Success, int ContextWindow)> GetModelContextWindowAsync(string model)
|
||||
public void Dispose()
|
||||
{
|
||||
return await ActiveBackend.GetModelContextWindowAsync(model);
|
||||
}
|
||||
|
||||
public async Task<long> MeasureLatencyAsync(string model)
|
||||
{
|
||||
return await ActiveBackend.MeasureLatencyAsync(model);
|
||||
}
|
||||
|
||||
public async Task<string> Chat(string model, string userMessage, string? systemPrompt = null, double? temperature = null)
|
||||
{
|
||||
_opState.SetThought($"{ActiveBackend.Name}: Sende Anfrage an {model}...");
|
||||
var result = await ActiveBackend.ChatAsync(model, userMessage, systemPrompt, temperature);
|
||||
_opState.AppendThought(" Fertig");
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task ChatStream(string model, string userMessage, Func<string, Task> onToken, string? systemPrompt = null, double? temperature = null)
|
||||
{
|
||||
_opState.SetThought($"{ActiveBackend.Name}: Verbinde mit Modell {model}...");
|
||||
|
||||
int tokenCount = 0;
|
||||
await ActiveBackend.ChatStreamAsync(model, userMessage, async chunk =>
|
||||
{
|
||||
tokenCount++;
|
||||
_opState.AppendThought(chunk);
|
||||
await onToken(chunk);
|
||||
}, systemPrompt, temperature);
|
||||
|
||||
_opState.AppendThought($" Fertig ({tokenCount} Token)");
|
||||
}
|
||||
|
||||
public async Task<bool> PullModelAsync(string modelName, IProgress<string> progress)
|
||||
{
|
||||
_opState.SetThought($"Lade Modell {modelName}...");
|
||||
var result = await ActiveBackend.PullModelAsync(modelName, progress);
|
||||
_opState.AppendThought(result ? " Erfolgreich" : " Fehlgeschlagen");
|
||||
return result;
|
||||
_client?.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,123 +0,0 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using AuthorBuddy.Web.Models;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public class OllamaSetupService : IOllamaSetupService
|
||||
{
|
||||
private readonly IOllamaService _ollamaService;
|
||||
private readonly OperationState _opState;
|
||||
private readonly OllamaSettings _currentSettings;
|
||||
|
||||
public OllamaSetupService(IOllamaService ollamaService, OperationState opState, OllamaSettings currentSettings)
|
||||
{
|
||||
_ollamaService = ollamaService;
|
||||
_opState = opState;
|
||||
_currentSettings = currentSettings;
|
||||
}
|
||||
|
||||
public async Task<SetupRecommendation> AnalyzeSettingsAsync()
|
||||
{
|
||||
var recommendation = new SetupRecommendation();
|
||||
var recommended = new OllamaSettings();
|
||||
|
||||
_opState.SetStatus("Überprüfe Ollama-Einstellungen...", true);
|
||||
_opState.SetThought("");
|
||||
|
||||
// OS Detection
|
||||
string os = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Windows" :
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "Linux" : "Unbekannt";
|
||||
|
||||
_opState.AppendThought($"System erkannt: {os}");
|
||||
recommendation.Findings.Add($"Betriebssystem: {os}");
|
||||
|
||||
// Connection Check
|
||||
_opState.AppendThought("Prüfe Verbindung zu Ollama...");
|
||||
var connection = await _ollamaService.TestConnectionAsync();
|
||||
if (connection.Success)
|
||||
{
|
||||
_opState.AppendThought("Verbindung erfolgreich.");
|
||||
recommendation.Findings.Add("Ollama ist erreichbar.");
|
||||
recommended.OllamaUrl = _currentSettings.OllamaUrl;
|
||||
}
|
||||
else
|
||||
{
|
||||
_opState.AppendThought("Verbindung fehlgeschlagen!");
|
||||
recommendation.Findings.Add("Ollama ist NICHT erreichbar. Bitte prüfen Sie, ob der Server läuft.");
|
||||
recommended.OllamaUrl = "http://localhost:11434";
|
||||
recommendation.IsCorrect = false;
|
||||
}
|
||||
|
||||
// Model Recommendations
|
||||
_opState.AppendThought("Analysiere Modell-Empfehlungen...");
|
||||
|
||||
// Recommendation logic
|
||||
recommended.EmbedModel = "nomic-embed-text";
|
||||
recommended.StyleModel = "gemma4:e4b";
|
||||
recommended.SpellingModel = "phi3:mini";
|
||||
|
||||
if (_currentSettings.EmbedModel != recommended.EmbedModel)
|
||||
{
|
||||
recommendation.Findings.Add($"Empfehlung: Nutze {recommended.EmbedModel} für Embeddings (aktuell: {_currentSettings.EmbedModel}).");
|
||||
recommendation.IsCorrect = false;
|
||||
}
|
||||
|
||||
if (_currentSettings.StyleModel != recommended.StyleModel)
|
||||
{
|
||||
recommendation.Findings.Add($"Empfehlung: Nutze {recommended.StyleModel} für Stil-Optimierung (aktuell: {_currentSettings.StyleModel}).");
|
||||
recommendation.IsCorrect = false;
|
||||
}
|
||||
|
||||
if (_currentSettings.SpellingModel != recommended.SpellingModel)
|
||||
{
|
||||
recommendation.Findings.Add($"Empfehlung: Nutze {recommended.SpellingModel} für Rechtschreibung (aktuell: {_currentSettings.SpellingModel}).");
|
||||
recommendation.IsCorrect = false;
|
||||
}
|
||||
|
||||
if (recommendation.IsCorrect == false)
|
||||
{
|
||||
recommendation.Message = "Es wurden Optimierungspotenziale in Ihren Ollama-Einstellungen gefunden.";
|
||||
}
|
||||
else
|
||||
{
|
||||
recommendation.IsCorrect = true;
|
||||
recommendation.Message = "Alle Ollama-Einstellungen sind optimal konfiguriert.";
|
||||
}
|
||||
|
||||
recommended.LektorSystemPrompt = _currentSettings.LektorSystemPrompt;
|
||||
recommended.FactCheckTemplate = _currentSettings.FactCheckTemplate;
|
||||
recommended.SpellingPrompt = _currentSettings.SpellingPrompt;
|
||||
recommended.Language = _currentSettings.Language;
|
||||
|
||||
_opState.SetStatus("Überprüfung abgeschlossen", false);
|
||||
recommendation.RecommendedSettings = recommended;
|
||||
|
||||
return recommendation;
|
||||
}
|
||||
|
||||
public async Task<bool> CreateConfigFileAsync(OllamaSettings settings)
|
||||
{
|
||||
try
|
||||
{
|
||||
_opState.SetStatus("Erstelle Konfigurationsdatei...", true);
|
||||
_opState.SetThought("Schreibe ollama_config.json in das App-Verzeichnis...");
|
||||
|
||||
var options = new JsonSerializerOptions { WriteIndented = true };
|
||||
var json = JsonSerializer.Serialize(settings, options);
|
||||
|
||||
// Writing to app root
|
||||
await File.WriteAllTextAsync("ollama_config.json", json);
|
||||
|
||||
_opState.SetStatus("Konfigurationsdatei erstellt", false);
|
||||
_opState.SetThought("Datei 'ollama_config.json' wurde erfolgreich gespeichert.");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_opState.SetStatus("Fehler beim Erstellen der Datei", false);
|
||||
_opState.SetThought($"Error: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public class OperationState
|
||||
{
|
||||
public string StatusText { get; private set; } = "Bereit";
|
||||
public bool IsBusy { get; private set; }
|
||||
public double Progress { get; private set; }
|
||||
public string ThoughtText { get; private set; } = "";
|
||||
|
||||
public event Action? OnChange;
|
||||
|
||||
public void SetStatus(string text, bool busy = false, double progress = 0)
|
||||
{
|
||||
StatusText = text;
|
||||
IsBusy = busy;
|
||||
Progress = progress;
|
||||
Notify();
|
||||
}
|
||||
|
||||
public void SetThought(string text)
|
||||
{
|
||||
ThoughtText = text;
|
||||
Notify();
|
||||
}
|
||||
|
||||
public void AppendThought(string text)
|
||||
{
|
||||
ThoughtText += text;
|
||||
Notify();
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
StatusText = "Bereit";
|
||||
IsBusy = false;
|
||||
Progress = 0;
|
||||
ThoughtText = "";
|
||||
Notify();
|
||||
}
|
||||
|
||||
private void Notify() => OnChange?.Invoke();
|
||||
}
|
||||
|
|
@ -1,54 +1,21 @@
|
|||
using AuthorBuddy.Web.Data;
|
||||
using AuthorBuddy.Web.Models;
|
||||
using LiteDB;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public class ProjectService : IProjectService
|
||||
{
|
||||
private readonly ILiteDatabase _db;
|
||||
private readonly IDbContextFactory<AppDbContext> _contextFactory;
|
||||
private readonly IFileScannerService _fileScannerService;
|
||||
|
||||
public ProjectService(ILiteDatabase db, IFileScannerService fileScannerService)
|
||||
public ProjectService(IDbContextFactory<AppDbContext> contextFactory, IFileScannerService fileScannerService)
|
||||
{
|
||||
_db = db;
|
||||
_contextFactory = contextFactory;
|
||||
_fileScannerService = fileScannerService;
|
||||
}
|
||||
|
||||
private ILiteCollection<FileCategoryEntity> CategoryCol =>
|
||||
_db.GetCollection<FileCategoryEntity>("fileCategories");
|
||||
|
||||
public Task<int> CreateNewProjectAsync(string projectName, string rootPath, int userId, ProjectTemplate template = ProjectTemplate.Novel)
|
||||
{
|
||||
if (template == ProjectTemplate.Technical)
|
||||
{
|
||||
string[] subFolders = { "00_Anforderungen", "01_UseCases", "02_Architektur", "03_Glossar" };
|
||||
foreach (var subFolder in subFolders)
|
||||
{
|
||||
Directory.CreateDirectory(Path.Combine(rootPath, subFolder));
|
||||
}
|
||||
|
||||
string readmePath = Path.Combine(rootPath, "00_Projekt_Info.md");
|
||||
File.WriteAllText(readmePath, $"# Projekt: {projectName}\nErstellt am: {DateTime.Now}\nTyp: Technische Vorlage (Requirements)");
|
||||
|
||||
string lastenheftPath = Path.Combine(rootPath, "Lastenheft.md");
|
||||
File.WriteAllText(lastenheftPath, $"# {projectName} – Lastenheft\n\n" +
|
||||
"## 1. Zielbestimmung\n\n" +
|
||||
"## 2. Produkteinsatz\n\n" +
|
||||
"## 3. Produktumgebung\n\n" +
|
||||
"## 4. Funktionale Anforderungen\n\n" +
|
||||
"## 5. Nicht-funktionale Anforderungen\n\n" +
|
||||
"## 6. Lieferumfang\n\n");
|
||||
|
||||
string pflichtenheftPath = Path.Combine(rootPath, "Pflichtenheft.md");
|
||||
File.WriteAllText(pflichtenheftPath, $"# {projectName} – Pflichtenheft\n\n" +
|
||||
"## 1. Systemarchitektur\n\n" +
|
||||
"## 2. Komponentenbeschreibung\n\n" +
|
||||
"## 3. Schnittstellendefinition\n\n" +
|
||||
"## 4. Datenmodell\n\n" +
|
||||
"## 5. Technische Umsetzung\n\n");
|
||||
}
|
||||
else
|
||||
public async Task<int> CreateNewProjectAsync(string projectName, string rootPath)
|
||||
{
|
||||
string[] subFolders = { "00_Drafts", "01_Fakten", "02_Characters", "03_Archive" };
|
||||
foreach (var subFolder in subFolders)
|
||||
|
|
@ -57,219 +24,40 @@ public class ProjectService : IProjectService
|
|||
}
|
||||
|
||||
string readmePath = Path.Combine(rootPath, "00_Projekt_Info.md");
|
||||
File.WriteAllText(readmePath, $"# Projekt: {projectName}\nErstellt am: {DateTime.Now}");
|
||||
await File.WriteAllTextAsync(readmePath, $"# Projekt: {projectName}\nErstellt am: {DateTime.Now}");
|
||||
|
||||
string exposeePath = Path.Combine(rootPath, "Die Handlung (Das Exposé).md");
|
||||
File.WriteAllText(exposeePath, $"# {projectName} – Die Handlung (Das Exposé)\n\n" +
|
||||
"## Überblick\n\n" +
|
||||
"## Erster Akt (Aufbau)\n\n" +
|
||||
"## Zweiter Akt (Konfrontation)\n\n" +
|
||||
"## Dritter Akt (Auflösung)\n\n" +
|
||||
"## Wichtige Wendepunkte\n\n" +
|
||||
"## Charakterbögen\n\n");
|
||||
await using var context = await _contextFactory.CreateDbContextAsync();
|
||||
|
||||
string ideasPath = Path.Combine(rootPath, "Ideen Sammlung.md");
|
||||
File.WriteAllText(ideasPath, $"# Ideen Sammlung – {projectName}\n\n");
|
||||
}
|
||||
|
||||
var col = _db.GetCollection<ProjectEntity>("projects");
|
||||
var entity = new ProjectEntity
|
||||
{
|
||||
Name = projectName,
|
||||
RootPath = rootPath,
|
||||
UserId = userId,
|
||||
LastOpened = DateTime.Now,
|
||||
Template = template
|
||||
LastOpened = DateTime.Now
|
||||
};
|
||||
|
||||
col.Insert(entity);
|
||||
return Task.FromResult(entity.Id);
|
||||
context.Projects.Add(entity);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
return entity.Id;
|
||||
}
|
||||
|
||||
public Task<List<Project>> GetAllProjectsAsync(int? userId = null)
|
||||
public async Task<List<Project>> GetAllProjectsAsync()
|
||||
{
|
||||
var col = _db.GetCollection<ProjectEntity>("projects");
|
||||
var query = userId == null
|
||||
? col.FindAll()
|
||||
: col.Find(p => p.UserId == userId);
|
||||
var list = query
|
||||
await using var context = await _contextFactory.CreateDbContextAsync();
|
||||
|
||||
return await context.Projects
|
||||
.OrderByDescending(p => p.LastOpened)
|
||||
.Select(p => new Project
|
||||
{
|
||||
Id = p.Id,
|
||||
Name = p.Name,
|
||||
RootPath = p.RootPath,
|
||||
Template = p.Template
|
||||
RootPath = p.RootPath
|
||||
})
|
||||
.ToList();
|
||||
return Task.FromResult(list);
|
||||
}
|
||||
|
||||
public Task<Project?> GetProjectAsync(int projectId)
|
||||
{
|
||||
var col = _db.GetCollection<ProjectEntity>("projects");
|
||||
var entity = col.FindById(projectId);
|
||||
if (entity == null) return Task.FromResult<Project?>(null);
|
||||
return Task.FromResult<Project?>(new Project
|
||||
{
|
||||
Id = entity.Id,
|
||||
Name = entity.Name,
|
||||
RootPath = entity.RootPath,
|
||||
Template = entity.Template
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<List<FileTreeItem>> GetProjectFileTreeAsync(int projectId)
|
||||
{
|
||||
var col = _db.GetCollection<ProjectEntity>("projects");
|
||||
var entity = col.FindById(projectId);
|
||||
if (entity == null || !Directory.Exists(entity.RootPath))
|
||||
return new List<FileTreeItem>();
|
||||
|
||||
var categories = await GetFileCategoriesAsync(projectId);
|
||||
var result = new List<FileTreeItem>();
|
||||
BuildTree(entity.RootPath, result, 0, categories);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void BuildTree(string directory, List<FileTreeItem> items, int depth, Dictionary<string, FileCategory> categories)
|
||||
{
|
||||
if (depth > 4) return;
|
||||
|
||||
var dirInfo = new DirectoryInfo(directory);
|
||||
if (!dirInfo.Exists) return;
|
||||
|
||||
foreach (var dir in dirInfo.GetDirectories().OrderBy(d => d.Name))
|
||||
{
|
||||
var node = new FileTreeItem
|
||||
{
|
||||
Name = dir.Name,
|
||||
FullPath = dir.FullName,
|
||||
IsDirectory = true
|
||||
};
|
||||
BuildTree(dir.FullName, node.Children, depth + 1, categories);
|
||||
if (node.Children.Count > 0 || dir.Name.StartsWith("00_") || dir.Name.StartsWith("01_") || dir.Name.StartsWith("02_"))
|
||||
items.Add(node);
|
||||
}
|
||||
|
||||
foreach (var file in dirInfo.GetFiles("*.md").OrderBy(f => f.Name))
|
||||
{
|
||||
categories.TryGetValue(file.FullName, out var cat);
|
||||
items.Add(new FileTreeItem
|
||||
{
|
||||
Name = file.Name,
|
||||
FullPath = file.FullName,
|
||||
IsDirectory = false,
|
||||
Category = cat
|
||||
});
|
||||
}
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task SyncProjectFilesAsync(int projectId, string directoryPath, IProgress<string> progress)
|
||||
{
|
||||
await _fileScannerService.ScanProjectFolderAsync(projectId, directoryPath, progress);
|
||||
}
|
||||
|
||||
public Task<int> CopyProjectAsync(int sourceProjectId, string newName, string newRootPath, int userId)
|
||||
{
|
||||
var col = _db.GetCollection<ProjectEntity>("projects");
|
||||
var source = col.FindById(sourceProjectId);
|
||||
if (source == null)
|
||||
throw new InvalidOperationException("Source project not found.");
|
||||
|
||||
Directory.CreateDirectory(newRootPath);
|
||||
CopyDirectory(source.RootPath, newRootPath);
|
||||
|
||||
var entity = new ProjectEntity
|
||||
{
|
||||
Name = newName,
|
||||
RootPath = newRootPath,
|
||||
UserId = userId,
|
||||
LastOpened = DateTime.Now,
|
||||
Template = source.Template
|
||||
};
|
||||
col.Insert(entity);
|
||||
|
||||
var settings = _db.GetCollection<SettingEntity>("settings");
|
||||
var projectSettings = settings.Find(s =>
|
||||
s.UserId == userId && s.ProjectId == sourceProjectId);
|
||||
foreach (var s in projectSettings)
|
||||
{
|
||||
settings.Insert(new SettingEntity
|
||||
{
|
||||
Key = s.Key,
|
||||
Value = s.Value,
|
||||
UserId = userId,
|
||||
ProjectId = entity.Id
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult(entity.Id);
|
||||
}
|
||||
|
||||
private static void CopyDirectory(string sourceDir, string destDir)
|
||||
{
|
||||
foreach (var dir in Directory.GetDirectories(sourceDir, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
Directory.CreateDirectory(dir.Replace(sourceDir, destDir));
|
||||
}
|
||||
foreach (var file in Directory.GetFiles(sourceDir, "*.*", SearchOption.AllDirectories))
|
||||
{
|
||||
var dest = file.Replace(sourceDir, destDir);
|
||||
File.Copy(file, dest, true);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<Dictionary<string, FileCategory>> GetFileCategoriesAsync(int projectId)
|
||||
{
|
||||
var entries = CategoryCol.Find(e => e.ProjectId == projectId).ToList();
|
||||
var result = new Dictionary<string, FileCategory>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var e in entries)
|
||||
result[e.FilePath] = e.Category;
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public Task<bool> SetFileCategoryAsync(int projectId, string filePath, FileCategory category)
|
||||
{
|
||||
if (!FileCategoryHelper.IsValid(category)) return Task.FromResult(false);
|
||||
|
||||
if (category.HasFlag(FileCategory.Exposee))
|
||||
{
|
||||
var existingExposee = CategoryCol.Find(e => e.ProjectId == projectId)
|
||||
.FirstOrDefault(e => e.Category.HasFlag(FileCategory.Exposee) && e.FilePath != filePath);
|
||||
if (existingExposee != null) return Task.FromResult(false);
|
||||
}
|
||||
|
||||
var col = CategoryCol;
|
||||
var existing = col.FindOne(e => e.ProjectId == projectId && e.FilePath == filePath);
|
||||
if (existing != null)
|
||||
{
|
||||
if (category == FileCategory.None)
|
||||
col.Delete(existing.Id);
|
||||
else
|
||||
{
|
||||
existing.Category = category;
|
||||
col.Update(existing);
|
||||
}
|
||||
}
|
||||
else if (category != FileCategory.None)
|
||||
{
|
||||
col.Insert(new FileCategoryEntity
|
||||
{
|
||||
ProjectId = projectId,
|
||||
FilePath = filePath,
|
||||
Category = category
|
||||
});
|
||||
}
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task ClearFileCategoryAsync(int projectId, string filePath)
|
||||
{
|
||||
var col = CategoryCol;
|
||||
var existing = col.FindOne(e => e.ProjectId == projectId && e.FilePath == filePath);
|
||||
if (existing != null)
|
||||
col.Delete(existing.Id);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,145 +0,0 @@
|
|||
using AuthorBuddy.Web.Data;
|
||||
using LiteDB;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public class RagService : IRagService
|
||||
{
|
||||
private readonly ILiteDatabase _db;
|
||||
private readonly IOllamaService _ollama;
|
||||
|
||||
public RagService(ILiteDatabase db, IOllamaService ollama)
|
||||
{
|
||||
_db = db;
|
||||
_ollama = ollama;
|
||||
}
|
||||
|
||||
public async Task IndexProjectAsync(int projectId, string rootPath, IProgress<string> progress)
|
||||
{
|
||||
var files = Directory.GetFiles(rootPath, "*.md", SearchOption.AllDirectories)
|
||||
.Where(f => f.Contains("00_Drafts") || f.Contains("01_Fakten"));
|
||||
|
||||
var metaCol = _db.GetCollection<DocumentMetaEntity>("documentMeta");
|
||||
var vecCol = _db.GetCollection<DocumentVector>("vectors");
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
progress?.Report($"Indexiere: {Path.GetFileName(file)}...");
|
||||
|
||||
string content = await File.ReadAllTextAsync(file);
|
||||
string hash = ComputeHash(content);
|
||||
|
||||
var meta = metaCol.FindOne(m => m.ProjectId == projectId && m.FilePath == file);
|
||||
|
||||
if (meta != null && meta.Hash == hash)
|
||||
{
|
||||
var existingVec = vecCol.FindOne(v => v.DocumentMetaId == meta.Id);
|
||||
if (existingVec != null)
|
||||
{
|
||||
progress?.Report($" ↳ unverändert, übersprungen");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var embedding = await _ollama.GetEmbeddingAsync(content);
|
||||
if (embedding.Length == 0)
|
||||
{
|
||||
progress?.Report($" ↳ Fehler beim Embedding, übersprungen");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (meta == null)
|
||||
{
|
||||
meta = new DocumentMetaEntity
|
||||
{
|
||||
ProjectId = projectId,
|
||||
FilePath = file,
|
||||
LastModified = File.GetLastWriteTimeUtc(file),
|
||||
Hash = hash,
|
||||
FileType = ".md"
|
||||
};
|
||||
metaCol.Insert(meta);
|
||||
}
|
||||
else
|
||||
{
|
||||
meta.LastModified = File.GetLastWriteTimeUtc(file);
|
||||
meta.Hash = hash;
|
||||
metaCol.Update(meta);
|
||||
}
|
||||
|
||||
var vec = vecCol.FindOne(v => v.DocumentMetaId == meta.Id);
|
||||
if (vec != null)
|
||||
{
|
||||
vec.Text = content;
|
||||
vec.Hash = hash;
|
||||
vec.Embedding = embedding;
|
||||
vecCol.Update(vec);
|
||||
}
|
||||
else
|
||||
{
|
||||
vecCol.Insert(new DocumentVector
|
||||
{
|
||||
DocumentMetaId = meta.Id,
|
||||
ProjectId = projectId,
|
||||
Text = content,
|
||||
Hash = hash,
|
||||
Embedding = embedding
|
||||
});
|
||||
}
|
||||
|
||||
progress?.Report($" ↳ ✓ {embedding.Length} Dimensionen");
|
||||
}
|
||||
|
||||
progress?.Report("Indexierung abgeschlossen.");
|
||||
}
|
||||
|
||||
public async Task<List<RagSearchResult>> SearchAsync(int projectId, string query, int topN = 5)
|
||||
{
|
||||
var queryVec = await _ollama.GetEmbeddingAsync(query);
|
||||
if (queryVec.Length == 0)
|
||||
return new List<RagSearchResult>();
|
||||
|
||||
var vecCol = _db.GetCollection<DocumentVector>("vectors");
|
||||
var metaCol = _db.GetCollection<DocumentMetaEntity>("documentMeta");
|
||||
|
||||
var vectors = vecCol.Find(v => v.ProjectId == projectId).ToList();
|
||||
var results = new List<RagSearchResult>();
|
||||
|
||||
foreach (var vec in vectors)
|
||||
{
|
||||
var sim = CosineSimilarity(queryVec, vec.Embedding);
|
||||
if (sim > 0)
|
||||
{
|
||||
var meta = metaCol.FindById(vec.DocumentMetaId);
|
||||
results.Add(new RagSearchResult
|
||||
{
|
||||
Text = vec.Text.Length > 500 ? vec.Text[..500] + "..." : vec.Text,
|
||||
FilePath = meta?.FilePath ?? "",
|
||||
Similarity = sim
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results.OrderByDescending(r => r.Similarity).Take(topN).ToList();
|
||||
}
|
||||
|
||||
private static string ComputeHash(string content)
|
||||
{
|
||||
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(content));
|
||||
return Convert.ToHexStringLower(bytes);
|
||||
}
|
||||
|
||||
private static double CosineSimilarity(float[] a, float[] b)
|
||||
{
|
||||
double dotProduct = 0.0, l2A = 0.0, l2B = 0.0;
|
||||
for (int i = 0; i < a.Length; i++)
|
||||
{
|
||||
dotProduct += a[i] * b[i];
|
||||
l2A += Math.Pow(a[i], 2);
|
||||
l2B += Math.Pow(b[i], 2);
|
||||
}
|
||||
return dotProduct / (Math.Sqrt(l2A) * Math.Sqrt(l2B));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,790 +0,0 @@
|
|||
using AuthorBuddy.Web.Data;
|
||||
using AuthorBuddy.Web.Models;
|
||||
using LiteDB;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public class SettingsService : ISettingsService
|
||||
{
|
||||
private const string KeyOllamaUrl = "ollama_url";
|
||||
private const string KeyLlamaCppUrl = "llama_cpp_url";
|
||||
private const string KeyBackend = "llm_backend";
|
||||
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 const string KeyAdvocatusDiaboli = "ollama_advocatus_diaboli";
|
||||
private const string KeyHookFinder = "ollama_hook_finder";
|
||||
private const string KeyEli5 = "ollama_eli5";
|
||||
private const string KeyCoAuthor = "ollama_coauthor";
|
||||
private const string KeyLektorTemp = "ollama_lektor_temp";
|
||||
private const string KeyFactCheckTemp = "ollama_fact_check_temp";
|
||||
private const string KeySpellingTemp = "ollama_spelling_temp";
|
||||
private const string KeyAdvocatusDiaboliTemp = "ollama_advocatus_diaboli_temp";
|
||||
private const string KeyHookFinderTemp = "ollama_hook_finder_temp";
|
||||
private const string KeyEli5Temp = "ollama_eli5_temp";
|
||||
private const string KeyCoAuthorTemp = "ollama_coauthor_temp";
|
||||
private const string KeyTechLastenheft = "tech_lastenheft";
|
||||
private const string KeyTechPflichtenheft = "tech_pflichtenheft";
|
||||
private const string KeyTechUseCase = "tech_usecase";
|
||||
private const string KeyTechStakeholder = "tech_stakeholder";
|
||||
private const string KeyTechGlossar = "tech_glossar";
|
||||
private const string KeyTechQualitaet = "tech_qualitaet";
|
||||
private const string KeyTechRisiko = "tech_risiko";
|
||||
private const string KeyTechLastenheftTemp = "tech_lastenheft_temp";
|
||||
private const string KeyTechPflichtenheftTemp = "tech_pflichtenheft_temp";
|
||||
private const string KeyTechUseCaseTemp = "tech_usecase_temp";
|
||||
private const string KeyTechStakeholderTemp = "tech_stakeholder_temp";
|
||||
private const string KeyTechGlossarTemp = "tech_glossar_temp";
|
||||
private const string KeyTechQualitaetTemp = "tech_qualitaet_temp";
|
||||
private const string KeyTechRisikoTemp = "tech_risiko_temp";
|
||||
private const string KeyLanguage = "language";
|
||||
|
||||
private const string KeyLastProjectId = "last_project_id";
|
||||
|
||||
private readonly ILiteDatabase _db;
|
||||
private readonly OllamaSettings _ollamaConfig;
|
||||
private int? _currentUserId;
|
||||
private int? _currentProjectId;
|
||||
|
||||
public SettingsService(ILiteDatabase db, OllamaSettings ollamaConfig)
|
||||
{
|
||||
_db = db;
|
||||
_ollamaConfig = ollamaConfig;
|
||||
}
|
||||
|
||||
public int? CurrentProjectId
|
||||
{
|
||||
get => _currentProjectId;
|
||||
set => _currentProjectId = value;
|
||||
}
|
||||
|
||||
public string? CurrentFilePath { get; set; }
|
||||
public string? PendingAssistantInput { get; set; }
|
||||
public string? PendingAssistantOutput { get; set; }
|
||||
|
||||
public Task<int?> GetLastProjectIdAsync()
|
||||
{
|
||||
var col = _db.GetCollection<SettingEntity>("settings");
|
||||
var setting = col.FindOne(s => s.Key == KeyLastProjectId && s.UserId == _currentUserId && s.ProjectId == null);
|
||||
if (setting != null && int.TryParse(setting.Value, out var id))
|
||||
return Task.FromResult<int?>(id);
|
||||
return Task.FromResult<int?>(null);
|
||||
}
|
||||
|
||||
public Task SaveLastProjectIdAsync(int projectId)
|
||||
{
|
||||
var col = _db.GetCollection<SettingEntity>("settings");
|
||||
var existing = col.FindOne(s => s.Key == KeyLastProjectId && s.UserId == _currentUserId && s.ProjectId == null);
|
||||
if (existing != null)
|
||||
{
|
||||
existing.Value = projectId.ToString();
|
||||
col.Update(existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
col.Insert(new SettingEntity
|
||||
{
|
||||
Key = KeyLastProjectId,
|
||||
Value = projectId.ToString(),
|
||||
UserId = _currentUserId,
|
||||
ProjectId = null
|
||||
});
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private string? GetSetting(string key, int? userId, int? projectId)
|
||||
{
|
||||
var col = _db.GetCollection<SettingEntity>("settings");
|
||||
|
||||
var matches = col.Find(s =>
|
||||
s.Key == key &&
|
||||
(s.UserId == userId || s.UserId == null) &&
|
||||
(s.ProjectId == projectId || s.ProjectId == null));
|
||||
|
||||
return matches
|
||||
.OrderByDescending(s => s.UserId != null)
|
||||
.ThenByDescending(s => s.ProjectId != null)
|
||||
.FirstOrDefault()?.Value;
|
||||
}
|
||||
|
||||
private string? Get(string key)
|
||||
{
|
||||
return GetSetting(key, _currentUserId, _currentProjectId);
|
||||
}
|
||||
|
||||
private string? GetGlobal(string key)
|
||||
{
|
||||
var col = _db.GetCollection<SettingEntity>("settings");
|
||||
return col.FindOne(s => s.Key == key && s.UserId == null && s.ProjectId == null)?.Value;
|
||||
}
|
||||
|
||||
private void SetGlobal(string key, string value)
|
||||
{
|
||||
var col = _db.GetCollection<SettingEntity>("settings");
|
||||
var existing = col.FindOne(s => s.Key == key && s.UserId == null && s.ProjectId == null);
|
||||
if (existing != null)
|
||||
{
|
||||
existing.Value = value;
|
||||
col.Update(existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
col.Insert(new SettingEntity { Key = key, Value = value, UserId = null, ProjectId = null });
|
||||
}
|
||||
}
|
||||
|
||||
private void Set(string key, string value)
|
||||
{
|
||||
var col = _db.GetCollection<SettingEntity>("settings");
|
||||
|
||||
var existing = col.FindOne(s =>
|
||||
s.Key == key && s.UserId == _currentUserId && s.ProjectId == null);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
existing.Value = value;
|
||||
col.Update(existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
col.Insert(new SettingEntity
|
||||
{
|
||||
Key = key,
|
||||
Value = value,
|
||||
UserId = _currentUserId,
|
||||
ProjectId = null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public Task<string?> GetOllamaUrlAsync() => Task.FromResult(GetGlobal(KeyOllamaUrl));
|
||||
public Task<string?> GetLlamaCppUrlAsync() => Task.FromResult(GetGlobal(KeyLlamaCppUrl));
|
||||
public Task<string?> GetBackendAsync() => Task.FromResult(GetGlobal(KeyBackend));
|
||||
public Task<string?> GetRagModelAsync() => Task.FromResult(GetGlobal(KeyRagModel));
|
||||
public Task<string?> GetStyleModelAsync() => Task.FromResult(GetGlobal(KeyStyleModel));
|
||||
public Task<string?> GetLektorSystemPromptAsync() => Task.FromResult(Get(KeyLektorPrompt));
|
||||
public Task<string?> GetFactCheckTemplateAsync() => Task.FromResult(Get(KeyFactCheckTemplate));
|
||||
public Task<string?> GetSpellingPromptAsync() => Task.FromResult(Get(KeySpellingPrompt));
|
||||
public Task<string?> GetSpellingModelAsync() => Task.FromResult(GetGlobal(KeySpellingModel));
|
||||
public Task<string?> GetAdvocatusDiaboliPromptAsync() => Task.FromResult(Get(KeyAdvocatusDiaboli));
|
||||
public Task<string?> GetHookFinderPromptAsync() => Task.FromResult(Get(KeyHookFinder));
|
||||
public Task<string?> GetEli5PromptAsync() => Task.FromResult(Get(KeyEli5));
|
||||
public Task<string?> GetCoAuthorBrainstormingPromptAsync() => Task.FromResult(Get(KeyCoAuthor));
|
||||
public Task<double> GetLektorTempAsync() => Task.FromResult(double.TryParse(Get(KeyLektorTemp), out var v) ? v : _ollamaConfig.LektorTemp);
|
||||
public Task<double> GetFactCheckTempAsync() => Task.FromResult(double.TryParse(Get(KeyFactCheckTemp), out var v) ? v : _ollamaConfig.FactCheckTemp);
|
||||
public Task<double> GetSpellingTempAsync() => Task.FromResult(double.TryParse(Get(KeySpellingTemp), out var v) ? v : _ollamaConfig.SpellingTemp);
|
||||
public Task<double> GetAdvocatusDiaboliTempAsync() => Task.FromResult(double.TryParse(Get(KeyAdvocatusDiaboliTemp), out var v) ? v : _ollamaConfig.AdvocatusDiaboliTemp);
|
||||
public Task<double> GetHookFinderTempAsync() => Task.FromResult(double.TryParse(Get(KeyHookFinderTemp), out var v) ? v : _ollamaConfig.HookFinderTemp);
|
||||
public Task<double> GetEli5TempAsync() => Task.FromResult(double.TryParse(Get(KeyEli5Temp), out var v) ? v : _ollamaConfig.Eli5Temp);
|
||||
public Task<double> GetCoAuthorTempAsync() => Task.FromResult(double.TryParse(Get(KeyCoAuthorTemp), out var v) ? v : _ollamaConfig.CoAuthorTemp);
|
||||
|
||||
public Task<string?> GetTechLastenheftPromptAsync() => Task.FromResult(Get(KeyTechLastenheft));
|
||||
public Task<string?> GetTechPflichtenheftPromptAsync() => Task.FromResult(Get(KeyTechPflichtenheft));
|
||||
public Task<string?> GetTechUseCasePromptAsync() => Task.FromResult(Get(KeyTechUseCase));
|
||||
public Task<string?> GetTechStakeholderPromptAsync() => Task.FromResult(Get(KeyTechStakeholder));
|
||||
public Task<string?> GetTechGlossarPromptAsync() => Task.FromResult(Get(KeyTechGlossar));
|
||||
public Task<string?> GetTechQualitaetPromptAsync() => Task.FromResult(Get(KeyTechQualitaet));
|
||||
public Task<string?> GetTechRisikoPromptAsync() => Task.FromResult(Get(KeyTechRisiko));
|
||||
public Task<double> GetTechLastenheftTempAsync() => Task.FromResult(double.TryParse(Get(KeyTechLastenheftTemp), out var v) ? v : _ollamaConfig.TechLastenheftTemp);
|
||||
public Task<double> GetTechPflichtenheftTempAsync() => Task.FromResult(double.TryParse(Get(KeyTechPflichtenheftTemp), out var v) ? v : _ollamaConfig.TechPflichtenheftTemp);
|
||||
public Task<double> GetTechUseCaseTempAsync() => Task.FromResult(double.TryParse(Get(KeyTechUseCaseTemp), out var v) ? v : _ollamaConfig.TechUseCaseTemp);
|
||||
public Task<double> GetTechStakeholderTempAsync() => Task.FromResult(double.TryParse(Get(KeyTechStakeholderTemp), out var v) ? v : _ollamaConfig.TechStakeholderTemp);
|
||||
public Task<double> GetTechGlossarTempAsync() => Task.FromResult(double.TryParse(Get(KeyTechGlossarTemp), out var v) ? v : _ollamaConfig.TechGlossarTemp);
|
||||
public Task<double> GetTechQualitaetTempAsync() => Task.FromResult(double.TryParse(Get(KeyTechQualitaetTemp), out var v) ? v : _ollamaConfig.TechQualitaetTemp);
|
||||
public Task<double> GetTechRisikoTempAsync() => Task.FromResult(double.TryParse(Get(KeyTechRisikoTemp), out var v) ? v : _ollamaConfig.TechRisikoTemp);
|
||||
public Task<string?> GetThemeAsync() => Task.FromResult(Get(KeyTheme));
|
||||
public Task<string?> GetLanguageAsync() => Task.FromResult(Get(KeyLanguage));
|
||||
public Task SaveLanguageAsync(string language)
|
||||
{
|
||||
Set(KeyLanguage, language);
|
||||
_ollamaConfig.Language = language;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveOllamaUrlAsync(string url)
|
||||
{
|
||||
SetGlobal(KeyOllamaUrl, url);
|
||||
_ollamaConfig.OllamaUrl = url;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveLlamaCppUrlAsync(string url)
|
||||
{
|
||||
SetGlobal(KeyLlamaCppUrl, url);
|
||||
_ollamaConfig.LlamaCppUrl = url;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveBackendAsync(string backend)
|
||||
{
|
||||
SetGlobal(KeyBackend, backend);
|
||||
_ollamaConfig.Backend = backend;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveRagModelAsync(string model)
|
||||
{
|
||||
SetGlobal(KeyRagModel, model);
|
||||
_ollamaConfig.EmbedModel = model;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveStyleModelAsync(string model)
|
||||
{
|
||||
SetGlobal(KeyStyleModel, model);
|
||||
_ollamaConfig.StyleModel = model;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveLektorSystemPromptAsync(string prompt)
|
||||
{
|
||||
Set(KeyLektorPrompt, prompt);
|
||||
_ollamaConfig.LektorSystemPrompt = prompt;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveFactCheckTemplateAsync(string prompt)
|
||||
{
|
||||
Set(KeyFactCheckTemplate, prompt);
|
||||
_ollamaConfig.FactCheckTemplate = prompt;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveSpellingPromptAsync(string prompt)
|
||||
{
|
||||
Set(KeySpellingPrompt, prompt);
|
||||
_ollamaConfig.SpellingPrompt = prompt;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveSpellingModelAsync(string model)
|
||||
{
|
||||
SetGlobal(KeySpellingModel, model);
|
||||
_ollamaConfig.SpellingModel = model;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveAdvocatusDiaboliPromptAsync(string prompt)
|
||||
{
|
||||
Set(KeyAdvocatusDiaboli, prompt);
|
||||
_ollamaConfig.AdvocatusDiaboliPrompt = prompt;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveHookFinderPromptAsync(string prompt)
|
||||
{
|
||||
Set(KeyHookFinder, prompt);
|
||||
_ollamaConfig.HookFinderPrompt = prompt;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveEli5PromptAsync(string prompt)
|
||||
{
|
||||
Set(KeyEli5, prompt);
|
||||
_ollamaConfig.Eli5Prompt = prompt;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveCoAuthorBrainstormingPromptAsync(string prompt)
|
||||
{
|
||||
Set(KeyCoAuthor, prompt);
|
||||
_ollamaConfig.CoAuthorBrainstormingPrompt = prompt;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveLektorTempAsync(double temp)
|
||||
{
|
||||
Set(KeyLektorTemp, temp.ToString("F1"));
|
||||
_ollamaConfig.LektorTemp = temp;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveFactCheckTempAsync(double temp)
|
||||
{
|
||||
Set(KeyFactCheckTemp, temp.ToString("F1"));
|
||||
_ollamaConfig.FactCheckTemp = temp;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveSpellingTempAsync(double temp)
|
||||
{
|
||||
Set(KeySpellingTemp, temp.ToString("F1"));
|
||||
_ollamaConfig.SpellingTemp = temp;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveAdvocatusDiaboliTempAsync(double temp)
|
||||
{
|
||||
Set(KeyAdvocatusDiaboliTemp, temp.ToString("F1"));
|
||||
_ollamaConfig.AdvocatusDiaboliTemp = temp;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveHookFinderTempAsync(double temp)
|
||||
{
|
||||
Set(KeyHookFinderTemp, temp.ToString("F1"));
|
||||
_ollamaConfig.HookFinderTemp = temp;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveEli5TempAsync(double temp)
|
||||
{
|
||||
Set(KeyEli5Temp, temp.ToString("F1"));
|
||||
_ollamaConfig.Eli5Temp = temp;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveCoAuthorTempAsync(double temp)
|
||||
{
|
||||
Set(KeyCoAuthorTemp, temp.ToString("F1"));
|
||||
_ollamaConfig.CoAuthorTemp = temp;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveTechLastenheftPromptAsync(string prompt)
|
||||
{
|
||||
Set(KeyTechLastenheft, prompt);
|
||||
_ollamaConfig.TechLastenheftPrompt = prompt;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveTechPflichtenheftPromptAsync(string prompt)
|
||||
{
|
||||
Set(KeyTechPflichtenheft, prompt);
|
||||
_ollamaConfig.TechPflichtenheftPrompt = prompt;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveTechUseCasePromptAsync(string prompt)
|
||||
{
|
||||
Set(KeyTechUseCase, prompt);
|
||||
_ollamaConfig.TechUseCasePrompt = prompt;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveTechStakeholderPromptAsync(string prompt)
|
||||
{
|
||||
Set(KeyTechStakeholder, prompt);
|
||||
_ollamaConfig.TechStakeholderPrompt = prompt;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveTechGlossarPromptAsync(string prompt)
|
||||
{
|
||||
Set(KeyTechGlossar, prompt);
|
||||
_ollamaConfig.TechGlossarPrompt = prompt;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveTechQualitaetPromptAsync(string prompt)
|
||||
{
|
||||
Set(KeyTechQualitaet, prompt);
|
||||
_ollamaConfig.TechQualitaetPrompt = prompt;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveTechRisikoPromptAsync(string prompt)
|
||||
{
|
||||
Set(KeyTechRisiko, prompt);
|
||||
_ollamaConfig.TechRisikoPrompt = prompt;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveTechLastenheftTempAsync(double temp)
|
||||
{
|
||||
Set(KeyTechLastenheftTemp, temp.ToString("F1"));
|
||||
_ollamaConfig.TechLastenheftTemp = temp;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveTechPflichtenheftTempAsync(double temp)
|
||||
{
|
||||
Set(KeyTechPflichtenheftTemp, temp.ToString("F1"));
|
||||
_ollamaConfig.TechPflichtenheftTemp = temp;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveTechUseCaseTempAsync(double temp)
|
||||
{
|
||||
Set(KeyTechUseCaseTemp, temp.ToString("F1"));
|
||||
_ollamaConfig.TechUseCaseTemp = temp;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveTechStakeholderTempAsync(double temp)
|
||||
{
|
||||
Set(KeyTechStakeholderTemp, temp.ToString("F1"));
|
||||
_ollamaConfig.TechStakeholderTemp = temp;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveTechGlossarTempAsync(double temp)
|
||||
{
|
||||
Set(KeyTechGlossarTemp, temp.ToString("F1"));
|
||||
_ollamaConfig.TechGlossarTemp = temp;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveTechQualitaetTempAsync(double temp)
|
||||
{
|
||||
Set(KeyTechQualitaetTemp, temp.ToString("F1"));
|
||||
_ollamaConfig.TechQualitaetTemp = temp;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveTechRisikoTempAsync(double temp)
|
||||
{
|
||||
Set(KeyTechRisikoTemp, temp.ToString("F1"));
|
||||
_ollamaConfig.TechRisikoTemp = temp;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveThemeAsync(string theme)
|
||||
{
|
||||
Set(KeyTheme, theme);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<List<UserEntity>> GetUsersAsync()
|
||||
{
|
||||
var col = _db.GetCollection<UserEntity>("users");
|
||||
return Task.FromResult(col.FindAll().ToList());
|
||||
}
|
||||
|
||||
public Task<UserEntity> GetCurrentUserAsync()
|
||||
{
|
||||
var col = _db.GetCollection<UserEntity>("users");
|
||||
if (_currentUserId != null)
|
||||
{
|
||||
var user = col.FindById(_currentUserId.Value);
|
||||
if (user != null) return Task.FromResult(user);
|
||||
}
|
||||
return Task.FromResult(col.FindAll().FirstOrDefault() ?? new UserEntity());
|
||||
}
|
||||
|
||||
public void SetCurrentUser(int userId)
|
||||
{
|
||||
_currentUserId = userId;
|
||||
}
|
||||
|
||||
public Task<UserEntity> CreateUserAsync(string name)
|
||||
{
|
||||
var col = _db.GetCollection<UserEntity>("users");
|
||||
var hasUsers = col.FindAll().Any();
|
||||
var user = new UserEntity
|
||||
{
|
||||
Name = name,
|
||||
Role = hasUsers ? UserRole.Author : UserRole.Administrator
|
||||
};
|
||||
col.Insert(user);
|
||||
return Task.FromResult(user);
|
||||
}
|
||||
|
||||
private void MigrateSchema()
|
||||
{
|
||||
const string schemaKey = "_schema_version";
|
||||
var settings = _db.GetCollection<SettingEntity>("settings");
|
||||
var schemaSetting = settings.FindOne(s => s.Key == schemaKey && s.UserId == null && s.ProjectId == null);
|
||||
var currentVersion = int.TryParse(schemaSetting?.Value, out var v) ? v : 0;
|
||||
|
||||
if (currentVersion < 1)
|
||||
{
|
||||
var users = _db.GetCollection<UserEntity>("users");
|
||||
var all = users.FindAll().OrderBy(u => u.Id).ToList();
|
||||
if (all.Count > 0)
|
||||
{
|
||||
all[0].Role = UserRole.Administrator;
|
||||
foreach (var u in all)
|
||||
{
|
||||
u.IsActive = true;
|
||||
users.Update(u);
|
||||
}
|
||||
}
|
||||
|
||||
var authorExists = all.Any(u => u.Name == "Author");
|
||||
if (!authorExists)
|
||||
{
|
||||
var authorUser = new UserEntity
|
||||
{
|
||||
Name = "Author",
|
||||
PasswordHash = TotpHelper.HashPassword("author123"),
|
||||
TwoFactorEnabled = false,
|
||||
TwoFactorSecret = string.Empty,
|
||||
Role = UserRole.Author,
|
||||
IsActive = true
|
||||
};
|
||||
users.Insert(authorUser);
|
||||
}
|
||||
|
||||
var projects = _db.GetCollection<ProjectEntity>("projects");
|
||||
var orphaned = projects.Find(p => p.UserId == 0).ToList();
|
||||
if (orphaned.Count > 0)
|
||||
{
|
||||
var admin = users.FindAll().OrderBy(u => u.Id).FirstOrDefault();
|
||||
if (admin != null)
|
||||
{
|
||||
foreach (var p in orphaned)
|
||||
{
|
||||
p.UserId = admin.Id;
|
||||
projects.Update(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentVersion = 1;
|
||||
}
|
||||
|
||||
if (currentVersion < 2)
|
||||
{
|
||||
var llmKeys = new[] { KeyOllamaUrl, KeyRagModel, KeyStyleModel, KeySpellingModel };
|
||||
foreach (var key in llmKeys)
|
||||
{
|
||||
var perUser = settings.Find(s => s.Key == key && s.UserId != null).ToList();
|
||||
foreach (var s in perUser)
|
||||
settings.Delete(s.Id);
|
||||
|
||||
if (settings.FindOne(s => s.Key == key && s.UserId == null && s.ProjectId == null) == null)
|
||||
{
|
||||
var defaultValue = key switch
|
||||
{
|
||||
var k when k == KeyOllamaUrl => _ollamaConfig.OllamaUrl,
|
||||
var k when k == KeyRagModel => _ollamaConfig.EmbedModel,
|
||||
var k when k == KeyStyleModel => _ollamaConfig.StyleModel,
|
||||
var k when k == KeySpellingModel => _ollamaConfig.SpellingModel,
|
||||
_ => ""
|
||||
};
|
||||
settings.Insert(new SettingEntity { Key = key, Value = defaultValue, UserId = null, ProjectId = null });
|
||||
}
|
||||
}
|
||||
currentVersion = 2;
|
||||
}
|
||||
|
||||
if (schemaSetting != null)
|
||||
{
|
||||
schemaSetting.Value = currentVersion.ToString();
|
||||
settings.Update(schemaSetting);
|
||||
}
|
||||
else
|
||||
{
|
||||
settings.Insert(new SettingEntity { Key = schemaKey, Value = currentVersion.ToString(), UserId = null, ProjectId = null });
|
||||
}
|
||||
}
|
||||
|
||||
public Task LoadProjectSettingsAsync()
|
||||
{
|
||||
var globalKeys = new[] { KeyOllamaUrl, KeyLlamaCppUrl, KeyBackend, KeyRagModel, KeyStyleModel, KeySpellingModel };
|
||||
|
||||
foreach (var key in globalKeys)
|
||||
{
|
||||
var val = GetGlobal(key);
|
||||
if (val == null) continue;
|
||||
switch (key)
|
||||
{
|
||||
case var k when k == KeyOllamaUrl: _ollamaConfig.OllamaUrl = val; break;
|
||||
case var k when k == KeyLlamaCppUrl: _ollamaConfig.LlamaCppUrl = val; break;
|
||||
case var k when k == KeyBackend: _ollamaConfig.Backend = val; break;
|
||||
case var k when k == KeyRagModel: _ollamaConfig.EmbedModel = val; break;
|
||||
case var k when k == KeyStyleModel: _ollamaConfig.StyleModel = val; break;
|
||||
case var k when k == KeySpellingModel: _ollamaConfig.SpellingModel = val; break;
|
||||
}
|
||||
}
|
||||
|
||||
var tempMap = new (string Key, Action<double> Setter)[]
|
||||
{
|
||||
(KeyLektorTemp, val => _ollamaConfig.LektorTemp = val),
|
||||
(KeyFactCheckTemp, val => _ollamaConfig.FactCheckTemp = val),
|
||||
(KeySpellingTemp, val => _ollamaConfig.SpellingTemp = val),
|
||||
(KeyAdvocatusDiaboliTemp, val => _ollamaConfig.AdvocatusDiaboliTemp = val),
|
||||
(KeyHookFinderTemp, val => _ollamaConfig.HookFinderTemp = val),
|
||||
(KeyEli5Temp, val => _ollamaConfig.Eli5Temp = val),
|
||||
(KeyCoAuthorTemp, val => _ollamaConfig.CoAuthorTemp = val),
|
||||
(KeyTechLastenheftTemp, val => _ollamaConfig.TechLastenheftTemp = val),
|
||||
(KeyTechPflichtenheftTemp, val => _ollamaConfig.TechPflichtenheftTemp = val),
|
||||
(KeyTechUseCaseTemp, val => _ollamaConfig.TechUseCaseTemp = val),
|
||||
(KeyTechStakeholderTemp, val => _ollamaConfig.TechStakeholderTemp = val),
|
||||
(KeyTechGlossarTemp, val => _ollamaConfig.TechGlossarTemp = val),
|
||||
(KeyTechQualitaetTemp, val => _ollamaConfig.TechQualitaetTemp = val),
|
||||
(KeyTechRisikoTemp, val => _ollamaConfig.TechRisikoTemp = val),
|
||||
};
|
||||
|
||||
foreach (var entry in tempMap)
|
||||
{
|
||||
var val = Get(entry.Key);
|
||||
if (val != null && double.TryParse(val, out var d))
|
||||
entry.Setter(d);
|
||||
}
|
||||
|
||||
var userEntries = new (string Key, Action<string> Setter)[]
|
||||
{
|
||||
(KeyTheme, val => _ollamaConfig.Theme = val),
|
||||
(KeyLektorPrompt, val => _ollamaConfig.LektorSystemPrompt = val),
|
||||
(KeyFactCheckTemplate, val => _ollamaConfig.FactCheckTemplate = val),
|
||||
(KeySpellingPrompt, val => _ollamaConfig.SpellingPrompt = val),
|
||||
(KeyAdvocatusDiaboli, val => _ollamaConfig.AdvocatusDiaboliPrompt = val),
|
||||
(KeyHookFinder, val => _ollamaConfig.HookFinderPrompt = val),
|
||||
(KeyEli5, val => _ollamaConfig.Eli5Prompt = val),
|
||||
(KeyCoAuthor, val => _ollamaConfig.CoAuthorBrainstormingPrompt = val),
|
||||
(KeyTechLastenheft, val => _ollamaConfig.TechLastenheftPrompt = val),
|
||||
(KeyTechPflichtenheft, val => _ollamaConfig.TechPflichtenheftPrompt = val),
|
||||
(KeyTechUseCase, val => _ollamaConfig.TechUseCasePrompt = val),
|
||||
(KeyTechStakeholder, val => _ollamaConfig.TechStakeholderPrompt = val),
|
||||
(KeyTechGlossar, val => _ollamaConfig.TechGlossarPrompt = val),
|
||||
(KeyTechQualitaet, val => _ollamaConfig.TechQualitaetPrompt = val),
|
||||
(KeyTechRisiko, val => _ollamaConfig.TechRisikoPrompt = val),
|
||||
(KeyLanguage, val => _ollamaConfig.Language = val),
|
||||
};
|
||||
|
||||
foreach (var entry in userEntries)
|
||||
{
|
||||
var val = Get(entry.Key);
|
||||
if (val != null) entry.Setter(val);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
{
|
||||
MigrateSchema();
|
||||
|
||||
var settings = _db.GetCollection<SettingEntity>("settings");
|
||||
|
||||
var globalEntries = new (string Key, string Default)[]
|
||||
{
|
||||
(KeyOllamaUrl, _ollamaConfig.OllamaUrl),
|
||||
(KeyLlamaCppUrl, _ollamaConfig.LlamaCppUrl),
|
||||
(KeyBackend, _ollamaConfig.Backend),
|
||||
(KeyRagModel, _ollamaConfig.EmbedModel),
|
||||
(KeyStyleModel, _ollamaConfig.StyleModel),
|
||||
(KeySpellingModel, _ollamaConfig.SpellingModel),
|
||||
};
|
||||
|
||||
foreach (var entry in globalEntries)
|
||||
{
|
||||
var existing = settings.FindOne(s =>
|
||||
s.Key == entry.Key && s.UserId == null && s.ProjectId == null);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
var val = existing.Value;
|
||||
if (entry.Key == KeyOllamaUrl) _ollamaConfig.OllamaUrl = val;
|
||||
else if (entry.Key == KeyLlamaCppUrl) _ollamaConfig.LlamaCppUrl = val;
|
||||
else if (entry.Key == KeyBackend) _ollamaConfig.Backend = val;
|
||||
else if (entry.Key == KeyRagModel) _ollamaConfig.EmbedModel = val;
|
||||
else if (entry.Key == KeyStyleModel) _ollamaConfig.StyleModel = val;
|
||||
else if (entry.Key == KeySpellingModel) _ollamaConfig.SpellingModel = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
settings.Insert(new SettingEntity
|
||||
{
|
||||
Key = entry.Key,
|
||||
Value = entry.Default,
|
||||
UserId = null,
|
||||
ProjectId = null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (_currentUserId == null)
|
||||
{
|
||||
var firstUser = _db.GetCollection<UserEntity>("users").FindAll().FirstOrDefault();
|
||||
if (firstUser == null) return Task.CompletedTask;
|
||||
_currentUserId = firstUser.Id;
|
||||
}
|
||||
|
||||
var lang = _ollamaConfig.Language;
|
||||
|
||||
var userEntries = new (string Key, string Default)[]
|
||||
{
|
||||
(KeyTheme, _ollamaConfig.Theme),
|
||||
(KeyLanguage, lang),
|
||||
(KeyLektorPrompt, lang == "en" ? _ollamaConfig.LektorSystemPromptEn : _ollamaConfig.LektorSystemPrompt),
|
||||
(KeyFactCheckTemplate, lang == "en" ? _ollamaConfig.FactCheckTemplateEn : _ollamaConfig.FactCheckTemplate),
|
||||
(KeySpellingPrompt, lang == "en" ? _ollamaConfig.SpellingPromptEn : _ollamaConfig.SpellingPrompt),
|
||||
(KeyAdvocatusDiaboli, lang == "en" ? _ollamaConfig.AdvocatusDiaboliPromptEn : _ollamaConfig.AdvocatusDiaboliPrompt),
|
||||
(KeyHookFinder, lang == "en" ? _ollamaConfig.HookFinderPromptEn : _ollamaConfig.HookFinderPrompt),
|
||||
(KeyEli5, lang == "en" ? _ollamaConfig.Eli5PromptEn : _ollamaConfig.Eli5Prompt),
|
||||
(KeyCoAuthor, lang == "en" ? _ollamaConfig.CoAuthorBrainstormingPromptEn : _ollamaConfig.CoAuthorBrainstormingPrompt),
|
||||
(KeyTechLastenheft, lang == "en" ? _ollamaConfig.TechLastenheftPromptEn : _ollamaConfig.TechLastenheftPrompt),
|
||||
(KeyTechPflichtenheft, lang == "en" ? _ollamaConfig.TechPflichtenheftPromptEn : _ollamaConfig.TechPflichtenheftPrompt),
|
||||
(KeyTechUseCase, lang == "en" ? _ollamaConfig.TechUseCasePromptEn : _ollamaConfig.TechUseCasePrompt),
|
||||
(KeyTechStakeholder, lang == "en" ? _ollamaConfig.TechStakeholderPromptEn : _ollamaConfig.TechStakeholderPrompt),
|
||||
(KeyTechGlossar, lang == "en" ? _ollamaConfig.TechGlossarPromptEn : _ollamaConfig.TechGlossarPrompt),
|
||||
(KeyTechQualitaet, lang == "en" ? _ollamaConfig.TechQualitaetPromptEn : _ollamaConfig.TechQualitaetPrompt),
|
||||
(KeyTechRisiko, lang == "en" ? _ollamaConfig.TechRisikoPromptEn : _ollamaConfig.TechRisikoPrompt),
|
||||
(KeyLektorTemp, _ollamaConfig.LektorTemp.ToString("F1")),
|
||||
(KeyFactCheckTemp, _ollamaConfig.FactCheckTemp.ToString("F1")),
|
||||
(KeySpellingTemp, _ollamaConfig.SpellingTemp.ToString("F1")),
|
||||
(KeyAdvocatusDiaboliTemp, _ollamaConfig.AdvocatusDiaboliTemp.ToString("F1")),
|
||||
(KeyHookFinderTemp, _ollamaConfig.HookFinderTemp.ToString("F1")),
|
||||
(KeyEli5Temp, _ollamaConfig.Eli5Temp.ToString("F1")),
|
||||
(KeyCoAuthorTemp, _ollamaConfig.CoAuthorTemp.ToString("F1")),
|
||||
(KeyTechLastenheftTemp, _ollamaConfig.TechLastenheftTemp.ToString("F1")),
|
||||
(KeyTechPflichtenheftTemp, _ollamaConfig.TechPflichtenheftTemp.ToString("F1")),
|
||||
(KeyTechUseCaseTemp, _ollamaConfig.TechUseCaseTemp.ToString("F1")),
|
||||
(KeyTechStakeholderTemp, _ollamaConfig.TechStakeholderTemp.ToString("F1")),
|
||||
(KeyTechGlossarTemp, _ollamaConfig.TechGlossarTemp.ToString("F1")),
|
||||
(KeyTechQualitaetTemp, _ollamaConfig.TechQualitaetTemp.ToString("F1")),
|
||||
(KeyTechRisikoTemp, _ollamaConfig.TechRisikoTemp.ToString("F1")),
|
||||
};
|
||||
|
||||
foreach (var entry in userEntries)
|
||||
{
|
||||
var existing = settings.FindOne(s =>
|
||||
s.Key == entry.Key && s.UserId == _currentUserId && s.ProjectId == _currentProjectId);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
var val = existing.Value;
|
||||
if (entry.Key == KeyTheme) _ollamaConfig.Theme = val;
|
||||
else if (entry.Key == KeyLanguage) _ollamaConfig.Language = 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 if (entry.Key == KeyAdvocatusDiaboli) _ollamaConfig.AdvocatusDiaboliPrompt = val;
|
||||
else if (entry.Key == KeyHookFinder) _ollamaConfig.HookFinderPrompt = val;
|
||||
else if (entry.Key == KeyEli5) _ollamaConfig.Eli5Prompt = val;
|
||||
else if (entry.Key == KeyCoAuthor) _ollamaConfig.CoAuthorBrainstormingPrompt = val;
|
||||
else if (entry.Key == KeyLektorTemp) { if (double.TryParse(val, out var t)) _ollamaConfig.LektorTemp = t; }
|
||||
else if (entry.Key == KeyFactCheckTemp) { if (double.TryParse(val, out var t)) _ollamaConfig.FactCheckTemp = t; }
|
||||
else if (entry.Key == KeySpellingTemp) { if (double.TryParse(val, out var t)) _ollamaConfig.SpellingTemp = t; }
|
||||
else if (entry.Key == KeyAdvocatusDiaboliTemp) { if (double.TryParse(val, out var t)) _ollamaConfig.AdvocatusDiaboliTemp = t; }
|
||||
else if (entry.Key == KeyHookFinderTemp) { if (double.TryParse(val, out var t)) _ollamaConfig.HookFinderTemp = t; }
|
||||
else if (entry.Key == KeyEli5Temp) { if (double.TryParse(val, out var t)) _ollamaConfig.Eli5Temp = t; }
|
||||
else if (entry.Key == KeyCoAuthorTemp) { if (double.TryParse(val, out var t)) _ollamaConfig.CoAuthorTemp = t; }
|
||||
else if (entry.Key == KeyTechLastenheft) _ollamaConfig.TechLastenheftPrompt = val;
|
||||
else if (entry.Key == KeyTechPflichtenheft) _ollamaConfig.TechPflichtenheftPrompt = val;
|
||||
else if (entry.Key == KeyTechUseCase) _ollamaConfig.TechUseCasePrompt = val;
|
||||
else if (entry.Key == KeyTechStakeholder) _ollamaConfig.TechStakeholderPrompt = val;
|
||||
else if (entry.Key == KeyTechGlossar) _ollamaConfig.TechGlossarPrompt = val;
|
||||
else if (entry.Key == KeyTechQualitaet) _ollamaConfig.TechQualitaetPrompt = val;
|
||||
else if (entry.Key == KeyTechRisiko) _ollamaConfig.TechRisikoPrompt = val;
|
||||
else if (entry.Key == KeyTechLastenheftTemp) { if (double.TryParse(val, out var t)) _ollamaConfig.TechLastenheftTemp = t; }
|
||||
else if (entry.Key == KeyTechPflichtenheftTemp) { if (double.TryParse(val, out var t)) _ollamaConfig.TechPflichtenheftTemp = t; }
|
||||
else if (entry.Key == KeyTechUseCaseTemp) { if (double.TryParse(val, out var t)) _ollamaConfig.TechUseCaseTemp = t; }
|
||||
else if (entry.Key == KeyTechStakeholderTemp) { if (double.TryParse(val, out var t)) _ollamaConfig.TechStakeholderTemp = t; }
|
||||
else if (entry.Key == KeyTechGlossarTemp) { if (double.TryParse(val, out var t)) _ollamaConfig.TechGlossarTemp = t; }
|
||||
else if (entry.Key == KeyTechQualitaetTemp) { if (double.TryParse(val, out var t)) _ollamaConfig.TechQualitaetTemp = t; }
|
||||
else if (entry.Key == KeyTechRisikoTemp) { if (double.TryParse(val, out var t)) _ollamaConfig.TechRisikoTemp = t; }
|
||||
}
|
||||
else
|
||||
{
|
||||
settings.Insert(new SettingEntity
|
||||
{
|
||||
Key = entry.Key,
|
||||
Value = entry.Default,
|
||||
UserId = _currentUserId,
|
||||
ProjectId = _currentProjectId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
using Microsoft.JSInterop;
|
||||
using System.Reflection.Metadata.Ecma335;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public class ThemeService : IThemeService
|
||||
{
|
||||
private readonly IJSRuntime _js;
|
||||
private string _theme = "";
|
||||
|
||||
public ThemeService(IJSRuntime js)
|
||||
{
|
||||
_js = js;
|
||||
}
|
||||
|
||||
public string Theme { get => _theme; }
|
||||
|
||||
public async Task ApplyTheme(string theme)
|
||||
{
|
||||
_theme = 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);");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
using System.Security.Cryptography;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public static class TotpHelper
|
||||
{
|
||||
private const int TotpSize = 6;
|
||||
private const int TotpStepSeconds = 30;
|
||||
private const int WindowSize = 1;
|
||||
|
||||
public static string GenerateSecret()
|
||||
{
|
||||
var secret = RandomNumberGenerator.GetBytes(20);
|
||||
return Base32Encode(secret);
|
||||
}
|
||||
|
||||
public static string GetProvisioningUri(string secret, string username, string issuer = "AuthorBuddy")
|
||||
{
|
||||
return $"otpauth://totp/{Uri.EscapeDataString(issuer)}:{Uri.EscapeDataString(username)}" +
|
||||
$"?secret={secret}&issuer={Uri.EscapeDataString(issuer)}&algorithm=SHA1&digits={TotpSize}&period={TotpStepSeconds}";
|
||||
}
|
||||
|
||||
public static bool VerifyCode(string secret, string code)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(code) || code.Length != TotpSize) return false;
|
||||
if (!int.TryParse(code, out _)) return false;
|
||||
|
||||
var secretBytes = Base32Decode(secret);
|
||||
var counter = GetCurrentCounter();
|
||||
|
||||
for (int i = -WindowSize; i <= WindowSize; i++)
|
||||
{
|
||||
var expected = GenerateTotp(secretBytes, counter + i);
|
||||
if (expected == code) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static long GetCurrentCounter()
|
||||
{
|
||||
var unix = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
return unix / TotpStepSeconds;
|
||||
}
|
||||
|
||||
private static string GenerateTotp(byte[] secret, long counter)
|
||||
{
|
||||
var counterBytes = BitConverter.GetBytes(counter);
|
||||
if (BitConverter.IsLittleEndian) Array.Reverse(counterBytes);
|
||||
|
||||
byte[] hash;
|
||||
using (var hmac = new HMACSHA1(secret))
|
||||
{
|
||||
hash = hmac.ComputeHash(counterBytes);
|
||||
}
|
||||
|
||||
int offset = hash[^1] & 0xf;
|
||||
int binary = ((hash[offset] & 0x7f) << 24) |
|
||||
((hash[offset + 1] & 0xff) << 16) |
|
||||
((hash[offset + 2] & 0xff) << 8) |
|
||||
(hash[offset + 3] & 0xff);
|
||||
|
||||
int otp = binary % (int)Math.Pow(10, TotpSize);
|
||||
return otp.ToString($"D{TotpSize}");
|
||||
}
|
||||
|
||||
private static string Base32Encode(byte[] data)
|
||||
{
|
||||
const string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
int bitCount = 0;
|
||||
int currentByte = 0;
|
||||
var result = new System.Text.StringBuilder();
|
||||
|
||||
foreach (var b in data)
|
||||
{
|
||||
currentByte = (currentByte << 8) | b;
|
||||
bitCount += 8;
|
||||
|
||||
while (bitCount >= 5)
|
||||
{
|
||||
bitCount -= 5;
|
||||
int index = (currentByte >> bitCount) & 0x1f;
|
||||
result.Append(alphabet[index]);
|
||||
}
|
||||
}
|
||||
|
||||
if (bitCount > 0)
|
||||
{
|
||||
int index = (currentByte << (5 - bitCount)) & 0x1f;
|
||||
result.Append(alphabet[index]);
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
private static byte[] Base32Decode(string base32)
|
||||
{
|
||||
const string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
base32 = base32.Trim().ToUpperInvariant()
|
||||
.Replace('0', 'O').Replace('1', 'I').Replace('8', 'B');
|
||||
|
||||
int bitCount = 0;
|
||||
int currentByte = 0;
|
||||
var bytes = new System.Collections.Generic.List<byte>();
|
||||
|
||||
foreach (var c in base32)
|
||||
{
|
||||
int index = alphabet.IndexOf(c);
|
||||
if (index < 0) continue;
|
||||
|
||||
currentByte = (currentByte << 5) | index;
|
||||
bitCount += 5;
|
||||
|
||||
if (bitCount >= 8)
|
||||
{
|
||||
bitCount -= 8;
|
||||
bytes.Add((byte)((currentByte >> bitCount) & 0xff));
|
||||
}
|
||||
}
|
||||
|
||||
return [.. bytes];
|
||||
}
|
||||
|
||||
public static string HashPassword(string password)
|
||||
{
|
||||
var salt = RandomNumberGenerator.GetBytes(16);
|
||||
var hash = Rfc2898DeriveBytes.Pbkdf2(
|
||||
password,
|
||||
salt,
|
||||
100_000,
|
||||
HashAlgorithmName.SHA256,
|
||||
32);
|
||||
return $"{Convert.ToBase64String(salt)}.{Convert.ToBase64String(hash)}";
|
||||
}
|
||||
|
||||
public static bool VerifyPassword(string password, string hashedPassword)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hashedPassword)) return false;
|
||||
var parts = hashedPassword.Split('.');
|
||||
if (parts.Length != 2) return false;
|
||||
|
||||
try
|
||||
{
|
||||
var salt = Convert.FromBase64String(parts[0]);
|
||||
var storedHash = Convert.FromBase64String(parts[1]);
|
||||
var computedHash = Rfc2898DeriveBytes.Pbkdf2(
|
||||
password,
|
||||
salt,
|
||||
100_000,
|
||||
HashAlgorithmName.SHA256,
|
||||
32);
|
||||
return CryptographicOperations.FixedTimeEquals(storedHash, computedHash);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
using AuthorBuddy.Web.Models;
|
||||
using System.Text.Json;
|
||||
using System.Web;
|
||||
|
||||
namespace AuthorBuddy.Web.Services;
|
||||
|
||||
public class WikipediaService : IWikipediaService
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
private readonly string[] _languages = { "de", "en" };
|
||||
|
||||
public WikipediaService()
|
||||
{
|
||||
_http = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
|
||||
_http.DefaultRequestHeaders.UserAgent.ParseAdd("AuthorBuddy/1.0 (writing-assistant)");
|
||||
}
|
||||
|
||||
public async Task<List<WikipediaResult>> SearchAsync(string query, int maxResults = 3)
|
||||
{
|
||||
return await SearchAsync(new WikipediaSearchRequest
|
||||
{
|
||||
Query = query,
|
||||
MaxResults = maxResults
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<List<WikipediaResult>> SearchAsync(WikipediaSearchRequest request)
|
||||
{
|
||||
var results = new List<WikipediaResult>();
|
||||
|
||||
foreach (var lang in _languages)
|
||||
{
|
||||
if (lang == "de" && !request.SearchGerman) continue;
|
||||
if (lang == "en" && !request.SearchEnglish) continue;
|
||||
|
||||
try
|
||||
{
|
||||
var langResults = await SearchLanguageAsync(lang, request.Query, request.MaxResults);
|
||||
results.AddRange(langResults);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Sprache überspringen bei Fehler
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Task<string?> GetExtractAsync(string title, string language = "de")
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"https://{language}.wikipedia.org/w/api.php" +
|
||||
"?action=query" +
|
||||
"&prop=extracts" +
|
||||
"&exintro&explaintext" +
|
||||
$"&titles={HttpUtility.UrlEncode(title)}" +
|
||||
"&format=json&utf8=1";
|
||||
|
||||
var response = await _http.GetStringAsync(url);
|
||||
using var doc = JsonDocument.Parse(response);
|
||||
var pages = doc.RootElement.GetProperty("query").GetProperty("pages");
|
||||
|
||||
foreach (var page in pages.EnumerateObject())
|
||||
{
|
||||
if (page.Value.TryGetProperty("extract", out var extract))
|
||||
return extract.GetString();
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<string?> GetFullArticleAsync(string title, string language = "de")
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"https://{language}.wikipedia.org/w/api.php" +
|
||||
"?action=query" +
|
||||
"&prop=extracts" +
|
||||
"&explaintext" +
|
||||
$"&titles={HttpUtility.UrlEncode(title)}" +
|
||||
"&format=json&utf8=1";
|
||||
|
||||
var response = await _http.GetStringAsync(url);
|
||||
using var doc = JsonDocument.Parse(response);
|
||||
var pages = doc.RootElement.GetProperty("query").GetProperty("pages");
|
||||
|
||||
foreach (var page in pages.EnumerateObject())
|
||||
{
|
||||
if (page.Value.TryGetProperty("extract", out var extract))
|
||||
return extract.GetString();
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<List<WikipediaResult>> SearchLanguageAsync(string lang, string query, int maxResults)
|
||||
{
|
||||
var results = new List<WikipediaResult>();
|
||||
|
||||
var url = $"https://{lang}.wikipedia.org/w/api.php" +
|
||||
"?action=query" +
|
||||
"&list=search" +
|
||||
$"&srsearch={HttpUtility.UrlEncode(query)}" +
|
||||
$"&srlimit={maxResults}" +
|
||||
"&format=json&utf8=1";
|
||||
|
||||
var response = await _http.GetStringAsync(url);
|
||||
using var doc = JsonDocument.Parse(response);
|
||||
var search = doc.RootElement.GetProperty("query").GetProperty("search");
|
||||
|
||||
foreach (var item in search.EnumerateArray())
|
||||
{
|
||||
var title = item.GetProperty("title").GetString() ?? "";
|
||||
results.Add(new WikipediaResult
|
||||
{
|
||||
Title = title,
|
||||
Snippet = StripHtml(item.GetProperty("snippet").GetString() ?? ""),
|
||||
Url = $"https://{lang}.wikipedia.org/wiki/{HttpUtility.UrlEncode(title)}",
|
||||
Language = lang
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static string StripHtml(string html)
|
||||
{
|
||||
return System.Text.RegularExpressions.Regex.Replace(html, "<[^>]*>", "");
|
||||
}
|
||||
}
|
||||
BIN
author_brain.db
Normal file
BIN
author_brain.db
Normal file
Binary file not shown.
0
err.txt
0
err.txt
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"StyleModel": "gemma4:e4b",
|
||||
"EmbedModel": "nomic-embed-text",
|
||||
"SpellingModel": "phi3:mini",
|
||||
"OllamaUrl": "http://localhost:11434",
|
||||
"Theme": "light",
|
||||
"Language": "de",
|
||||
"LektorSystemPrompt": "Du bist ein erfahrener Lektor. Analysiere den Text des Autors. Optimiere ihn auf einen lebendigen, anschaulichen und subjektiven Stil. Nutze starke Verben, vermeide unn\u00F6tige Adjektive und achte auf \u0027Show, don\u0027t tell\u0027. Antworte direkt mit dem verbesserten Text oder konkreten Vorschl\u00E4gen.",
|
||||
"FactCheckTemplate": "FAKTEN AUS DER DATENBANK:\n{0}\n\nFRAGE / KONTEXT:\n{1}\n\nBasierend auf den Fakten, schreibe eine kurze, lebendige Erg\u00E4nzung oder Korrektur.",
|
||||
"SpellingPrompt": "Du bist ein Rechtschreibpr\u00FCfer. Korrigiere NUR Fehler (Rechtschreibung/Grammatik). \u00C4ndere den Stil nicht. Gib nur den korrigierten Text zur\u00FCck.",
|
||||
"AdvocatusDiaboliPrompt": "Nimm eine radikale Gegenposition zu meinem Text ein. Welche Schwachstellen in meiner Argumentation fallen dir auf?",
|
||||
"HookFinderPrompt": "Generiere mir 5 emotionale, packende Einstiegss\u00E4tze (Hooks) f\u00FCr diesen Text, die den Leser sofort fesseln.",
|
||||
"Eli5Prompt": "Wo ist der Text zu kompliziert geschrieben? Liefere mir konkrete Vorschl\u00E4ge, wie ich komplexe Abs\u00E4tze einfacher formulieren kann, ohne den Kern zu verlieren.",
|
||||
"CoAuthorBrainstormingPrompt": "Du bist ein kreativer Co-Autor. Der Nutzer gibt dir eine Idee, einen Charakter oder eine Situation. Hilf ihm, die Handlung oder Personen weiterzuentwickeln. Stelle kluge Fragen, schlage alternative Wendungen vor, denke \u00FCber Motivationen, Konflikte und emotionale Tiefe nach. Sei inspirierend, aber bleib beim Thema.",
|
||||
"LektorSystemPromptEn": "You are an experienced editor. Analyze the author\u0027s text. Optimize it for a vivid, descriptive, and subjective style. Use strong verbs, avoid unnecessary adjectives, and follow \u0027Show, don\u0027t tell\u0027. Respond directly with the improved text or concrete suggestions.",
|
||||
"FactCheckTemplateEn": "FACTS FROM DATABASE:\n{0}\n\nQUESTION / CONTEXT:\n{1}\n\nBased on the facts, write a short, vivid addition or correction.",
|
||||
"SpellingPromptEn": "You are a spell checker. Correct ONLY errors (spelling/grammar). Do not change the style. Return only the corrected text.",
|
||||
"AdvocatusDiaboliPromptEn": "Take a radical opposing position to my text. What weaknesses in my argumentation do you notice?",
|
||||
"HookFinderPromptEn": "Generate 5 emotional, captivating opening sentences (hooks) for this text that immediately grab the reader\u0027s attention.",
|
||||
"Eli5PromptEn": "Where is the text too complicated? Give me concrete suggestions on how to simplify complex paragraphs without losing the core message.",
|
||||
"CoAuthorBrainstormingPromptEn": "You are a creative co-author. The user gives you an idea, a character, or a situation. Help them develop the plot or characters further. Ask smart questions, suggest alternative twists, think about motivations, conflicts, and emotional depth. Be inspiring but stay on topic.",
|
||||
"LektorTemp": 0.7,
|
||||
"FactCheckTemp": 0.3,
|
||||
"SpellingTemp": 0.1,
|
||||
"AdvocatusDiaboliTemp": 0.9,
|
||||
"HookFinderTemp": 0.9,
|
||||
"Eli5Temp": 0.5,
|
||||
"CoAuthorTemp": 1
|
||||
}
|
||||
10
out.txt
10
out.txt
|
|
@ -1,10 +0,0 @@
|
|||
Die Starteinstellungen von E:\ollama_net\ollama\AuthorBuddy.Web\Properties\launchSettings.json werden verwendet…
|
||||
Buildvorgang wird ausgeführt...
|
||||
info: Microsoft.Hosting.Lifetime[14]
|
||||
Now listening on: http://localhost:5216
|
||||
info: Microsoft.Hosting.Lifetime[0]
|
||||
Application started. Press Ctrl+C to shut down.
|
||||
info: Microsoft.Hosting.Lifetime[0]
|
||||
Hosting environment: Development
|
||||
info: Microsoft.Hosting.Lifetime[0]
|
||||
Content root path: E:\ollama_net\ollama\AuthorBuddy.Web
|
||||
BIN
vec0.dll
Normal file
BIN
vec0.dll
Normal file
Binary file not shown.
1046
wwwroot/app.css
1046
wwwroot/app.css
File diff suppressed because it is too large
Load diff
|
|
@ -7,6 +7,4 @@
|
|||
--menu-bg: #252526;
|
||||
--card-bg: #2d2d2d;
|
||||
--status-bar-bg: #007acc;
|
||||
--disabled-bg: #2a2a2a;
|
||||
--disabled-text: #606060;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,4 @@
|
|||
--menu-bg: #e0e0e0;
|
||||
--card-bg: #ffffff;
|
||||
--status-bar-bg: #dcdcdc;
|
||||
--disabled-bg: #e8e8e8;
|
||||
--disabled-text: #a0a0a0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,4 @@
|
|||
--menu-bg: #c0c0c0;
|
||||
--card-bg: #ffffff;
|
||||
--status-bar-bg: #c0c0c0;
|
||||
--disabled-bg: #c0c0c0;
|
||||
--disabled-text: #808080;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
window.downloadFile = function (fileName, base64, mimeType) {
|
||||
const byteChars = atob(base64);
|
||||
const bytes = new Uint8Array(byteChars.length);
|
||||
for (let i = 0; i < byteChars.length; i++) bytes[i] = byteChars.charCodeAt(i);
|
||||
const blob = new Blob([bytes], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
setTimeout(() => URL.revokeObjectURL(url), 10000);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue