- continue

This commit is contained in:
Roland 2026-08-30 08:11:01 +02:00
parent 890b5bbf0b
commit 5eba92e83a
41 changed files with 3096 additions and 586 deletions

View file

@ -7,13 +7,29 @@
"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\\wwwroot\\app.css||{A5401142-F49D-43DB-90B1-F57BA349E55C}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:wwwroot\\app.css||{A5401142-F49D-43DB-90B1-F57BA349E55C}"
"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\\services\\ollamaservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\ollamaservice.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\\ollamacloudbackend.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\ollamacloudbackend.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\\app.css||{A5401142-F49D-43DB-90B1-F57BA349E55C}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:wwwroot\\app.css||{A5401142-F49D-43DB-90B1-F57BA349E55C}"
},
{
"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}"
@ -26,14 +42,6 @@
"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\\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\\services\\ollamaservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\ollamaservice.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\\ollamabackend.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\ollamabackend.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
@ -54,10 +62,6 @@
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\resources\\de.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:resources\\de.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}"
},
{
"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\\components\\pages\\chat.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\pages\\chat.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
@ -208,7 +212,7 @@
"DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": 5,
"SelectedChildIndex": 6,
"Children": [
{
"$type": "Bookmark",
@ -216,7 +220,19 @@
},
{
"$type": "Document",
"DocumentIndex": 1,
"DocumentIndex": 5,
"Title": "OllamaCloudBackend.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\OllamaCloudBackend.cs",
"RelativeDocumentMoniker": "Services\\OllamaCloudBackend.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\OllamaCloudBackend.cs",
"RelativeToolTip": "Services\\OllamaCloudBackend.cs",
"ViewState": "AgIAAMMAAAAAAAAAAAAqwNgAAAA6AAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-07-12T07:08:37.774Z"
},
{
"$type": "Document",
"DocumentIndex": 6,
"Title": "app.css",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\wwwroot\\app.css",
"RelativeDocumentMoniker": "wwwroot\\app.css",
@ -228,7 +244,7 @@
},
{
"$type": "Document",
"DocumentIndex": 5,
"DocumentIndex": 9,
"Title": "Editor.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Editor.razor",
"RelativeDocumentMoniker": "Components\\Pages\\Editor.razor",
@ -240,7 +256,68 @@
},
{
"$type": "Document",
"DocumentIndex": 9,
"DocumentIndex": 2,
"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": "AgIAAIsCAAAAAAAAAAArwKACAAAQAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
"WhenOpened": "2026-05-15T16:43:26.6Z"
},
{
"$type": "Document",
"DocumentIndex": 3,
"Title": "OllamaService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\OllamaService.cs",
"RelativeDocumentMoniker": "Services\\OllamaService.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\OllamaService.cs",
"RelativeToolTip": "Services\\OllamaService.cs",
"ViewState": "AgIAAI0AAAAAAAAAAAAqwKMAAAAEAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-06-26T13:33:25.661Z"
},
{
"$type": "Document",
"DocumentIndex": 0,
"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": "AgIAAO8BAAAAAAAAAAAiwAICAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
"WhenOpened": "2026-05-15T16:04:36.444Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 1,
"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": "AgIAAE8AAAAAAAAAAAAQwGIAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-20T04:07:21.54Z"
},
{
"$type": "Document",
"DocumentIndex": 4,
"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": "AgIAAKYCAAAAAAAAAAAAAOIAAAAEAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-21T17:37:24.777Z"
},
{
"$type": "Document",
"DocumentIndex": 11,
"Title": "IOllamaService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IOllamaService.cs",
"RelativeDocumentMoniker": "Services\\IOllamaService.cs",
@ -252,7 +329,7 @@
},
{
"$type": "Document",
"DocumentIndex": 14,
"DocumentIndex": 15,
"Title": "Chat.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Chat.razor",
"RelativeDocumentMoniker": "Components\\Pages\\Chat.razor",
@ -264,32 +341,7 @@
},
{
"$type": "Document",
"DocumentIndex": 0,
"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": "AgIAADMAAAAAAAAAAAApwEYAAAByAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
"WhenOpened": "2026-05-15T16:04:36.444Z",
"EditorCaption": ""
},
{
"$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": "AgIAAEcAAAAAAAAAAAAQwFoAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-20T04:07:21.54Z"
},
{
"$type": "Document",
"DocumentIndex": 8,
"DocumentIndex": 10,
"Title": "OllamaBackend.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\OllamaBackend.cs",
"RelativeDocumentMoniker": "Services\\OllamaBackend.cs",
@ -301,19 +353,7 @@
},
{
"$type": "Document",
"DocumentIndex": 2,
"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": "AgIAABIAAAAAAAAAAAAAADMAAAAfAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
"WhenOpened": "2026-05-15T16:43:26.6Z"
},
{
"$type": "Document",
"DocumentIndex": 23,
"DocumentIndex": 24,
"Title": "DbAdminService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\DbAdminService.cs",
"RelativeDocumentMoniker": "Services\\DbAdminService.cs",
@ -325,7 +365,7 @@
},
{
"$type": "Document",
"DocumentIndex": 3,
"DocumentIndex": 7,
"Title": "AuthorBuddy.Web.csproj",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\AuthorBuddy.Web.csproj",
"RelativeDocumentMoniker": "AuthorBuddy.Web.csproj",
@ -337,19 +377,7 @@
},
{
"$type": "Document",
"DocumentIndex": 7,
"Title": "OllamaService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\OllamaService.cs",
"RelativeDocumentMoniker": "Services\\OllamaService.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\OllamaService.cs",
"RelativeToolTip": "Services\\OllamaService.cs",
"ViewState": "AgIAAK8AAAAAAAAAAAAqwLwAAAAIAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-06-26T13:33:25.661Z"
},
{
"$type": "Document",
"DocumentIndex": 4,
"DocumentIndex": 8,
"Title": "QuickMarkdown.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\QuickMarkdown.razor",
"RelativeDocumentMoniker": "Components\\Pages\\QuickMarkdown.razor",
@ -361,7 +389,7 @@
},
{
"$type": "Document",
"DocumentIndex": 19,
"DocumentIndex": 20,
"Title": "Login.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Login.razor",
"RelativeDocumentMoniker": "Components\\Pages\\Login.razor",
@ -373,7 +401,7 @@
},
{
"$type": "Document",
"DocumentIndex": 20,
"DocumentIndex": 21,
"Title": "FirstRunSetup.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\FirstRunSetup.razor",
"RelativeDocumentMoniker": "Components\\Pages\\FirstRunSetup.razor",
@ -385,19 +413,7 @@
},
{
"$type": "Document",
"DocumentIndex": 13,
"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": "AgIAANwBAAAAAAAAAAAAAO4BAAAIAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-21T17:37:24.777Z"
},
{
"$type": "Document",
"DocumentIndex": 10,
"DocumentIndex": 12,
"Title": "ChatService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ChatService.cs",
"RelativeDocumentMoniker": "Services\\ChatService.cs",
@ -409,7 +425,7 @@
},
{
"$type": "Document",
"DocumentIndex": 12,
"DocumentIndex": 14,
"Title": "de.json",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Resources\\de.json",
"RelativeDocumentMoniker": "Resources\\de.json",
@ -421,7 +437,7 @@
},
{
"$type": "Document",
"DocumentIndex": 11,
"DocumentIndex": 13,
"Title": "en.json",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Resources\\en.json",
"RelativeDocumentMoniker": "Resources\\en.json",
@ -433,7 +449,7 @@
},
{
"$type": "Document",
"DocumentIndex": 16,
"DocumentIndex": 17,
"Title": "NavMenu.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\NavMenu.razor",
"RelativeDocumentMoniker": "Components\\Layout\\NavMenu.razor",
@ -445,7 +461,7 @@
},
{
"$type": "Document",
"DocumentIndex": 15,
"DocumentIndex": 16,
"Title": "MainLayout.razor.css",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\MainLayout.razor.css",
"RelativeDocumentMoniker": "Components\\Layout\\MainLayout.razor.css",
@ -457,7 +473,7 @@
},
{
"$type": "Document",
"DocumentIndex": 22,
"DocumentIndex": 23,
"Title": "DbAdmin.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\DbAdmin.razor",
"RelativeDocumentMoniker": "Components\\Pages\\DbAdmin.razor",
@ -469,7 +485,7 @@
},
{
"$type": "Document",
"DocumentIndex": 17,
"DocumentIndex": 18,
"Title": "ILLMBackend.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ILLMBackend.cs",
"RelativeDocumentMoniker": "Services\\ILLMBackend.cs",
@ -481,7 +497,7 @@
},
{
"$type": "Document",
"DocumentIndex": 18,
"DocumentIndex": 19,
"Title": "UserEntity.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\UserEntity.cs",
"RelativeDocumentMoniker": "Data\\UserEntity.cs",
@ -493,7 +509,7 @@
},
{
"$type": "Document",
"DocumentIndex": 21,
"DocumentIndex": 22,
"Title": "OllamaAssistant.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\OllamaAssistant.razor",
"RelativeDocumentMoniker": "Components\\Pages\\OllamaAssistant.razor",
@ -505,7 +521,7 @@
},
{
"$type": "Document",
"DocumentIndex": 27,
"DocumentIndex": 28,
"Title": "ProjectService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ProjectService.cs",
"RelativeDocumentMoniker": "Services\\ProjectService.cs",
@ -517,7 +533,7 @@
},
{
"$type": "Document",
"DocumentIndex": 28,
"DocumentIndex": 29,
"Title": "IOllamaSetupService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IOllamaSetupService.cs",
"RelativeDocumentMoniker": "Services\\IOllamaSetupService.cs",
@ -529,7 +545,7 @@
},
{
"$type": "Document",
"DocumentIndex": 24,
"DocumentIndex": 25,
"Title": "SafeFileHandle.Windows.cs",
"DocumentMoniker": "C:\\Users\\HP\\AppData\\Local\\SourceServer\\dddeb33c46e3c746b90c5f64463103c08ec03b9a85e92eab3b21c11218135d28\\src\\runtime\\src\\libraries\\System.Private.CoreLib\\src\\Microsoft\\Win32\\SafeHandles\\SafeFileHandle.Windows.cs",
"ToolTip": "C:\\Users\\HP\\AppData\\Local\\SourceServer\\dddeb33c46e3c746b90c5f64463103c08ec03b9a85e92eab3b21c11218135d28\\src\\runtime\\src\\libraries\\System.Private.CoreLib\\src\\Microsoft\\Win32\\SafeHandles\\SafeFileHandle.Windows.cs [Schreibgesch\u00FCtzt]",
@ -540,7 +556,7 @@
},
{
"$type": "Document",
"DocumentIndex": 25,
"DocumentIndex": 26,
"Title": "A7405018.jpg",
"DocumentMoniker": "C:\\Users\\HP\\Desktop\\Website\\A7405018.jpg",
"ToolTip": "C:\\Users\\HP\\Desktop\\Website\\A7405018.jpg",
@ -549,7 +565,7 @@
},
{
"$type": "Document",
"DocumentIndex": 29,
"DocumentIndex": 30,
"Title": "INSTALL.md",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\INSTALL.md",
"RelativeDocumentMoniker": "INSTALL.md",
@ -561,7 +577,7 @@
},
{
"$type": "Document",
"DocumentIndex": 30,
"DocumentIndex": 31,
"Title": "IThemeService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IThemeService.cs",
"RelativeDocumentMoniker": "Services\\IThemeService.cs",
@ -573,7 +589,7 @@
},
{
"$type": "Document",
"DocumentIndex": 31,
"DocumentIndex": 32,
"Title": "RedirectToLogin.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\RedirectToLogin.razor",
"RelativeDocumentMoniker": "Components\\RedirectToLogin.razor",
@ -585,7 +601,7 @@
},
{
"$type": "Document",
"DocumentIndex": 32,
"DocumentIndex": 33,
"Title": "ISettingsService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ISettingsService.cs",
"RelativeDocumentMoniker": "Services\\ISettingsService.cs",
@ -597,7 +613,7 @@
},
{
"$type": "Document",
"DocumentIndex": 33,
"DocumentIndex": 34,
"Title": "IRagService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IRagService.cs",
"RelativeDocumentMoniker": "Services\\IRagService.cs",
@ -609,7 +625,7 @@
},
{
"$type": "Document",
"DocumentIndex": 34,
"DocumentIndex": 35,
"Title": "ThemeService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ThemeService.cs",
"RelativeDocumentMoniker": "Services\\ThemeService.cs",
@ -621,7 +637,7 @@
},
{
"$type": "Document",
"DocumentIndex": 35,
"DocumentIndex": 36,
"Title": "logo.jpg",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Assets\\logo.jpg",
"RelativeDocumentMoniker": "Assets\\logo.jpg",
@ -632,7 +648,7 @@
},
{
"$type": "Document",
"DocumentIndex": 36,
"DocumentIndex": 37,
"Title": "OllamaSettings.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\OllamaSettings.cs",
"RelativeDocumentMoniker": "Models\\OllamaSettings.cs",
@ -644,7 +660,7 @@
},
{
"$type": "Document",
"DocumentIndex": 26,
"DocumentIndex": 27,
"Title": "AppDbContext.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\AppDbContext.cs",
"RelativeDocumentMoniker": "Data\\AppDbContext.cs",
@ -655,7 +671,7 @@
},
{
"$type": "Document",
"DocumentIndex": 37,
"DocumentIndex": 38,
"Title": "FileScannerService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\FileScannerService.cs",
"RelativeDocumentMoniker": "Services\\FileScannerService.cs",
@ -667,7 +683,7 @@
},
{
"$type": "Document",
"DocumentIndex": 38,
"DocumentIndex": 39,
"Title": "TotpHelper.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\TotpHelper.cs",
"RelativeDocumentMoniker": "Services\\TotpHelper.cs",
@ -679,7 +695,7 @@
},
{
"$type": "Document",
"DocumentIndex": 39,
"DocumentIndex": 40,
"Title": "Project.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\Project.cs",
"RelativeDocumentMoniker": "Models\\Project.cs",
@ -691,7 +707,7 @@
},
{
"$type": "Document",
"DocumentIndex": 40,
"DocumentIndex": 41,
"Title": "RagService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\RagService.cs",
"RelativeDocumentMoniker": "Services\\RagService.cs",
@ -703,7 +719,7 @@
},
{
"$type": "Document",
"DocumentIndex": 41,
"DocumentIndex": 42,
"Title": "App.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\App.razor",
"RelativeDocumentMoniker": "Components\\App.razor",
@ -715,7 +731,7 @@
},
{
"$type": "Document",
"DocumentIndex": 42,
"DocumentIndex": 43,
"Title": "Models.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Models.razor",
"RelativeDocumentMoniker": "Components\\Pages\\Models.razor",
@ -727,7 +743,7 @@
},
{
"$type": "Document",
"DocumentIndex": 43,
"DocumentIndex": 44,
"Title": "LLM_Model.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\LLM_Model.cs",
"RelativeDocumentMoniker": "Models\\LLM_Model.cs",
@ -739,7 +755,7 @@
},
{
"$type": "Document",
"DocumentIndex": 44,
"DocumentIndex": 45,
"Title": "ModelStatus.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\ModelStatus.cs",
"RelativeDocumentMoniker": "Models\\ModelStatus.cs",
@ -751,7 +767,7 @@
},
{
"$type": "Document",
"DocumentIndex": 45,
"DocumentIndex": 46,
"Title": "IFileScannerService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IFileScannerService.cs",
"RelativeDocumentMoniker": "Services\\IFileScannerService.cs",
@ -763,7 +779,7 @@
},
{
"$type": "Document",
"DocumentIndex": 46,
"DocumentIndex": 47,
"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",
@ -775,7 +791,7 @@
},
{
"$type": "Document",
"DocumentIndex": 47,
"DocumentIndex": 48,
"Title": "ProjectEntity.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\ProjectEntity.cs",
"RelativeDocumentMoniker": "Data\\ProjectEntity.cs",
@ -787,7 +803,7 @@
},
{
"$type": "Document",
"DocumentIndex": 48,
"DocumentIndex": 49,
"Title": "IProjectService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IProjectService.cs",
"RelativeDocumentMoniker": "Services\\IProjectService.cs",
@ -799,7 +815,7 @@
},
{
"$type": "Document",
"DocumentIndex": 49,
"DocumentIndex": 50,
"Title": "appsettings.json",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\appsettings.json",
"RelativeDocumentMoniker": "appsettings.json",
@ -808,6 +824,10 @@
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.001642|",
"WhenOpened": "2026-05-14T10:14:25.605Z"
},
{
"$type": "Bookmark",
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
}
]
}

View file

@ -7,13 +7,29 @@
"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\\wwwroot\\app.css||{A5401142-F49D-43DB-90B1-F57BA349E55C}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:wwwroot\\app.css||{A5401142-F49D-43DB-90B1-F57BA349E55C}"
"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\\services\\ollamaservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\ollamaservice.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\\ollamacloudbackend.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\ollamacloudbackend.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\\app.css||{A5401142-F49D-43DB-90B1-F57BA349E55C}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:wwwroot\\app.css||{A5401142-F49D-43DB-90B1-F57BA349E55C}"
},
{
"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}"
@ -26,14 +42,6 @@
"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\\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\\services\\ollamaservice.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\ollamaservice.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\\ollamabackend.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:services\\ollamabackend.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
@ -54,10 +62,6 @@
"AbsoluteMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|e:\\ollama_net\\ollama\\authorbuddy.web\\resources\\de.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:resources\\de.json||{90A6B3A7-C1A3-4009-A288-E2FF89E96FA0}"
},
{
"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\\components\\pages\\chat.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}",
"RelativeMoniker": "D:0:0:{4F4697EF-EFFA-26AC-C8A1-C9859805D247}|AuthorBuddy.Web.csproj|solutionrelative:components\\pages\\chat.razor||{40D31677-CBC0-4297-A9EF-89D907823A98}"
@ -208,15 +212,31 @@
"DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": 5,
"SelectedChildIndex": 7,
"Children": [
{
"$type": "Bookmark",
"Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}"
},
{
"$type": "Bookmark",
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
},
{
"$type": "Document",
"DocumentIndex": 1,
"DocumentIndex": 5,
"Title": "OllamaCloudBackend.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\OllamaCloudBackend.cs",
"RelativeDocumentMoniker": "Services\\OllamaCloudBackend.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\OllamaCloudBackend.cs",
"RelativeToolTip": "Services\\OllamaCloudBackend.cs",
"ViewState": "AgIAAMMAAAAAAAAAAAAqwNgAAAA6AAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-07-12T07:08:37.774Z"
},
{
"$type": "Document",
"DocumentIndex": 6,
"Title": "app.css",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\wwwroot\\app.css",
"RelativeDocumentMoniker": "wwwroot\\app.css",
@ -228,7 +248,7 @@
},
{
"$type": "Document",
"DocumentIndex": 5,
"DocumentIndex": 9,
"Title": "Editor.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Editor.razor",
"RelativeDocumentMoniker": "Components\\Pages\\Editor.razor",
@ -240,7 +260,69 @@
},
{
"$type": "Document",
"DocumentIndex": 9,
"DocumentIndex": 2,
"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": "AgIAAIsCAAAAAAAAAAArwKACAAAQAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
"WhenOpened": "2026-05-15T16:43:26.6Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 3,
"Title": "OllamaService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\OllamaService.cs",
"RelativeDocumentMoniker": "Services\\OllamaService.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\OllamaService.cs",
"RelativeToolTip": "Services\\OllamaService.cs",
"ViewState": "AgIAAI0AAAAAAAAAAAAqwKMAAAAEAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-06-26T13:33:25.661Z"
},
{
"$type": "Document",
"DocumentIndex": 0,
"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": "AgIAAFUCAAAAAAAAAAAiwGECAABPAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
"WhenOpened": "2026-05-15T16:04:36.444Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 1,
"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": "AgIAAE8AAAAAAAAAAAAQwGIAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-20T04:07:21.54Z"
},
{
"$type": "Document",
"DocumentIndex": 4,
"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": "AgIAAKYCAAAAAAAAAAAAAOIAAAAEAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-21T17:37:24.777Z"
},
{
"$type": "Document",
"DocumentIndex": 11,
"Title": "IOllamaService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IOllamaService.cs",
"RelativeDocumentMoniker": "Services\\IOllamaService.cs",
@ -252,7 +334,7 @@
},
{
"$type": "Document",
"DocumentIndex": 14,
"DocumentIndex": 15,
"Title": "Chat.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Chat.razor",
"RelativeDocumentMoniker": "Components\\Pages\\Chat.razor",
@ -264,32 +346,7 @@
},
{
"$type": "Document",
"DocumentIndex": 0,
"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": "AgIAADMAAAAAAAAAAAApwEYAAAByAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
"WhenOpened": "2026-05-15T16:04:36.444Z",
"EditorCaption": ""
},
{
"$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": "AgIAAEcAAAAAAAAAAAAQwFoAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-20T04:07:21.54Z"
},
{
"$type": "Document",
"DocumentIndex": 8,
"DocumentIndex": 10,
"Title": "OllamaBackend.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\OllamaBackend.cs",
"RelativeDocumentMoniker": "Services\\OllamaBackend.cs",
@ -301,19 +358,7 @@
},
{
"$type": "Document",
"DocumentIndex": 2,
"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": "AgIAABIAAAAAAAAAAAAAADMAAAAfAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
"WhenOpened": "2026-05-15T16:43:26.6Z"
},
{
"$type": "Document",
"DocumentIndex": 23,
"DocumentIndex": 24,
"Title": "DbAdminService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\DbAdminService.cs",
"RelativeDocumentMoniker": "Services\\DbAdminService.cs",
@ -325,7 +370,7 @@
},
{
"$type": "Document",
"DocumentIndex": 3,
"DocumentIndex": 7,
"Title": "AuthorBuddy.Web.csproj",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\AuthorBuddy.Web.csproj",
"RelativeDocumentMoniker": "AuthorBuddy.Web.csproj",
@ -337,19 +382,7 @@
},
{
"$type": "Document",
"DocumentIndex": 7,
"Title": "OllamaService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\OllamaService.cs",
"RelativeDocumentMoniker": "Services\\OllamaService.cs",
"ToolTip": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\OllamaService.cs",
"RelativeToolTip": "Services\\OllamaService.cs",
"ViewState": "AgIAAK8AAAAAAAAAAAAqwLwAAAAIAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-06-26T13:33:25.661Z"
},
{
"$type": "Document",
"DocumentIndex": 4,
"DocumentIndex": 8,
"Title": "QuickMarkdown.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\QuickMarkdown.razor",
"RelativeDocumentMoniker": "Components\\Pages\\QuickMarkdown.razor",
@ -361,7 +394,7 @@
},
{
"$type": "Document",
"DocumentIndex": 19,
"DocumentIndex": 20,
"Title": "Login.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Login.razor",
"RelativeDocumentMoniker": "Components\\Pages\\Login.razor",
@ -373,7 +406,7 @@
},
{
"$type": "Document",
"DocumentIndex": 20,
"DocumentIndex": 21,
"Title": "FirstRunSetup.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\FirstRunSetup.razor",
"RelativeDocumentMoniker": "Components\\Pages\\FirstRunSetup.razor",
@ -385,19 +418,7 @@
},
{
"$type": "Document",
"DocumentIndex": 13,
"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": "AgIAANwBAAAAAAAAAAAAAO4BAAAIAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-05-21T17:37:24.777Z"
},
{
"$type": "Document",
"DocumentIndex": 10,
"DocumentIndex": 12,
"Title": "ChatService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ChatService.cs",
"RelativeDocumentMoniker": "Services\\ChatService.cs",
@ -409,7 +430,7 @@
},
{
"$type": "Document",
"DocumentIndex": 12,
"DocumentIndex": 14,
"Title": "de.json",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Resources\\de.json",
"RelativeDocumentMoniker": "Resources\\de.json",
@ -421,7 +442,7 @@
},
{
"$type": "Document",
"DocumentIndex": 11,
"DocumentIndex": 13,
"Title": "en.json",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Resources\\en.json",
"RelativeDocumentMoniker": "Resources\\en.json",
@ -433,7 +454,7 @@
},
{
"$type": "Document",
"DocumentIndex": 16,
"DocumentIndex": 17,
"Title": "NavMenu.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\NavMenu.razor",
"RelativeDocumentMoniker": "Components\\Layout\\NavMenu.razor",
@ -445,7 +466,7 @@
},
{
"$type": "Document",
"DocumentIndex": 15,
"DocumentIndex": 16,
"Title": "MainLayout.razor.css",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Layout\\MainLayout.razor.css",
"RelativeDocumentMoniker": "Components\\Layout\\MainLayout.razor.css",
@ -457,7 +478,7 @@
},
{
"$type": "Document",
"DocumentIndex": 22,
"DocumentIndex": 23,
"Title": "DbAdmin.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\DbAdmin.razor",
"RelativeDocumentMoniker": "Components\\Pages\\DbAdmin.razor",
@ -469,7 +490,7 @@
},
{
"$type": "Document",
"DocumentIndex": 17,
"DocumentIndex": 18,
"Title": "ILLMBackend.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ILLMBackend.cs",
"RelativeDocumentMoniker": "Services\\ILLMBackend.cs",
@ -481,7 +502,7 @@
},
{
"$type": "Document",
"DocumentIndex": 18,
"DocumentIndex": 19,
"Title": "UserEntity.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\UserEntity.cs",
"RelativeDocumentMoniker": "Data\\UserEntity.cs",
@ -493,7 +514,7 @@
},
{
"$type": "Document",
"DocumentIndex": 21,
"DocumentIndex": 22,
"Title": "OllamaAssistant.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\OllamaAssistant.razor",
"RelativeDocumentMoniker": "Components\\Pages\\OllamaAssistant.razor",
@ -505,7 +526,7 @@
},
{
"$type": "Document",
"DocumentIndex": 27,
"DocumentIndex": 28,
"Title": "ProjectService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ProjectService.cs",
"RelativeDocumentMoniker": "Services\\ProjectService.cs",
@ -517,7 +538,7 @@
},
{
"$type": "Document",
"DocumentIndex": 28,
"DocumentIndex": 29,
"Title": "IOllamaSetupService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IOllamaSetupService.cs",
"RelativeDocumentMoniker": "Services\\IOllamaSetupService.cs",
@ -529,7 +550,7 @@
},
{
"$type": "Document",
"DocumentIndex": 24,
"DocumentIndex": 25,
"Title": "SafeFileHandle.Windows.cs",
"DocumentMoniker": "C:\\Users\\HP\\AppData\\Local\\SourceServer\\dddeb33c46e3c746b90c5f64463103c08ec03b9a85e92eab3b21c11218135d28\\src\\runtime\\src\\libraries\\System.Private.CoreLib\\src\\Microsoft\\Win32\\SafeHandles\\SafeFileHandle.Windows.cs",
"ToolTip": "C:\\Users\\HP\\AppData\\Local\\SourceServer\\dddeb33c46e3c746b90c5f64463103c08ec03b9a85e92eab3b21c11218135d28\\src\\runtime\\src\\libraries\\System.Private.CoreLib\\src\\Microsoft\\Win32\\SafeHandles\\SafeFileHandle.Windows.cs [Schreibgesch\u00FCtzt]",
@ -540,7 +561,7 @@
},
{
"$type": "Document",
"DocumentIndex": 25,
"DocumentIndex": 26,
"Title": "A7405018.jpg",
"DocumentMoniker": "C:\\Users\\HP\\Desktop\\Website\\A7405018.jpg",
"ToolTip": "C:\\Users\\HP\\Desktop\\Website\\A7405018.jpg",
@ -549,7 +570,7 @@
},
{
"$type": "Document",
"DocumentIndex": 29,
"DocumentIndex": 30,
"Title": "INSTALL.md",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\INSTALL.md",
"RelativeDocumentMoniker": "INSTALL.md",
@ -561,7 +582,7 @@
},
{
"$type": "Document",
"DocumentIndex": 30,
"DocumentIndex": 31,
"Title": "IThemeService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IThemeService.cs",
"RelativeDocumentMoniker": "Services\\IThemeService.cs",
@ -573,7 +594,7 @@
},
{
"$type": "Document",
"DocumentIndex": 31,
"DocumentIndex": 32,
"Title": "RedirectToLogin.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\RedirectToLogin.razor",
"RelativeDocumentMoniker": "Components\\RedirectToLogin.razor",
@ -585,7 +606,7 @@
},
{
"$type": "Document",
"DocumentIndex": 32,
"DocumentIndex": 33,
"Title": "ISettingsService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ISettingsService.cs",
"RelativeDocumentMoniker": "Services\\ISettingsService.cs",
@ -597,7 +618,7 @@
},
{
"$type": "Document",
"DocumentIndex": 33,
"DocumentIndex": 34,
"Title": "IRagService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IRagService.cs",
"RelativeDocumentMoniker": "Services\\IRagService.cs",
@ -609,7 +630,7 @@
},
{
"$type": "Document",
"DocumentIndex": 34,
"DocumentIndex": 35,
"Title": "ThemeService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\ThemeService.cs",
"RelativeDocumentMoniker": "Services\\ThemeService.cs",
@ -621,7 +642,7 @@
},
{
"$type": "Document",
"DocumentIndex": 35,
"DocumentIndex": 36,
"Title": "logo.jpg",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Assets\\logo.jpg",
"RelativeDocumentMoniker": "Assets\\logo.jpg",
@ -632,7 +653,7 @@
},
{
"$type": "Document",
"DocumentIndex": 36,
"DocumentIndex": 37,
"Title": "OllamaSettings.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\OllamaSettings.cs",
"RelativeDocumentMoniker": "Models\\OllamaSettings.cs",
@ -644,7 +665,7 @@
},
{
"$type": "Document",
"DocumentIndex": 26,
"DocumentIndex": 27,
"Title": "AppDbContext.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\AppDbContext.cs",
"RelativeDocumentMoniker": "Data\\AppDbContext.cs",
@ -655,7 +676,7 @@
},
{
"$type": "Document",
"DocumentIndex": 37,
"DocumentIndex": 38,
"Title": "FileScannerService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\FileScannerService.cs",
"RelativeDocumentMoniker": "Services\\FileScannerService.cs",
@ -667,7 +688,7 @@
},
{
"$type": "Document",
"DocumentIndex": 38,
"DocumentIndex": 39,
"Title": "TotpHelper.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\TotpHelper.cs",
"RelativeDocumentMoniker": "Services\\TotpHelper.cs",
@ -679,7 +700,7 @@
},
{
"$type": "Document",
"DocumentIndex": 39,
"DocumentIndex": 40,
"Title": "Project.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\Project.cs",
"RelativeDocumentMoniker": "Models\\Project.cs",
@ -691,7 +712,7 @@
},
{
"$type": "Document",
"DocumentIndex": 40,
"DocumentIndex": 41,
"Title": "RagService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\RagService.cs",
"RelativeDocumentMoniker": "Services\\RagService.cs",
@ -703,7 +724,7 @@
},
{
"$type": "Document",
"DocumentIndex": 41,
"DocumentIndex": 42,
"Title": "App.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\App.razor",
"RelativeDocumentMoniker": "Components\\App.razor",
@ -715,7 +736,7 @@
},
{
"$type": "Document",
"DocumentIndex": 42,
"DocumentIndex": 43,
"Title": "Models.razor",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Components\\Pages\\Models.razor",
"RelativeDocumentMoniker": "Components\\Pages\\Models.razor",
@ -727,7 +748,7 @@
},
{
"$type": "Document",
"DocumentIndex": 43,
"DocumentIndex": 44,
"Title": "LLM_Model.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\LLM_Model.cs",
"RelativeDocumentMoniker": "Models\\LLM_Model.cs",
@ -739,7 +760,7 @@
},
{
"$type": "Document",
"DocumentIndex": 44,
"DocumentIndex": 45,
"Title": "ModelStatus.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Models\\ModelStatus.cs",
"RelativeDocumentMoniker": "Models\\ModelStatus.cs",
@ -751,7 +772,7 @@
},
{
"$type": "Document",
"DocumentIndex": 45,
"DocumentIndex": 46,
"Title": "IFileScannerService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IFileScannerService.cs",
"RelativeDocumentMoniker": "Services\\IFileScannerService.cs",
@ -763,7 +784,7 @@
},
{
"$type": "Document",
"DocumentIndex": 46,
"DocumentIndex": 47,
"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",
@ -775,7 +796,7 @@
},
{
"$type": "Document",
"DocumentIndex": 47,
"DocumentIndex": 48,
"Title": "ProjectEntity.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Data\\ProjectEntity.cs",
"RelativeDocumentMoniker": "Data\\ProjectEntity.cs",
@ -787,7 +808,7 @@
},
{
"$type": "Document",
"DocumentIndex": 48,
"DocumentIndex": 49,
"Title": "IProjectService.cs",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\Services\\IProjectService.cs",
"RelativeDocumentMoniker": "Services\\IProjectService.cs",
@ -799,7 +820,7 @@
},
{
"$type": "Document",
"DocumentIndex": 49,
"DocumentIndex": 50,
"Title": "appsettings.json",
"DocumentMoniker": "E:\\ollama_net\\ollama\\AuthorBuddy.Web\\appsettings.json",
"RelativeDocumentMoniker": "appsettings.json",

View file

@ -26,6 +26,30 @@
else
{
<div class="page">
@if (OpState.IsBusy)
{
<div class="llm-overlay">
<div class="llm-modal">
<div class="llm-title">
<span class="llm-title-spinner"></span>
@Loc["llm.overlay.title"]
</div>
<div class="llm-body">
<div class="llm-status">@OpState.StatusText</div>
@if (!string.IsNullOrEmpty(OpState.ThoughtText))
{
<div class="llm-thoughts">@OpState.ThoughtText</div>
}
else
{
<div class="llm-thoughts llm-thoughts-empty">@Loc["llm.overlay.placeholder"]</div>
}
</div>
</div>
</div>
}
<div class="main-content">
@if (_isAuthenticated)
{
@ -468,6 +492,7 @@ else
private bool _showExportDialog;
private string exportFileName = string.Empty;
private string exportFormat = "Html";
private long _lastThoughtScrollTicks;
[Inject]
private NavigationManager Navigation { get; set; } = default!;
@ -554,6 +579,16 @@ else
_isLoading = false;
StateHasChanged();
}
if (OpState.IsBusy && OpState.ThoughtText.Length > 0)
{
var now = DateTime.UtcNow.Ticks;
if (now - _lastThoughtScrollTicks > TimeSpan.TicksPerMillisecond * 80)
{
_lastThoughtScrollTicks = now;
await JS.InvokeVoidAsync("eval", "var el=document.querySelector('.llm-thoughts'); if(el){el.scrollTop=el.scrollHeight;}");
}
}
}
public void Dispose()
@ -570,6 +605,9 @@ else
{
await LoadProjects();
await LoadProjectContext();
// Save current page to localStorage
await JS.InvokeVoidAsync("localStorage.setItem", "last_visited_page", Navigation.Uri);
}
StateHasChanged();
}

View file

@ -7,6 +7,7 @@
@inject IAuthService AuthService
@inject NavigationManager Navigation
@inject IJSRuntime JS
@inject IFactCheckService FactCheckService
@rendermode InteractiveServer
@ -38,6 +39,12 @@
<span>kreativ (2)</span>
</div>
</div>
<div class="form-group">
<label class="factcheck-toggle">
<input type="checkbox" @bind="_factCheckEnabled" />
Antwort automatisch per DuckDuckGo verifizieren
</label>
</div>
<div class="form-group">
<button class="btn btn-sm btn-primary" @onclick="SaveSettings">System-Prompt & Temperatur speichern</button>
<button class="btn btn-sm" @onclick="ResetToDefault">Auf Standard zurücksetzen</button>
@ -50,22 +57,36 @@
{
<div class="chat-empty">Noch keine Nachrichten. Stelle eine Frage.</div>
}
@foreach (var msg in _messages)
@for (var i = 0; i < _messages.Count; i++)
{
var msg = _messages[i];
var checks = _factChecks.Count > i ? _factChecks[i] : null;
<div class="chat-message chat-user">
<div class="chat-avatar">U</div>
<div class="chat-bubble">@msg.UserMessage</div>
</div>
<div class="chat-message chat-assistant">
<div class="chat-avatar">A</div>
<div class="chat-bubble">@MarkdownToHtml(msg.AssistantResponse)</div>
<div class="chat-bubble">
@RenderAssistantMessage(msg.AssistantResponse)
@if (checks != null && checks.Count > 0)
{
<div class="factcheck-section">
<div class="factcheck-title">Faktenprüfung</div>
@foreach (var check in checks)
{
@RenderFactCheck(check)
}
</div>
}
</div>
</div>
}
@if (_isBusy)
{
<div class="chat-message chat-assistant">
<div class="chat-avatar">A</div>
<div class="chat-bubble chat-thinking">…</div>
<div class="chat-bubble">@RenderAssistantMessage(_streamingResponse, true)</div>
</div>
}
</div>
@ -97,6 +118,9 @@
private string _systemPrompt = string.Empty;
private double _temperature = 0.7;
private bool _needsScroll;
private string _streamingResponse = string.Empty;
private bool _factCheckEnabled;
private List<List<FactCheckResult>?> _factChecks = new();
protected override async Task OnInitializedAsync()
{
@ -108,9 +132,11 @@
}
_userId = user.Id;
_messages = await ChatService.GetHistoryAsync(_userId);
_factChecks = _messages.Select(_ => (List<FactCheckResult>?)null).ToList();
_systemPrompt = await SettingsService.GetChatPromptAsync()
?? Settings.GetChatDefaultPrompt(Settings.Language);
_temperature = await SettingsService.GetChatTempAsync();
_factCheckEnabled = await SettingsService.GetChatFactCheckEnabledAsync();
_needsScroll = true;
}
@ -132,6 +158,7 @@
{
await SettingsService.SaveChatPromptAsync(_systemPrompt);
await SettingsService.SaveChatTempAsync(_temperature);
await SettingsService.SaveChatFactCheckEnabledAsync(_factCheckEnabled);
}
private async Task ResetToDefault()
@ -157,22 +184,40 @@
_input = string.Empty;
_error = string.Empty;
_isBusy = true;
_streamingResponse = string.Empty;
_needsScroll = true;
try
{
var response = await OllamaService.Chat(Settings.StyleModel, text, _systemPrompt, _temperature);
var model = await SettingsService.GetStyleModelAsync() ?? "llama3.2";
await OllamaService.ChatStream(model, text, async chunk =>
{
_streamingResponse += chunk;
_needsScroll = true;
await InvokeAsync(StateHasChanged);
}, _systemPrompt, _temperature, Settings.StyleBackend);
var msg = new ChatEntity
{
UserId = _userId,
UserMessage = text,
AssistantResponse = response,
Model = Settings.StyleModel,
AssistantResponse = _streamingResponse,
Model = model,
Timestamp = DateTime.UtcNow
};
await ChatService.AddMessageAsync(msg);
_messages.Add(msg);
if (_factCheckEnabled && !string.IsNullOrWhiteSpace(_streamingResponse))
{
_needsScroll = true;
_factChecks.Add(await FactCheckService.VerifyAsync(_streamingResponse, Settings.StyleBackend));
}
else
{
_factChecks.Add(null);
}
_needsScroll = true;
}
catch (Exception ex)
@ -182,7 +227,6 @@
finally
{
_isBusy = false;
_input = string.Empty;
_needsScroll = true;
}
}
@ -198,13 +242,99 @@
await JS.InvokeVoidAsync("eval", "document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight");
}
private MarkupString MarkdownToHtml(string text)
private static (string thinking, string answer) ParseResponse(string text)
{
if (string.IsNullOrEmpty(text)) return new MarkupString(string.Empty);
var html = System.Web.HttpUtility.HtmlEncode(text);
html = System.Text.RegularExpressions.Regex.Replace(html, @"\*\*(.+?)\*\*", "<strong>$1</strong>");
html = System.Text.RegularExpressions.Regex.Replace(html, @"\*(.+?)\*", "<em>$1</em>");
html = html.Replace("\n", "<br />");
return new MarkupString(html);
if (string.IsNullOrEmpty(text)) return ("", "");
var closeTag = "</thinking>";
var closeIdx = text.IndexOf(closeTag, StringComparison.Ordinal);
if (closeIdx >= 0)
{
var openTag = "<thinking>";
var openIdx = text.IndexOf(openTag, StringComparison.Ordinal);
if (openIdx >= 0 && openIdx < closeIdx)
{
var thinking = text[(openIdx + openTag.Length)..closeIdx].Trim();
var answer = text[(closeIdx + closeTag.Length)..].Trim();
return (thinking, answer);
}
}
var lastOpenIdx = text.LastIndexOf("<thinking>", StringComparison.Ordinal);
if (lastOpenIdx >= 0)
return (text[(lastOpenIdx + "<thinking>".Length)..], "");
return ("", text);
}
private MarkupString RenderAssistantMessage(string text, bool isStreaming = false)
{
if (string.IsNullOrEmpty(text))
return new MarkupString(isStreaming ? "<span class=\"chat-thinking\">…</span>" : string.Empty);
var (thinking, answer) = ParseResponse(text);
var html = new System.Text.StringBuilder();
if (!string.IsNullOrEmpty(thinking))
{
html.Append("<details class=\"thinking-block\"");
if (isStreaming || !string.IsNullOrEmpty(answer))
html.Append(" open");
html.Append("><summary>Gedankengang</summary><div class=\"thinking-content\">");
html.Append(System.Web.HttpUtility.HtmlEncode(thinking));
html.Append("</div></details>");
}
if (!string.IsNullOrEmpty(answer))
{
var encoded = System.Web.HttpUtility.HtmlEncode(answer);
encoded = System.Text.RegularExpressions.Regex.Replace(encoded, @"\*\*(.+?)\*\*", "<strong>$1</strong>");
encoded = System.Text.RegularExpressions.Regex.Replace(encoded, @"\*(.+?)\*", "<em>$1</em>");
encoded = encoded.Replace("\n", "<br />");
html.Append(encoded);
}
else if (string.IsNullOrEmpty(thinking))
{
html.Append("<span class=\"chat-thinking\">…</span>");
}
return new MarkupString(html.ToString());
}
private MarkupString RenderFactCheck(FactCheckResult check)
{
var (icon, label, cssClass) = check.Verdict switch
{
FactCheckVerdict.Confirmed => ("✓", "Bestätigt", "verdict-confirmed"),
FactCheckVerdict.Contradicted => ("✗", "Widersprochen", "verdict-contradicted"),
FactCheckVerdict.Partial => ("⚠", "Teilweise bestätigt", "verdict-partial"),
_ => ("?", "Keine Information", "verdict-none")
};
var html = new System.Text.StringBuilder();
html.Append("<div class=\"factcheck-item\">");
html.Append($"<div class=\"factcheck-header\"><span class=\"factcheck-verdict {cssClass}\">{icon} {label}</span></div>");
html.Append("<div class=\"factcheck-claim\">");
html.Append(System.Web.HttpUtility.HtmlEncode(check.Claim));
html.Append("</div>");
if (!string.IsNullOrEmpty(check.Reasoning))
{
html.Append("<div class=\"factcheck-reasoning\">");
html.Append(System.Web.HttpUtility.HtmlEncode(check.Reasoning));
html.Append("</div>");
}
if (check.Sources.Count > 0)
{
html.Append("<div class=\"factcheck-sources\">");
foreach (var source in check.Sources)
{
html.Append("<div class=\"factcheck-source\">");
html.Append(System.Web.HttpUtility.HtmlEncode(source));
html.Append("</div>");
}
html.Append("</div>");
}
html.Append("</div>");
return new MarkupString(html.ToString());
}
}

View file

@ -4,7 +4,7 @@
@using System.Threading
@inject IOllamaService OllamaService
@inject IProjectService ProjectService
@inject IRagService RagService
@inject ILlmWikiService WikiService
@inject ISettingsService SettingsService
@inject IWikipediaService WikipediaService
@inject NavigationManager Navigation
@ -62,7 +62,7 @@
<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>
<button @onclick="CompileWiki" disabled="@isChecking">@Loc["editor.button.compile_wiki"]</button>
</div>
@if (showVersionDialog)
@ -87,7 +87,7 @@
<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="QueryWiki" disabled=@buttonDisabled>@Loc["editor.button.search_wiki"]</button>
<button @onclick="SearchWikipedia" disabled=@buttonDisabled>@Loc["editor.button.search_wikipedia"]</button>
</div>
@ -153,18 +153,11 @@
<button class="btn btn-secondary" @onclick="CloseVersionBrowser">@Loc["common.cancel"]</button>
</div>
}
else if (ragResults != null && ragResults.Count > 0)
else if (!string.IsNullOrEmpty(wikiResult))
{
<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>
}
<h4>@Loc["editor.wiki.heading"]</h4>
<div class="wiki-result">@Markup(wikiResult)</div>
</div>
}
else if (wikipediaResults != null && wikipediaResults.Count > 0)
@ -261,7 +254,7 @@
private string originalText = string.Empty;
private string correctedText = string.Empty;
private bool showSpellDiff;
private List<RagSearchResult>? ragResults;
private string? wikiResult;
private List<WikipediaResult>? wikipediaResults;
private int currentProjectId;
@ -414,7 +407,7 @@
showSpellDiff = false;
showVersionBrowser = false;
showVersionDiff = false;
ragResults = null;
wikiResult = null;
wikipediaResults = null;
analysisResult = string.Empty;
}
@ -464,7 +457,7 @@
correctedText += chunk;
OpState.AppendThought(chunk);
await InvokeAsync(StateHasChanged);
}, OllamaSettings.SpellingPrompt, OllamaSettings.SpellingTemp);
}, OllamaSettings.SpellingPrompt, OllamaSettings.SpellingTemp, OllamaSettings.SpellingBackend);
showSpellDiff = true;
OpState.SetStatus(Loc["common.status.ready"], false);
@ -493,12 +486,12 @@
statusText = Loc["editor.status.corrections_applied"];
}
private async Task RagSearch()
private async Task QueryWiki()
{
if (string.IsNullOrWhiteSpace(searchQuery)) return;
isChecking = true;
statusText = Loc["editor.status.searching_rag"];
statusText = Loc["editor.status.searching_wiki"];
ClearResults();
analysisResult = string.Empty;
@ -506,20 +499,16 @@
{
if (currentProjectId == 0)
{
analysisResult = Loc["editor.rag.error.no_project"];
analysisResult = Loc["editor.wiki.error.no_project"];
return;
}
ragResults = await RagService.SearchAsync(currentProjectId, searchQuery, 5);
if (ragResults.Count == 0)
{
analysisResult = Loc["editor.rag.error.no_facts"];
}
var answer = await WikiService.QueryWikiAsync(currentProjectId, searchQuery);
wikiResult = answer;
}
catch (Exception ex)
{
analysisResult = $"{Loc["editor.rag.error.generic"]} {ex.Message}";
analysisResult = $"{Loc["editor.wiki.error.generic"]} {ex.Message}";
}
finally
{
@ -682,18 +671,18 @@
}
}
private async Task IndexProject()
private async Task CompileWiki()
{
if (currentProjectId == 0)
{
analysisResult = Loc["editor.index.error.no_project"];
analysisResult = Loc["editor.wiki.error.no_project"];
return;
}
isChecking = true;
statusText = Loc["editor.status.indexing"];
statusText = Loc["editor.status.compiling_wiki"];
analysisResult = string.Empty;
ragResults = null;
wikiResult = null;
try
{
@ -701,7 +690,7 @@
var project = projects.FirstOrDefault(p => p.Id == currentProjectId);
if (project == null)
{
analysisResult = Loc["editor.index.error.not_found"];
analysisResult = Loc["editor.wiki.error.not_found"];
return;
}
@ -711,12 +700,12 @@
InvokeAsync(StateHasChanged);
});
await RagService.IndexProjectAsync(currentProjectId, project.RootPath, p);
analysisResult = Loc["editor.index.status.success"];
await WikiService.CompileWikiAsync(currentProjectId, project.RootPath, p);
analysisResult = Loc["editor.wiki.status.success"];
}
catch (Exception ex)
{
analysisResult = $"{Loc["editor.index.error.generic"]} {ex.Message}";
analysisResult = $"{Loc["editor.wiki.error.generic"]} {ex.Message}";
}
finally
{
@ -900,4 +889,6 @@
comment
);
}
private static MarkupString Markup(string text) => new(text);
}

View file

@ -6,39 +6,18 @@
<PageTitle>@Loc["page.title.home"]</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>
<div class="tab-content">
@if (activeTab == "rag")
@if (styleSuggestions.Length == 0)
{
@if (foundFacts.Count == 0)
{
<p style="color: var(--border-color);">@Loc["home.rag.empty"]</p>
<p style="color: var(--border-color);">Willkommen bei Author Buddy. Öffne ein Projekt im Editor, um loszulegen.</p>
}
else
{
@foreach (var fact in foundFacts)
{
<div class="fact-card">@fact</div>
}
}
}
else if (activeTab == "style")
{
<h3>@Loc["home.tab.style_analysis"]</h3>
<textarea style="width: 100%; min-height: 300px; background: var(--editor-bg); color: var(--text-color); border: 1px solid var(--border-color); padding: 8px; font-family: Consolas;" readonly>@styleSuggestions</textarea>
}
</div>
@code {
private string activeTab = "rag";
private List<string> foundFacts = new();
private string styleSuggestions = string.Empty;
private string GetTabClass(string tab)
{
return activeTab == tab ? "active" : "";
}
}

View file

@ -7,6 +7,15 @@
<PageTitle>@Loc["page.title.models"]</PageTitle>
@if (_isCloudBackend)
{
<div style="padding: 24px; text-align: center; color: var(--text-muted);">
<p>@Loc["models.cloud.not_available"]</p>
<p style="font-size: 12px; margin-top: 8px;">@Loc["models.cloud.hint"]</p>
</div>
}
else
{
<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>
@ -87,6 +96,7 @@ else
</div>
</div>
}
}
@code {
private List<LLM_Model> localModels = new();
@ -100,9 +110,12 @@ else
private string? pullResult;
private string pullPercentDisplay => $"{pullProgress:F0}%";
private string? _confirmDeleteModel;
private bool _isCloudBackend;
protected override async Task OnInitializedAsync()
{
_isCloudBackend = !OllamaService.SupportsModelManagement;
if (!_isCloudBackend)
await RefreshModels();
}

View file

@ -2,11 +2,12 @@
@attribute [Authorize]
@inject IOllamaService OllamaService
@inject ISettingsService SettingsService
@inject IRagService RagService
@inject ILlmWikiService WikiService
@inject IProjectService ProjectService
@inject NavigationManager Navigation
@inject ILocalizationService Loc
@inject OperationState OpState
@inject OllamaSettings OllamaConfig
<PageTitle>@Loc["page.title.assistant"]</PageTitle>
@ -18,7 +19,7 @@
<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>
<option value="wiki">@Loc["assistant.mode.wiki"]</option>
@if (currentTemplate == ProjectTemplate.Technical)
{
<option disabled style="font-style:italic;">── Requirements ──</option>
@ -46,7 +47,7 @@
}
</div>
@if (showPromptEditor && selectedMode != "rag")
@if (showPromptEditor && selectedMode != "wiki")
{
<div class="assistant-prompt-editor">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
@ -131,7 +132,7 @@
private async Task LoadPromptForMode()
{
if (selectedMode == "rag")
if (selectedMode == "wiki")
{
promptOverride = string.Empty;
temperatureOverride = 0.3;
@ -186,7 +187,7 @@
try
{
if (selectedMode == "rag")
if (selectedMode == "wiki")
{
var projectId = SettingsService.CurrentProjectId;
if (projectId == null)
@ -197,18 +198,10 @@
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);
var answer = await WikiService.QueryWikiAsync(projectId.Value, inputText);
progress = 100;
outputText = answer;
OpState.SetThought(answer);
statusText = Loc["assistant.status.done"];
OpState.SetStatus(Loc["assistant.status.done"], false);
}
@ -254,7 +247,7 @@
outputText += chunk;
OpState.AppendThought(chunk);
await InvokeAsync(StateHasChanged);
}, systemPrompt, temperatureOverride);
}, systemPrompt, temperatureOverride, OllamaConfig.StyleBackend);
statusText = Loc["assistant.status.done"];
OpState.SetStatus(Loc["assistant.status.done"], false);

View file

@ -6,12 +6,55 @@
@inject IAuthService AuthService
@inject ISettingsService SettingsService
@inject IQuickMarkdownService QuickMarkdownService
@inject IPluginManager PluginManager
@inject NavigationManager Navigation
@rendermode InteractiveServer
@inject IFactSearchToolService FactSearchToolService
<PageTitle>Quick Markdown</PageTitle>
<style>
.qm-fullscreen-textarea {
width: 100%;
height: calc(100vh - 140px);
font-size: 16px;
}
.qm-fullscreen-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
background: var(--bs-dark);
color: var(--bs-light);
}
.qm-fullscreen-container .qm-container {
max-width: 100%;
margin: 0;
height: 100%;
display: flex;
flex-direction: column;
}
.qm-fullscreen-container .qm-output-panel {
height: 40%;
border-top: 1px solid var(--bs-gray);
}
.qm-fullscreen-container .qm-input-panel {
height: 60%;
overflow-y: auto;
}
.qm-fullscreen-container .qm-prompt-selector,
.qm-fullscreen-container .qm-generate-row,
.qm-fullscreen-container .qm-range-labels {
display: none;
}
</style>
<div class="qm-container">
<h2>Quick Markdown</h2>
<p>Erzeugt aus einem Text und einem System-Prompt direkt formatiertes Markdown – ohne Projekt oder Speicherung.</p>
@ -29,6 +72,7 @@
<option value="@p.Id">@p.Name</option>
}
</select>
<button class="btn btn-sm @(_fullscreenMode ? "btn-primary" : "")" @onclick="ToggleFullscreen">Vollbild</button>
<button class="btn btn-sm" @onclick="ShowSavePromptDialog">+ Als Prompt speichern</button>
<button class="btn btn-sm" @onclick="DeleteCurrentPrompt" disabled="@(_selectedPromptId == 0)">Löschen</button>
</div>
@ -44,12 +88,12 @@
<div class="form-group">
<label>System-Prompt</label>
<textarea class="qm-textarea qm-prompt" @bind="_systemPrompt" rows="4"></textarea>
<textarea class="qm-textarea qm-prompt @(_fullscreenMode ? "qm-fullscreen-textarea" : "")" @bind="_systemPrompt" @bind:after="PersistPromptDraft" rows="4"></textarea>
</div>
<div class="form-group">
<label for="tempRange">Temperatur: @_temperature.ToString("F2")</label>
<input id="tempRange" type="range" min="0" max="2" step="0.05" @bind="_temperature" />
<input id="tempRange" type="range" min="0" max="2" step="0.05" @bind="_temperature" @bind:event="oninput" @bind:after="PersistPromptDraft" />
<div class="qm-range-labels">
<span>präzise (0)</span>
<span>kreativ (2)</span>
@ -58,7 +102,7 @@
<div class="form-group">
<label>Text</label>
<textarea class="qm-textarea" @bind="_inputText" @bind:event="oninput" rows="12" placeholder="Text hier eingeben…"></textarea>
<textarea class="qm-textarea" @bind="_inputText" @bind:event="oninput" @bind:after="PersistTextDraft" rows="12" placeholder="Text hier eingeben…"></textarea>
</div>
<div class="qm-generate-row">
@ -75,6 +119,9 @@
<button class="btn btn-sm" @onclick="SaveEntry" disabled="@_saveEntryDisabled">
Eingabe & Ergebnis speichern
</button>
<button class="btn btn-sm" @onclick="StartFactSearch" disabled="@(_isBusy || string.IsNullOrWhiteSpace(_inputText))" title="Prüft die Zeile mit [Fakt] im Text per Websuche">
🔍 Fakten prüfen
</button>
</div>
</div>
@ -84,7 +131,24 @@
<div class="qm-output-toolbar">
<button class="btn btn-small" @onclick="CopyOutput" disabled="@string.IsNullOrEmpty(_output)">📋 Kopieren</button>
<button class="btn btn-small" @onclick="ClearOutput">🗑 Leeren</button>
@if (_availablePlugins.Count > 0)
{
<select @bind="_selectedPluginId" style="flex:1;max-width:180px;font-size:12px;">
<option value="">Export via Plugin…</option>
@foreach (var p in _availablePlugins)
{
<option value="@p.Id">@p.Name</option>
}
</select>
<button class="btn btn-small btn-primary" @onclick="ExportViaPlugin" disabled="@(_selectedPluginId == "" || string.IsNullOrEmpty(_output) || _isExporting)">
@(_isExporting ? "Exportiere…" : "Export")
</button>
}
</div>
@if (!string.IsNullOrEmpty(_exportResult))
{
<div class="qm-notice" style="margin-top:4px;font-size:12px;">@_exportResult</div>
}
</div>
<textarea class="qm-textarea qm-output" @bind="_output" rows="30" readonly></textarea>
</div>
@ -116,9 +180,83 @@
{
<div class="qm-notice">@_modelNotice</div>
}
@if (_showFactSearch)
{
<div class="qm-fact-drawer">
<div class="qm-fact-drawer-header">
<span>🔍 Fakten prüfen</span>
<button class="btn btn-sm" @onclick="CloseFactSearch">✕</button>
</div>
<div class="qm-fact-drawer-body">
<div class="form-group">
<label>Behauptung</label>
<div class="qm-fact-claim">@_factText</div>
</div>
<div class="form-group">
<label for="factQuery">Suchanfrage</label>
<div class="qm-fact-query-row">
<input id="factQuery" @bind="_searchQuery" @bind:event="oninput"
placeholder="Suchanfrage bearbeiten…" />
<button class="btn btn-sm btn-primary" @onclick="RunFactSearch"
disabled="@(_factSearching || string.IsNullOrWhiteSpace(_searchQuery))">
Suchen
</button>
</div>
<button class="btn btn-sm" @onclick="RegenerateFactQuery" disabled="@_factSearching">
Anfrage neu generieren
</button>
</div>
@if (_factSearching)
{
<div class="qm-fact-loading">Suche läuft…</div>
}
@if (!string.IsNullOrEmpty(_factSearchError))
{
<div class="qm-fact-error">@_factSearchError</div>
}
@if (_factResults.Count > 0)
{
<div class="qm-fact-results">
@foreach (var r in _factResults)
{
<div class="qm-fact-result">
<a href="@r.Url" target="_blank" rel="noopener noreferrer">@r.Title</a>
@if (!string.IsNullOrEmpty(r.Snippet))
{
<div class="qm-fact-snippet">@r.Snippet</div>
}
<div class="qm-fact-result-actions">
<button class="btn btn-sm btn-primary" @onclick="() => SelectFactResult(r)"
disabled="@_factSummarizing">
@(_factSummarizing ? "Fasse zusammen…" : "Übernehmen")
</button>
</div>
</div>
}
</div>
}
else if (!_factSearching && _factSearchSearched && _factSearchError.Length == 0)
{
<div class="qm-fact-hint">Keine Ergebnisse gefunden. Versuche die Suchanfrage anzupassen.</div>
}
else if (!_factSearching && !_factSearchSearched)
{
<div class="qm-fact-hint">Klicke auf „Suchen", um Ergebnisse zu laden.</div>
}
</div>
</div>
}
</div>
@code {
private const string DraftKeyText = "qm_draft_text";
private const string DraftKeyPrompt = "qm_draft_prompt";
private const string DraftKeyTemp = "qm_draft_temp";
private const string DraftKeyPromptId = "qm_draft_prompt_id";
private bool _fullscreenMode;
private string _systemPrompt = string.Empty;
private string _inputText = string.Empty;
private string _output = string.Empty;
@ -134,6 +272,20 @@
private int _selectedPromptId;
private bool _showPromptNameInput;
private string _newPromptName = string.Empty;
private List<IExportPlugin> _availablePlugins = new();
private string _selectedPluginId = string.Empty;
private bool _isExporting;
private string _exportResult = string.Empty;
private bool _draftRestored;
private bool _showFactSearch;
private bool _factSearching;
private bool _factSearchSearched;
private bool _factSummarizing;
private string _factSearchError = string.Empty;
private string _factLine = string.Empty;
private string _factText = string.Empty;
private string _searchQuery = string.Empty;
private List<FactSearchResult> _factResults = new();
protected override async Task OnInitializedAsync()
{
@ -150,10 +302,77 @@
?? Settings.GetQuickMarkdownDefaultPrompt(Settings.Language);
_temperature = await SettingsService.GetQuickMarkdownTempAsync();
_savedEntries = await QuickMarkdownService.GetAllAsync(_userId);
_availablePlugins = PluginManager.GetPlugins().ToList();
Navigation.LocationChanged += OnLocationChanged;
_modelNotice = $"Verwendetes Modell: {Settings.StyleModel} (via {Settings.Backend})";
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && !_draftRestored)
{
_draftRestored = true;
await RestoreDraftAsync();
await InvokeAsync(StateHasChanged);
}
}
public void Dispose()
{
Navigation.LocationChanged -= OnLocationChanged;
}
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
{
_ = PersistDraftAsync();
}
private async Task PersistTextDraft()
{
await JS.InvokeVoidAsync("localStorage.setItem", DraftKeyText, _inputText ?? string.Empty);
}
private async Task PersistPromptDraft()
{
await JS.InvokeVoidAsync("localStorage.setItem", DraftKeyPrompt, _systemPrompt ?? string.Empty);
await JS.InvokeVoidAsync("localStorage.setItem", DraftKeyTemp, _temperature.ToString(System.Globalization.CultureInfo.InvariantCulture));
await JS.InvokeVoidAsync("localStorage.setItem", DraftKeyPromptId, _selectedPromptId.ToString());
}
private async Task PersistDraftAsync()
{
await PersistTextDraft();
await PersistPromptDraft();
}
private async Task RestoreDraftAsync()
{
var text = await JS.InvokeAsync<string>("localStorage.getItem", DraftKeyText);
if (!string.IsNullOrEmpty(text))
_inputText = text;
var promptIdStr = await JS.InvokeAsync<string>("localStorage.getItem", DraftKeyPromptId);
if (int.TryParse(promptIdStr, out var promptId) && _savedPrompts.Any(p => p.Id == promptId))
{
_selectedPromptId = promptId;
var prompt = _savedPrompts.First(p => p.Id == promptId);
_systemPrompt = prompt.PromptText;
_temperature = prompt.Temperature;
}
else
{
var prompt = await JS.InvokeAsync<string>("localStorage.getItem", DraftKeyPrompt);
if (!string.IsNullOrEmpty(prompt))
_systemPrompt = prompt;
var tempStr = await JS.InvokeAsync<string>("localStorage.getItem", DraftKeyTemp);
if (double.TryParse(tempStr, System.Globalization.CultureInfo.InvariantCulture, out var temp))
_temperature = temp;
}
}
private async Task SwitchPrompt(ChangeEventArgs e)
{
var id = int.Parse(e.Value?.ToString() ?? "0");
@ -172,6 +391,8 @@
_temperature = prompt.Temperature;
}
}
await PersistPromptDraft();
}
private void ShowSavePromptDialog()
@ -185,6 +406,20 @@
if (string.IsNullOrWhiteSpace(_newPromptName))
return;
if (_fullscreenMode && _selectedPromptId > 0)
{
var prompt = _savedPrompts.FirstOrDefault(p => p.Id == _selectedPromptId);
if (prompt != null)
{
prompt.PromptText = _systemPrompt;
prompt.Temperature = _temperature;
prompt.Timestamp = DateTime.UtcNow;
await QuickMarkdownService.SavePromptAsync(prompt);
_selectedPromptId = prompt.Id;
}
}
else
{
var existing = _savedPrompts.FirstOrDefault(p =>
p.Name == _newPromptName && p.Id != _selectedPromptId);
if (existing != null)
@ -208,10 +443,13 @@
await QuickMarkdownService.SavePromptAsync(prompt);
_selectedPromptId = prompt.Id;
}
}
_savedPrompts = await QuickMarkdownService.GetPromptsAsync(_userId);
_showPromptNameInput = false;
_newPromptName = string.Empty;
await PersistPromptDraft();
}
private void CancelSavePrompt()
@ -220,6 +458,11 @@
_newPromptName = string.Empty;
}
private void ToggleFullscreen()
{
_fullscreenMode = !_fullscreenMode;
}
private async Task DeleteCurrentPrompt()
{
if (_selectedPromptId == 0)
@ -230,6 +473,8 @@
_selectedPromptId = 0;
_systemPrompt = Settings.GetQuickMarkdownDefaultPrompt(Settings.Language);
_temperature = Settings.QuickMarkdownTemp;
await PersistPromptDraft();
}
private async Task SaveEntry()
@ -263,6 +508,8 @@
_systemPrompt = entry.SystemPrompt;
_temperature = entry.Temperature;
}
await PersistDraftAsync();
}
private async Task DeleteEntry(int id)
@ -275,13 +522,15 @@
{
if (string.IsNullOrWhiteSpace(_inputText)) return;
await PersistDraftAsync();
_isBusy = true;
_error = string.Empty;
_output = string.Empty;
try
{
var result = await OllamaService.Chat(Settings.StyleModel, _inputText, _systemPrompt, _temperature);
var result = await OllamaService.Chat(Settings.StyleModel, _inputText, _systemPrompt, _temperature, Settings.StyleBackend);
_output = result;
}
catch (Exception ex)
@ -305,4 +554,181 @@
_output = string.Empty;
_error = string.Empty;
}
private async Task StartFactSearch()
{
if (_isBusy || string.IsNullOrWhiteSpace(_inputText)) return;
var found = FindFirstFact(_inputText);
if (found == null)
{
_error = "Keine Zeile mit [Fakt] gefunden. Schreibe in eine Zeile z.B.: [Fakt] Paris ist die Hauptstadt von Frankreich.";
return;
}
_error = string.Empty;
_factLine = found.Value.Line;
_factText = found.Value.Fact;
_showFactSearch = true;
_factResults = new();
_factSearchError = string.Empty;
_factSearchSearched = false;
_factSearching = true;
await InvokeAsync(StateHasChanged);
try
{
_searchQuery = await FactSearchToolService.CreateSearchQueryAsync(_factText);
await SearchCoreAsync();
}
catch (Exception ex)
{
_factSearchError = $"Fehler: {ex.Message}";
}
finally
{
_factSearching = false;
}
}
private async Task RunFactSearch()
{
if (_factSearching || string.IsNullOrWhiteSpace(_searchQuery)) return;
_factSearching = true;
_factSearchError = string.Empty;
_factSearchSearched = false;
await InvokeAsync(StateHasChanged);
try
{
await SearchCoreAsync();
}
catch (Exception ex)
{
_factSearchError = $"Fehler: {ex.Message}";
}
finally
{
_factSearching = false;
}
}
private async Task RegenerateFactQuery()
{
if (_factSearching) return;
_factSearching = true;
_factSearchError = string.Empty;
await InvokeAsync(StateHasChanged);
try
{
_searchQuery = await FactSearchToolService.CreateSearchQueryAsync(_factText);
_factSearchSearched = false;
await SearchCoreAsync();
}
catch (Exception ex)
{
_factSearchError = $"Fehler: {ex.Message}";
}
finally
{
_factSearching = false;
}
}
private async Task SearchCoreAsync()
{
_factResults = await FactSearchToolService.SearchAsync(_searchQuery);
_factSearchSearched = true;
}
private async Task SelectFactResult(FactSearchResult result)
{
if (_factSummarizing) return;
_factSummarizing = true;
_factSearchError = string.Empty;
await InvokeAsync(StateHasChanged);
try
{
if (!string.IsNullOrWhiteSpace(result.Url))
await JS.InvokeVoidAsync("window.open", result.Url, "_blank");
var info = string.IsNullOrWhiteSpace(result.Snippet) ? result.Title : result.Snippet;
var summary = await FactSearchToolService.SummarizeAsync(_searchQuery, info, result.Url);
var replacement = string.IsNullOrWhiteSpace(summary)
? $"({result.Title}, Quelle: {result.Url})"
: $"{summary} (Quelle: {result.Url})";
_inputText = _inputText.Replace(_factLine, replacement);
await PersistDraftAsync();
_showFactSearch = false;
}
catch (Exception ex)
{
_factSearchError = $"Fehler: {ex.Message}";
}
finally
{
_factSummarizing = false;
}
}
private void CloseFactSearch()
{
_showFactSearch = false;
}
private static (string Line, string Fact)? FindFirstFact(string text)
{
if (string.IsNullOrEmpty(text)) return null;
foreach (var line in text.Split('\n'))
{
var idx = line.IndexOf("[Fakt]", StringComparison.OrdinalIgnoreCase);
if (idx < 0) continue;
var fact = line.Remove(idx, "[Fakt]".Length);
fact = fact.Trim().TrimEnd('.', '!', '?', ':', ';', ',', '-').Trim();
if (!string.IsNullOrWhiteSpace(fact))
return (line, fact);
}
return null;
}
private async Task ExportViaPlugin()
{
if (string.IsNullOrEmpty(_selectedPluginId) || string.IsNullOrEmpty(_output)) return;
var plugin = PluginManager.GetPlugin(_selectedPluginId);
if (plugin == null) return;
_isExporting = true;
_exportResult = string.Empty;
try
{
var title = _inputText.Length > 80 ? _inputText[..80] + "…" : _inputText;
title = string.IsNullOrWhiteSpace(title) ? "Quick Markdown Export" : title;
var context = new ExportPluginContext
{
Title = title,
Content = _output
};
var result = await plugin.ExportAsync(context);
_exportResult = result.Message;
}
catch (Exception ex)
{
_exportResult = $"Fehler: {ex.Message}";
}
finally
{
_isExporting = false;
}
}
}

View file

@ -4,11 +4,13 @@
@inject ISettingsService SettingsService
@inject IThemeService ThemeService
@inject IOllamaService OllamaService
@inject OllamaSettings OllamaConfig
@inject NavigationManager Navigation
@inject IJSRuntime JSRuntime
@inject IProjectService ProjectService
@inject ILocalizationService Loc
@inject IAuthService AuthService
@inject IPluginManager PluginManager
<PageTitle>@Loc["page.title.settings"] - Author Buddy</PageTitle>
@ -73,40 +75,55 @@
<div class="settings-section">
<h3 class="settings-section-title">@Loc["settings.section.models"]</h3>
<div class="form-group">
<div class="form-group settings-model-row">
<label for="ragModel">@Loc["settings.models.label.rag"]</label>
<select id="ragBackend" value="@ragBackend" @onchange="OnRagBackendChanged">
<option value="Ollama">Ollama</option>
<option value="Ollama Cloud">Ollama Cloud</option>
<option value="llama.cpp">llama.cpp</option>
</select>
<select id="ragModel" @bind="ragModel">
@if (availableModels.Count == 0)
@if (availableRagModels.Count == 0)
{
<option value="">@Loc["settings.models.empty"]</option>
}
@foreach (var m in availableModels)
@foreach (var m in availableRagModels)
{
<option value="@m.Name">@m.Name</option>
}
</select>
</div>
<div class="form-group">
<div class="form-group settings-model-row">
<label for="styleModel">@Loc["settings.models.label.style"]</label>
<select id="styleBackend" value="@styleBackend" @onchange="OnStyleBackendChanged">
<option value="Ollama">Ollama</option>
<option value="Ollama Cloud">Ollama Cloud</option>
<option value="llama.cpp">llama.cpp</option>
</select>
<select id="styleModel" @bind="styleModel">
@if (availableModels.Count == 0)
@if (availableStyleModels.Count == 0)
{
<option value="">@Loc["settings.models.empty"]</option>
}
@foreach (var m in availableModels)
@foreach (var m in availableStyleModels)
{
<option value="@m.Name">@m.Name</option>
}
</select>
</div>
<div class="form-group">
<div class="form-group settings-model-row">
<label for="spellingModel">@Loc["settings.models.label.spellcheck"]</label>
<select id="spellingBackend" value="@spellingBackend" @onchange="OnSpellingBackendChanged">
<option value="Ollama">Ollama</option>
<option value="Ollama Cloud">Ollama Cloud</option>
<option value="llama.cpp">llama.cpp</option>
</select>
<select id="spellingModel" @bind="spellingModel">
@if (availableModels.Count == 0)
@if (availableSpellingModels.Count == 0)
{
<option value="">@Loc["settings.models.empty"]</option>
}
@foreach (var m in availableModels)
@foreach (var m in availableSpellingModels)
{
<option value="@m.Name">@m.Name</option>
}
@ -116,13 +133,6 @@
<div class="settings-section">
<h3 class="settings-section-title">@Loc["settings.section.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">
@ -144,6 +154,16 @@
<input id="llamaCppUrl" @bind="llamaCppUrl" placeholder="http://localhost:8080" />
<div class="form-hint">Nur aktiv wenn Backend auf "llama.cpp" gesetzt ist</div>
</div>
<div class="form-group">
<label for="ollamaCloudUrl">URL (Ollama Cloud)</label>
<input id="ollamaCloudUrl" @bind="_ollamaCloudUrl" placeholder="https://api.ollama.com" />
<div class="form-hint">Nur aktiv wenn Backend auf "Ollama Cloud" gesetzt ist</div>
</div>
<div class="form-group">
<label for="ollamaCloudApiKey">API-Key (Ollama Cloud)</label>
<input id="ollamaCloudApiKey" type="password" @bind="_ollamaCloudApiKey" placeholder="Bearer-Token" />
<div class="form-hint">Wird nur lokal gespeichert, nie ausgeloggt</div>
</div>
</div>
</div>
@ -163,6 +183,30 @@
</div>
}
</div>
@if (_availablePlugins.Count > 0)
{
<div class="settings-section">
<h3 class="settings-section-title">Plugins</h3>
@foreach (var p in _availablePlugins)
{
<div class="form-group" style="border:1px solid var(--border-color);padding:8px;border-radius:4px;margin-bottom:8px;">
<strong>@p.Name</strong>
<div style="font-size:12px;opacity:.7;margin-bottom:6px;">@p.Description</div>
@if (p.Id == "wordpress")
{
<label>WordPress-URL</label>
<input @bind="_wpUrl" placeholder="https://example.com" />
<label>Benutzername</label>
<input @bind="_wpUsername" placeholder="admin" />
<label>App-Passwort</label>
<input type="password" @bind="_wpPassword" placeholder="xxxx xxxx xxxx xxxx" />
}
</div>
}
<button class="btn btn-sm btn-primary" @onclick="SavePluginConfig">Plugin-Konfiguration speichern</button>
<span style="margin-left:8px;font-size:12px;">@_pluginStatus</span>
</div>
}
<div class="settings-section">
<h3 class="settings-section-title">@Loc["settings.section.appearance"]</h3>
<div class="form-group">
@ -409,12 +453,14 @@
private void SwitchToTechnicalTab() => _activePromptTab = "technical";
private void SwitchToAdminTab() => _activePromptTab = "admin";
private string ragModel = "llama3.2";
private string ragBackend = "Ollama";
private string styleModel = "llama3.2";
private string styleBackend = "Ollama";
private string spellingModel = "phi3:mini";
private string spellingBackend = "Ollama";
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;
@ -447,7 +493,9 @@
private string newProjectName = string.Empty;
private string newUserName = string.Empty;
private string statusMessage = string.Empty;
private List<LLM_Model> availableModels = new();
private List<LLM_Model> availableRagModels = new();
private List<LLM_Model> availableStyleModels = new();
private List<LLM_Model> availableSpellingModels = new();
private List<UserEntity> users = new();
private int selectedUserId;
private string? currentProjectName;
@ -459,6 +507,13 @@
private bool _isTestingConnection;
private List<string>? _diagnosticsResults;
private bool _isDiagnosing;
private string _ollamaCloudUrl = string.Empty;
private string _ollamaCloudApiKey = string.Empty;
private List<IExportPlugin> _availablePlugins = new();
private string _wpUrl = string.Empty;
private string _wpUsername = string.Empty;
private string _wpPassword = string.Empty;
private string _pluginStatus = string.Empty;
protected override async Task OnInitializedAsync()
{
@ -481,27 +536,24 @@
if (_isAdmin)
{
try
{
var models = await OllamaService.GetLocalModelsAsync();
availableModels = models.ToList();
}
catch
{
availableModels = new();
}
await RefreshModelsForAllBackends();
_availablePlugins = PluginManager.GetPlugins().ToList();
await LoadPluginConfig();
}
}
private async Task LoadUserSettings()
{
ragModel = await SettingsService.GetRagModelAsync() ?? "llama3.2";
ragBackend = await SettingsService.GetRagBackendAsync() ?? "Ollama";
styleModel = await SettingsService.GetStyleModelAsync() ?? "llama3.2";
styleBackend = await SettingsService.GetStyleBackendAsync() ?? "Ollama";
spellingModel = await SettingsService.GetSpellingModelAsync() ?? "phi3:mini";
spellingBackend = await SettingsService.GetSpellingBackendAsync() ?? "Ollama";
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;
@ -531,6 +583,8 @@
techQualitaetTemp = await SettingsService.GetTechQualitaetTempAsync();
techRisikoTemp = await SettingsService.GetTechRisikoTempAsync();
selectedLanguage = await SettingsService.GetLanguageAsync() ?? "de";
_ollamaCloudUrl = await SettingsService.GetOllamaCloudUrlAsync() ?? OllamaConfig.OllamaCloudUrl;
_ollamaCloudApiKey = await SettingsService.GetOllamaCloudApiKeyAsync() ?? OllamaConfig.OllamaCloudApiKey;
}
private async Task SwitchUser(ChangeEventArgs e)
@ -552,29 +606,84 @@
statusMessage = string.Format(Loc["settings.status.user_created"], user.Name);
}
private async Task OnRagBackendChanged(ChangeEventArgs e)
{
ragBackend = e.Value?.ToString() ?? "Ollama";
await RefreshModelsForRag();
}
private async Task OnStyleBackendChanged(ChangeEventArgs e)
{
styleBackend = e.Value?.ToString() ?? "Ollama";
await RefreshModelsForStyle();
}
private async Task OnSpellingBackendChanged(ChangeEventArgs e)
{
spellingBackend = e.Value?.ToString() ?? "Ollama";
await RefreshModelsForSpelling();
}
private async Task RefreshModelsForRag()
{
OllamaConfig.OllamaCloudUrl = _ollamaCloudUrl;
OllamaConfig.OllamaCloudApiKey = _ollamaCloudApiKey;
try { availableRagModels = (await OllamaService.GetModelsForBackendAsync(ragBackend)).ToList(); }
catch { availableRagModels = new(); }
}
private async Task RefreshModelsForStyle()
{
OllamaConfig.OllamaCloudUrl = _ollamaCloudUrl;
OllamaConfig.OllamaCloudApiKey = _ollamaCloudApiKey;
try { availableStyleModels = (await OllamaService.GetModelsForBackendAsync(styleBackend)).ToList(); }
catch { availableStyleModels = new(); }
}
private async Task RefreshModelsForSpelling()
{
OllamaConfig.OllamaCloudUrl = _ollamaCloudUrl;
OllamaConfig.OllamaCloudApiKey = _ollamaCloudApiKey;
try { availableSpellingModels = (await OllamaService.GetModelsForBackendAsync(spellingBackend)).ToList(); }
catch { availableSpellingModels = new(); }
}
private async Task RefreshModelsForAllBackends()
{
await Task.WhenAll(RefreshModelsForRag(), RefreshModelsForStyle(), RefreshModelsForSpelling());
}
private async Task SaveSettings()
{
if (_isAdmin)
{
await SettingsService.SaveOllamaUrlAsync(ollamaUrl);
await SettingsService.SaveLlamaCppUrlAsync(llamaCppUrl);
await SettingsService.SaveBackendAsync(selectedBackend);
OllamaService.Reconfigure(ollamaUrl);
await SettingsService.SaveOllamaCloudUrlAsync(_ollamaCloudUrl);
await SettingsService.SaveOllamaCloudApiKeyAsync(_ollamaCloudApiKey);
await SettingsService.SaveRagBackendAsync(ragBackend);
await SettingsService.SaveStyleBackendAsync(styleBackend);
await SettingsService.SaveSpellingBackendAsync(spellingBackend);
if (!string.IsNullOrWhiteSpace(ragModel))
{
var ok = await OllamaService.TestEmbeddingAsync(ragModel);
var ok = await OllamaService.TestEmbeddingAsync(ragModel, ragBackend);
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;
//return;
}
}
await SettingsService.SaveRagModelAsync(ragModel);
await SettingsService.SaveStyleModelAsync(styleModel);
await SettingsService.SaveSpellingModelAsync(spellingModel);
}
await SettingsService.SaveThemeAsync(selectedTheme);
await SettingsService.SaveLektorSystemPromptAsync(lektorPrompt);
@ -605,7 +714,7 @@
await SettingsService.SaveTechGlossarTempAsync(techGlossarTemp);
await SettingsService.SaveTechQualitaetTempAsync(techQualitaetTemp);
await SettingsService.SaveTechRisikoTempAsync(techRisikoTemp);
await SettingsService.SaveLanguageAsync(selectedLanguage);
if (selectedLanguage != Loc.CurrentLanguage)
await Loc.SetLanguageAsync(selectedLanguage);
@ -614,6 +723,33 @@
await ThemeService.ApplyTheme(selectedTheme);
}
private async Task LoadPluginConfig()
{
var cfg = await PluginManager.GetConfigAsync("wordpress");
if (cfg != null)
{
if (cfg.Values.TryGetValue("url", out var url)) _wpUrl = url;
if (cfg.Values.TryGetValue("username", out var user)) _wpUsername = user;
if (cfg.Values.TryGetValue("password", out var pass)) _wpPassword = pass;
}
}
private async Task SavePluginConfig()
{
var cfg = new PluginConfig
{
PluginId = "wordpress",
Values = new()
{
["url"] = _wpUrl ?? "",
["username"] = _wpUsername ?? "",
["password"] = _wpPassword ?? ""
}
};
await PluginManager.SaveConfigAsync(cfg);
_pluginStatus = "Gespeichert.";
}
private string GetTempCategory(double temp)
{
if (temp <= 0.2) return Loc["settings.temperature.category.precise"];
@ -675,33 +811,33 @@
}
var models = new[] {
(Loc["settings.models.label.style"] ?? "Style", styleModel),
(Loc["settings.models.label.rag"] ?? "RAG", ragModel),
(Loc["settings.models.label.spellcheck"] ?? "Spelling", spellingModel)
(Label: Loc["settings.models.label.style"] ?? "Style", Model: styleModel, Backend: styleBackend),
(Label: Loc["settings.models.label.rag"] ?? "RAG", Model: ragModel, Backend: ragBackend),
(Label: Loc["settings.models.label.spellcheck"] ?? "Spelling", Model: spellingModel, Backend: spellingBackend)
};
foreach (var (label, model) in models)
foreach (var entry in models)
{
if (string.IsNullOrWhiteSpace(model))
if (string.IsNullOrWhiteSpace(entry.Model))
{
results.Add($"? {label}: {(!string.IsNullOrEmpty(lang) && lang == "en" ? "not configured" : "nicht konfiguriert")}");
results.Add($"? {entry.Label}: {(!string.IsNullOrEmpty(lang) && lang == "en" ? "not configured" : "nicht konfiguriert")}");
continue;
}
var exists = await OllamaService.ModelExistsAsync(model);
var exists = await OllamaService.ModelExistsAsync(entry.Model, entry.Backend);
if (!exists)
{
results.Add($"✗ {label} ({model}): {(!string.IsNullOrEmpty(lang) && lang == "en" ? "not found" : "nicht gefunden")}");
results.Add($"✗ {entry.Label} ({entry.Model}): {(!string.IsNullOrEmpty(lang) && lang == "en" ? "not found" : "nicht gefunden")}");
continue;
}
var latency = await OllamaService.MeasureLatencyAsync(model);
var latency = await OllamaService.MeasureLatencyAsync(entry.Model, entry.Backend);
var latencyStr = latency >= 0 ? $"{latency} ms" : "—";
var ctx = await OllamaService.GetModelContextWindowAsync(model);
var ctx = await OllamaService.GetModelContextWindowAsync(entry.Model, entry.Backend);
var ctxStr = ctx.Success ? $"{ctx.ContextWindow}" : "?";
results.Add($"✓ {label} ({model}): {latencyStr}, {(!string.IsNullOrEmpty(lang) && lang == "en" ? "Context" : "Kontext")}: {ctxStr}");
results.Add($"✓ {entry.Label} ({entry.Model}): {latencyStr}, {(!string.IsNullOrEmpty(lang) && lang == "en" ? "Context" : "Kontext")}: {ctxStr}");
}
_diagnosticsResults = results;

View file

@ -0,0 +1,11 @@
namespace AuthorBuddy.Web.Models;
public class DuckDuckGoResult
{
public string Heading { get; set; } = string.Empty;
public string AbstractText { get; set; } = string.Empty;
public string AbstractUrl { get; set; } = string.Empty;
public string Answer { get; set; } = string.Empty;
public List<string> RelatedTexts { get; set; } = new();
public List<string> RelatedUrls { get; set; } = new();
}

View file

@ -0,0 +1,22 @@
namespace AuthorBuddy.Web.Models;
public class ExportPluginContext
{
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public string? Tags { get; set; }
public string? Status { get; set; } = "draft";
public Dictionary<string, string> Extra { get; set; } = new();
}
public class PluginResult
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
}
public class PluginConfig
{
public string PluginId { get; set; } = string.Empty;
public Dictionary<string, string> Values { get; set; } = new();
}

17
Models/FactCheckResult.cs Normal file
View file

@ -0,0 +1,17 @@
namespace AuthorBuddy.Web.Models;
public class FactCheckResult
{
public string Claim { get; set; } = string.Empty;
public FactCheckVerdict Verdict { get; set; } = FactCheckVerdict.NoInformation;
public string Reasoning { get; set; } = string.Empty;
public List<string> Sources { get; set; } = new();
}
public enum FactCheckVerdict
{
Confirmed,
Contradicted,
Partial,
NoInformation
}

View file

@ -0,0 +1,8 @@
namespace AuthorBuddy.Web.Models;
public class FactSearchResult
{
public string Title { get; set; } = string.Empty;
public string Snippet { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
}

View file

@ -0,0 +1,8 @@
namespace AuthorBuddy.Web.Models;
public class McpToolDefinition
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string InputSchemaJson { get; set; } = string.Empty;
}

9
Models/McpToolResult.cs Normal file
View file

@ -0,0 +1,9 @@
namespace AuthorBuddy.Web.Models;
public class McpToolResult
{
public bool IsError { get; set; }
public string ToolName { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public object? Data { get; set; }
}

View file

@ -8,8 +8,13 @@ public class OllamaSettings
public string OllamaUrl { get; set; } = "http://localhost:11434";
public string LlamaCppUrl { get; set; } = "http://localhost:8080";
public string Backend { get; set; } = "Ollama";
public string RagBackend { get; set; } = "Ollama";
public string StyleBackend { get; set; } = "Ollama";
public string SpellingBackend { get; set; } = "Ollama";
public string Theme { get; set; } = "light";
public string Language { get; set; } = "de";
public string OllamaCloudUrl { get; set; } = "https://api.ollama.com";
public string OllamaCloudApiKey { get; set; } = string.Empty;
public string LektorSystemPrompt { get; set; } = "Du bist ein erfahrener Lektor. Analysiere den Text des Autors. " +
"Optimiere ihn auf einen lebendigen, anschaulichen und subjektiven Stil. " +
@ -108,11 +113,17 @@ public class OllamaSettings
public string ChatPrompt { get; set; } = "Du bist ein hilfreicher Assistent. Antworte freundlich und präzise auf Deutsch.";
public string ChatPromptEn { get; } = "You are a helpful assistant. Answer friendly and precisely in English.";
public double ChatTemp { get; set; } = 0.7;
public bool ChatFactCheckEnabled { get; set; } = false;
public string FactSearchPrompt { get; set; } = "Du bist ein Recherche-Assistent. Erstelle aus der folgenden Behauptung eine präzise Suchanfrage für eine Websuche (maximal 10 Wörter, nur Schlüsselbegriffe). Gib NUR die Suchanfrage zurück – ohne Anführungszeichen, ohne Vor- oder Nachbemerkungen.";
public string FactSearchPromptEn { get; } = "You are a research assistant. From the following claim, create a precise web-search query (max 10 words, keywords only). Return ONLY the query – no quotes, no preamble.";
public double FactSearchTemp { get; set; } = 0.2;
public string GetChatDefaultPrompt(string currentLanguage) => GetDefaultPrompt(ChatPrompt, ChatPromptEn, currentLanguage);
public string GetQuickMarkdownDefaultPrompt(string currentLanguage) => GetDefaultPrompt(QuickMarkdownPrompt, QuickMarkdownPromptEn, currentLanguage);
public string GetFactSearchDefaultPrompt(string currentLanguage) => GetDefaultPrompt(FactSearchPrompt, FactSearchPromptEn, currentLanguage);
public string GetDefaultPrompt(string dePrompt, string enPrompt, string currentLanguage)
{
return currentLanguage == "en" ? enPrompt : dePrompt;

View file

@ -0,0 +1,94 @@
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using AuthorBuddy.Web.Models;
using AuthorBuddy.Web.Services;
namespace AuthorBuddy.Web.Plugins;
public class WordPressPlugin : IExportPlugin
{
private readonly ISettingsService _settings;
private readonly IHttpClientFactory _httpFactory;
public string Id => "wordpress";
public string Name => "WordPress";
public string Description => "Erstellt einen neuen WordPress-Beitrag als Draft mit Titel, Inhalt und Stichwörtern.";
public WordPressPlugin(ISettingsService settings, IHttpClientFactory httpFactory)
{
_settings = settings;
_httpFactory = httpFactory;
}
public async Task<PluginResult> ExportAsync(ExportPluginContext context)
{
var json = await _settings.GetPluginConfigAsync(Id);
PluginConfig? cfg = null;
if (!string.IsNullOrEmpty(json))
cfg = JsonSerializer.Deserialize<PluginConfig>(json);
if (cfg == null)
return new PluginResult { Success = false, Message = "WordPress-Plugin nicht konfiguriert." };
cfg.Values.TryGetValue("url", out var wpUrl);
cfg.Values.TryGetValue("username", out var username);
cfg.Values.TryGetValue("password", out var password);
if (string.IsNullOrEmpty(wpUrl) || string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
return new PluginResult { Success = false, Message = "WordPress-URL, Benutzername oder App-Passwort fehlt." };
wpUrl = wpUrl.TrimEnd('/');
var tags = new List<string>();
if (!string.IsNullOrWhiteSpace(context.Tags))
tags.AddRange(context.Tags.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries));
var post = new Dictionary<string, object>
{
["title"] = context.Title,
["content"] = context.Content,
["status"] = context.Status ?? "draft"
};
if (tags.Count > 0)
post["tags"] = tags;
var body = JsonSerializer.Serialize(post);
var request = new HttpRequestMessage(HttpMethod.Post, $"{wpUrl}/wp/v2/posts")
{
Content = new StringContent(body, Encoding.UTF8, "application/json")
};
var auth = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"));
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", auth);
try
{
var http = _httpFactory.CreateClient();
var response = await http.SendAsync(request);
if (response.IsSuccessStatusCode)
{
var resBody = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(resBody);
var id = doc.RootElement.TryGetProperty("id", out var idProp) ? idProp.GetInt32() : 0;
var link = doc.RootElement.TryGetProperty("link", out var linkProp) ? linkProp.GetString() : "";
return new PluginResult
{
Success = true,
Message = $"WordPress-Beitrag als Draft erstellt (ID: {id}).{(string.IsNullOrEmpty(link) ? "" : $" {link}")}"
};
}
var err = await response.Content.ReadAsStringAsync();
return new PluginResult { Success = false, Message = $"WordPress-Fehler ({response.StatusCode}): {Truncate(err, 200)}" };
}
catch (Exception ex)
{
return new PluginResult { Success = false, Message = $"Verbindungsfehler: {ex.Message}" };
}
}
private static string Truncate(string s, int max) => s.Length <= max ? s : s[..max] + "…";
}

View file

@ -1,6 +1,7 @@
using AuthorBuddy.Web.Components;
using AuthorBuddy.Web.Data;
using AuthorBuddy.Web.Models;
using AuthorBuddy.Web.Plugins;
using AuthorBuddy.Web.Services;
using LiteDB;
using Microsoft.AspNetCore.Authentication.Cookies;
@ -29,14 +30,18 @@ builder.Services.AddSingleton<OllamaSettings>();
builder.Services.AddSingleton<IOllamaService, OllamaService>();
builder.Services.AddSingleton<ILLMBackend, OllamaBackend>();
builder.Services.AddSingleton<ILLMBackend, LlamaCppBackend>();
builder.Services.AddSingleton<ILLMBackend, OllamaCloudBackend>();
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<ILlmWikiService, LlmWikiService>();
builder.Services.AddSingleton<IDuckDuckGoService, DuckDuckGoService>();
builder.Services.AddSingleton<IFactCheckService, FactCheckService>();
builder.Services.AddScoped<IFactSearchToolService, FactSearchToolService>();
builder.Services.AddSingleton<OperationState>();
builder.Services.AddSingleton<IDbAdminService, DbAdminService>();
builder.Services.AddScoped<IAuthService, AuthService>();
@ -44,6 +49,10 @@ builder.Services.AddSingleton<IEmailService, EmailService>();
builder.Services.AddSingleton<IChatService, ChatService>();
builder.Services.AddSingleton<IQuickMarkdownService, QuickMarkdownService>();
builder.Services.AddHttpClient();
builder.Services.AddScoped<IExportPlugin, WordPressPlugin>();
builder.Services.AddScoped<IPluginManager, PluginManager>();
var smtpSettings = new SmtpSettings();
builder.Configuration.GetSection("Smtp").Bind(smtpSettings);
builder.Services.AddSingleton(smtpSettings);

View file

@ -3,7 +3,7 @@
<Project>
<PropertyGroup>
<_PublishTargetUrl>E:\ollama_net\ollama\AuthorBuddy.Web\bin\Release\net10.0\publish\</_PublishTargetUrl>
<History>True|2026-07-05T05:05:59.5910017Z||;True|2026-07-04T10:31:55.1105019+02:00||;True|2026-07-03T19:42:19.2725521+02:00||;True|2026-07-03T19:14:41.2291817+02:00||;True|2026-06-26T17:29:16.2419779+02:00||;True|2026-06-14T08:09:44.9432726+02:00||;</History>
<History>True|2026-08-02T07:53:31.4907144Z||;True|2026-07-12T15:04:45.0075628+02:00||;True|2026-07-12T14:50:27.1555269+02:00||;True|2026-07-12T14:49:01.4372949+02:00||;True|2026-07-05T07:05:59.5910017+02:00||;True|2026-07-04T10:31:55.1105019+02:00||;True|2026-07-03T19:42:19.2725521+02:00||;True|2026-07-03T19:14:41.2291817+02:00||;True|2026-06-26T17:29:16.2419779+02:00||;True|2026-06-14T08:09:44.9432726+02:00||;</History>
<LastFailureDetails />
</PropertyGroup>
</Project>

View file

@ -4,6 +4,8 @@
"common.error": "Fehler:",
"common.copy": "Kopieren",
"common.status.error": "Fehler",
"llm.overlay.title": "KI-Kommunikation",
"llm.overlay.placeholder": "Die KI denkt nach…",
"page.title.login": "Anmelden - Author Buddy",
"page.title.register": "Registrieren - Author Buddy",
"page.title.setup2fa": "2FA einrichten - Author Buddy",
@ -156,7 +158,7 @@
"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.wiki": "Wiki-Wissen",
"assistant.mode.tech_lastenheft": "📋 Lastenheft",
"assistant.mode.tech_pflichtenheft": "⚙️ Pflichtenheft",
"assistant.mode.tech_usecase": "🔄 Use Cases",
@ -197,17 +199,16 @@
"editor.button.save": "Speichern",
"editor.button.version": "Version",
"editor.button.assistant": "Assistent",
"editor.button.index_project": "Projekt indexieren",
"editor.button.compile_wiki": "Wiki kompilieren",
"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_wiki": "Wiki fragen",
"editor.button.search_wikipedia": "Wikipedia-Recherche",
"editor.spellcheck.heading": "Rechtschreibprüfung - Änderungen:",
"editor.rag.heading": "Gefundene Fakten (RAG):",
"editor.rag.label.similarity": "Ähnlichkeit:",
"editor.wiki.heading": "Wiki-Antwort:",
"editor.wikipedia.heading": "Wikipedia-Recherche:",
"editor.wikipedia.status.loaded": "✓ Geladen",
"editor.wikipedia.button.load": "Vollständigen Artikel laden",
@ -233,18 +234,19 @@
"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_wiki": "Frage Wiki...",
"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.compiling_wiki": "Komprimiere Wiki...",
"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.wiki.error.no_project": "Kein Projekt geladen. Öffne zuerst ein Projekt über die Sidebar.",
"editor.wiki.error.not_found": "Projekt nicht gefunden.",
"editor.wiki.status.success": "Wiki erfolgreich kompiliert.",
"editor.wiki.error.generic": "Wiki-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.",
@ -252,9 +254,6 @@
"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)",
@ -282,6 +281,8 @@
"usermanagement.status.deleted": "Benutzer gelöscht.",
"common.back": "Zurück",
"export.title": "Dokumente exportieren",
"models.cloud.not_available": "Modellverwaltung ist nur für das lokale Ollama-Backend verfügbar.",
"models.cloud.hint": "Wechsle in den Einstellungen auf das Ollama-Backend, um Modelle zu verwalten.",
"export.file_count": "Ausgewählte Dateien",
"export.label.filename": "Dateiname",
"export.placeholder.filename": "z.B. MeinExport",

View file

@ -4,6 +4,8 @@
"common.error": "Error:",
"common.copy": "Copy",
"common.status.error": "Error",
"llm.overlay.title": "AI Communication",
"llm.overlay.placeholder": "The AI is thinking…",
"page.title.login": "Login - Author Buddy",
"page.title.register": "Register - Author Buddy",
"page.title.setup2fa": "2FA setup - Author Buddy",
@ -156,7 +158,7 @@
"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.wiki": "Wiki knowledge",
"assistant.mode.tech_lastenheft": "📋 Requirements Spec",
"assistant.mode.tech_pflichtenheft": "⚙️ Functional Spec",
"assistant.mode.tech_usecase": "🔄 Use Cases",
@ -197,17 +199,16 @@
"editor.button.save": "Save",
"editor.button.version": "Version",
"editor.button.assistant": "Assistant",
"editor.button.index_project": "Index project",
"editor.button.compile_wiki": "Compile wiki",
"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_wiki": "Ask wiki",
"editor.button.search_wikipedia": "Wikipedia research",
"editor.spellcheck.heading": "Spell check - Changes:",
"editor.rag.heading": "Found facts (RAG):",
"editor.rag.label.similarity": "Similarity:",
"editor.wiki.heading": "Wiki answer:",
"editor.wikipedia.heading": "Wikipedia research:",
"editor.wikipedia.status.loaded": "\u2713 Loaded",
"editor.wikipedia.button.load": "Load full article",
@ -233,18 +234,19 @@
"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_wiki": "Asking wiki...",
"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.compiling_wiki": "Compiling wiki...",
"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.wiki.error.no_project": "No project loaded. Open a project via the sidebar first.",
"editor.wiki.error.not_found": "Project not found.",
"editor.wiki.status.success": "Wiki compiled successfully.",
"editor.wiki.error.generic": "Wiki 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.",
@ -252,9 +254,6 @@
"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)",
@ -282,6 +281,8 @@
"usermanagement.status.deleted": "User deleted.",
"common.back": "Back",
"export.title": "Export documents",
"models.cloud.not_available": "Model management is only available for the local Ollama backend.",
"models.cloud.hint": "Switch to the Ollama backend in settings to manage models.",
"export.file_count": "Selected files",
"export.label.filename": "Filename",
"export.placeholder.filename": "e.g. MyExport",

View file

@ -0,0 +1,166 @@
using AuthorBuddy.Web.Models;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Web;
namespace AuthorBuddy.Web.Services;
public class DuckDuckGoService : IDuckDuckGoService
{
private readonly HttpClient _http;
private readonly HttpClient _htmlHttp;
public DuckDuckGoService()
{
_http = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
_http.DefaultRequestHeaders.UserAgent.ParseAdd("AuthorBuddy/1.0 (fact-checker)");
_htmlHttp = new HttpClient { Timeout = TimeSpan.FromSeconds(20) };
_htmlHttp.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
_htmlHttp.DefaultRequestHeaders.Accept.ParseAdd("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
_htmlHttp.DefaultRequestHeaders.Add("Accept-Language", "de,en;q=0.8");
}
public async Task<List<DuckDuckGoResult>> SearchWebAsync(string query, int maxResults = 5)
{
var results = new List<DuckDuckGoResult>();
try
{
var url = "https://html.duckduckgo.com/html/?q=" + HttpUtility.UrlEncode(query);
var html = await _htmlHttp.GetStringAsync(url);
if (html.Contains("anomaly", StringComparison.OrdinalIgnoreCase) ||
html.Contains("challenge-form", StringComparison.OrdinalIgnoreCase))
{
return results;
}
var blocks = Regex.Split(html, "<div[^>]*class=\"result[^\"]*\"", RegexOptions.IgnoreCase);
foreach (var block in blocks.Skip(1))
{
var link = Regex.Match(block, "<a[^>]*class=\"result__a\"[^>]*href=\"([^\"]+)\"[^>]*>(.*?)</a>", RegexOptions.Singleline);
if (!link.Success) continue;
var realUrl = DecodeDdgUrl(System.Net.WebUtility.HtmlDecode(link.Groups[1].Value));
if (string.IsNullOrWhiteSpace(realUrl)) continue;
var title = StripHtml(link.Groups[2].Value);
var snippet = string.Empty;
var snip = Regex.Match(block, "<a[^>]*class=\"result__snippet\"[^>]*>(.*?)</a>", RegexOptions.Singleline);
if (snip.Success) snippet = StripHtml(snip.Groups[1].Value);
results.Add(new DuckDuckGoResult { Heading = title, AbstractText = snippet, AbstractUrl = realUrl });
if (results.Count >= maxResults) break;
}
}
catch
{
// Keine Ergebnisse bei Fehler
}
return results;
}
public async Task<List<DuckDuckGoResult>> SearchAsync(string query, int maxResults = 3)
{
var results = new List<DuckDuckGoResult>();
try
{
var url = "https://api.duckduckgo.com/" +
$"?q={HttpUtility.UrlEncode(query)}" +
"&format=json" +
"&no_html=1" +
"&skip_disambig=1" +
"&t=AuthorBuddy";
var response = await _http.GetStringAsync(url);
using var doc = JsonDocument.Parse(response);
var root = doc.RootElement;
var result = new DuckDuckGoResult
{
Heading = GetString(root, "Heading"),
AbstractText = GetString(root, "AbstractText"),
AbstractUrl = GetString(root, "AbstractURL"),
Answer = GetString(root, "Answer")
};
if (root.TryGetProperty("RelatedTopics", out var related) && related.ValueKind == JsonValueKind.Array)
{
foreach (var topic in related.EnumerateArray())
{
if (topic.TryGetProperty("Text", out var text))
{
var t = text.GetString();
if (!string.IsNullOrWhiteSpace(t))
result.RelatedTexts.Add(t);
if (topic.TryGetProperty("FirstURL", out var firstUrl))
{
var u = firstUrl.GetString();
if (!string.IsNullOrWhiteSpace(u))
result.RelatedUrls.Add(u);
}
}
else if (topic.TryGetProperty("Topics", out var subTopics) && subTopics.ValueKind == JsonValueKind.Array)
{
foreach (var sub in subTopics.EnumerateArray())
{
if (sub.TryGetProperty("Text", out var subText))
{
var t = subText.GetString();
if (!string.IsNullOrWhiteSpace(t))
result.RelatedTexts.Add(t);
if (sub.TryGetProperty("FirstURL", out var firstUrl))
{
var u = firstUrl.GetString();
if (!string.IsNullOrWhiteSpace(u))
result.RelatedUrls.Add(u);
}
}
}
}
}
}
if (!string.IsNullOrWhiteSpace(result.AbstractText) ||
!string.IsNullOrWhiteSpace(result.Answer) ||
result.RelatedTexts.Count > 0)
{
results.Add(result);
}
}
catch
{
// Kein Ergebnis bei Fehler
}
return results.Take(maxResults).ToList();
}
private static string GetString(JsonElement element, string property)
{
if (element.TryGetProperty(property, out var value) && value.ValueKind == JsonValueKind.String)
return value.GetString() ?? string.Empty;
return string.Empty;
}
private static string DecodeDdgUrl(string rawUrl)
{
if (string.IsNullOrWhiteSpace(rawUrl)) return rawUrl;
var uddg = Regex.Match(rawUrl, "[?&]uddg=([^&]+)");
if (uddg.Success)
return HttpUtility.UrlDecode(uddg.Groups[1].Value);
if (rawUrl.StartsWith("//"))
return "https:" + rawUrl;
return rawUrl;
}
private static string StripHtml(string html)
{
var text = Regex.Replace(html, "<[^>]+>", string.Empty);
return System.Net.WebUtility.HtmlDecode(text).Trim();
}
}

View file

@ -0,0 +1,146 @@
using AuthorBuddy.Web.Models;
using System.Text;
namespace AuthorBuddy.Web.Services;
public class FactCheckService : IFactCheckService
{
private readonly IDuckDuckGoService _duckDuckGo;
private readonly IOllamaService _ollama;
private readonly OllamaSettings _settings;
public FactCheckService(IDuckDuckGoService duckDuckGo, IOllamaService ollama, OllamaSettings settings)
{
_duckDuckGo = duckDuckGo;
_ollama = ollama;
_settings = settings;
}
public async Task<List<FactCheckResult>> VerifyAsync(string answer, string? backendName = null, int maxClaims = 3)
{
var results = new List<FactCheckResult>();
var claims = ExtractClaims(answer);
foreach (var claim in claims.Take(maxClaims))
{
var searchResults = await _duckDuckGo.SearchAsync(claim);
if (searchResults.Count == 0)
{
results.Add(new FactCheckResult
{
Claim = claim,
Verdict = FactCheckVerdict.NoInformation,
Reasoning = "Keine Quellen gefunden."
});
continue;
}
var verdict = await GetVerdictAsync(claim, searchResults, backendName);
results.Add(verdict);
}
return results;
}
private async Task<FactCheckResult> GetVerdictAsync(string claim, List<DuckDuckGoResult> searchResults, string? backendName)
{
var sourcesText = new StringBuilder();
var sources = new List<string>();
foreach (var r in searchResults)
{
if (!string.IsNullOrWhiteSpace(r.AbstractText))
sourcesText.AppendLine($"- {r.AbstractText}");
if (!string.IsNullOrWhiteSpace(r.Answer))
sourcesText.AppendLine($"- {r.Answer}");
foreach (var related in r.RelatedTexts)
sourcesText.AppendLine($"- {related}");
if (!string.IsNullOrWhiteSpace(r.AbstractUrl))
sources.Add(r.AbstractUrl);
}
if (sourcesText.Length == 0)
{
return new FactCheckResult
{
Claim = claim,
Verdict = FactCheckVerdict.NoInformation,
Reasoning = "Keine verwertbaren Quellen gefunden.",
Sources = sources
};
}
var prompt = $@"Du bist ein Faktenprüfer. Vergleiche die folgende Behauptung mit den DuckDuckGo-Suchergebnissen.
BEHAUPTUNG: {claim}
SUCHERGEBNISSE:
{sourcesText}
Bewerte, ob die Behauptung durch die Suchergebnisse bestätigt oder widerlegt wird.
Antworte NUR in diesem Format:
URTEIL: BESTAETIGT oder WIDERSPROCHEN oder TEILWEISE oder KEINE_INFORMATION
BEGRUENDUNG: Maximal 2 Sätze.";
var raw = await _ollama.Chat(_settings.StyleModel, prompt, temperature: 0.1, backendName: backendName ?? _settings.StyleBackend);
return new FactCheckResult
{
Claim = claim,
Verdict = ParseVerdict(raw),
Reasoning = ExtractReasoning(raw),
Sources = sources
};
}
private static List<string> ExtractClaims(string text)
{
var claims = new List<string>();
if (string.IsNullOrWhiteSpace(text)) return claims;
var sentences = System.Text.RegularExpressions.Regex.Split(text, @"(?<=[.!?])\s+");
foreach (var sentence in sentences)
{
var trimmed = sentence.Trim().Trim('"');
if (trimmed.Length < 25) continue;
if (trimmed.EndsWith("?")) continue;
if (trimmed.StartsWith("```") || trimmed.StartsWith("#")) continue;
claims.Add(trimmed.Length > 200 ? trimmed[..200] + "..." : trimmed);
if (claims.Count >= 5) break;
}
return claims;
}
private static FactCheckVerdict ParseVerdict(string raw)
{
var upper = raw.ToUpperInvariant();
if (upper.Contains("BESTAETIGT") || upper.Contains("CONFIRMED") || upper.Contains("BESTÄTIGT"))
return FactCheckVerdict.Confirmed;
if (upper.Contains("WIDERSPROCHEN") || upper.Contains("CONTRADICT") || upper.Contains("REFUTED"))
return FactCheckVerdict.Contradicted;
if (upper.Contains("TEILWEISE") || upper.Contains("PARTIAL"))
return FactCheckVerdict.Partial;
return FactCheckVerdict.NoInformation;
}
private static string ExtractReasoning(string raw)
{
var idx = raw.IndexOf("BEGRUENDUNG:", StringComparison.OrdinalIgnoreCase);
if (idx < 0)
idx = raw.IndexOf("BEGRÜNDUNG:", StringComparison.OrdinalIgnoreCase);
if (idx < 0)
idx = raw.IndexOf("REASONING:", StringComparison.OrdinalIgnoreCase);
if (idx >= 0)
{
var reasoning = raw[(idx + 1)..];
if (reasoning.Contains(':'))
reasoning = reasoning[(reasoning.IndexOf(':') + 1)..];
return reasoning.Trim();
}
return raw.Trim();
}
}

View file

@ -0,0 +1,209 @@
using AuthorBuddy.Web.Models;
using System.Text.Json;
namespace AuthorBuddy.Web.Services;
public class FactSearchToolService : IFactSearchToolService
{
private readonly IOllamaService _ollama;
private readonly IDuckDuckGoService _duckDuckGo;
private readonly IWikipediaService _wikipedia;
private readonly ISettingsService _settingsService;
private readonly OllamaSettings _settings;
public IReadOnlyList<McpToolDefinition> Tools { get; }
public FactSearchToolService(
IOllamaService ollama,
IDuckDuckGoService duckDuckGo,
IWikipediaService wikipedia,
ISettingsService settingsService,
OllamaSettings settings)
{
_ollama = ollama;
_duckDuckGo = duckDuckGo;
_wikipedia = wikipedia;
_settingsService = settingsService;
_settings = settings;
Tools = new List<McpToolDefinition>
{
new()
{
Name = "fact_search_query",
Description = "Erzeugt aus einer Behauptung (Fakt) eine präzise Suchanfrage für eine Websuche.",
InputSchemaJson = "{\"type\":\"object\",\"properties\":{\"fact\":{\"type\":\"string\"}},\"required\":[\"fact\"]}"
},
new()
{
Name = "fact_web_search",
Description = "Führt eine Websuche aus (DuckDuckGo, ggf. Wikipedia) und liefert Titel, Snippets und URLs.",
InputSchemaJson = "{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"},\"maxResults\":{\"type\":\"integer\"}},\"required\":[\"query\"]}"
},
new()
{
Name = "fact_summarize",
Description = "Erstellt eine kurze Zusammenfassung über ein Thema anhand von Suchinformationen.",
InputSchemaJson = "{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"},\"text\":{\"type\":\"string\"},\"url\":{\"type\":\"string\"}},\"required\":[\"query\",\"text\"]}"
}
};
}
public async Task<McpToolResult> CallAsync(string toolName, string argumentsJson)
{
try
{
using var doc = JsonDocument.Parse(string.IsNullOrWhiteSpace(argumentsJson) ? "{}" : argumentsJson);
var args = doc.RootElement;
switch (toolName)
{
case "fact_search_query":
{
var fact = GetString(args, "fact");
if (string.IsNullOrWhiteSpace(fact))
return Error(toolName, "Argument 'fact' fehlt.");
var query = await CreateSearchQueryAsync(fact);
return new McpToolResult { ToolName = toolName, Content = JsonSerializer.Serialize(new { query }), Data = query };
}
case "fact_web_search":
{
var query = GetString(args, "query");
if (string.IsNullOrWhiteSpace(query))
return Error(toolName, "Argument 'query' fehlt.");
var maxResults = args.TryGetProperty("maxResults", out var m) && m.TryGetInt32(out var mi) ? mi : 5;
var results = await SearchAsync(query, maxResults);
return new McpToolResult { ToolName = toolName, Content = JsonSerializer.Serialize(results), Data = results };
}
case "fact_summarize":
{
var query = GetString(args, "query");
var text = GetString(args, "text");
var url = GetString(args, "url");
if (string.IsNullOrWhiteSpace(query) || string.IsNullOrWhiteSpace(text))
return Error(toolName, "Argumente 'query' und 'text' sind erforderlich.");
var summary = await SummarizeAsync(query, text, url);
return new McpToolResult { ToolName = toolName, Content = JsonSerializer.Serialize(new { summary, url }), Data = summary };
}
default:
return Error(toolName, $"Unbekanntes Tool '{toolName}'.");
}
}
catch (Exception ex)
{
return Error(toolName, ex.Message);
}
}
public async Task<string> CreateSearchQueryAsync(string factText)
{
var prompt = await _settingsService.GetFactSearchPromptAsync()
?? _settings.GetFactSearchDefaultPrompt(_settings.Language);
var temperature = await _settingsService.GetFactSearchTempAsync();
var result = await _ollama.Chat(_settings.StyleModel, factText, prompt, temperature, _settings.StyleBackend);
return SanitizeQuery(result);
}
public async Task<List<FactSearchResult>> SearchAsync(string query, int maxResults = 5)
{
var results = new List<FactSearchResult>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
void Add(FactSearchResult r)
{
if (string.IsNullOrWhiteSpace(r.Url)) return;
if (!seen.Add(r.Url)) return;
results.Add(r);
}
// 1) Echte Websuche über DuckDuckGo HTML (liefert Titel, Snippet, URL)
try
{
var web = await _duckDuckGo.SearchWebAsync(query, maxResults);
foreach (var r in web)
Add(new FactSearchResult { Title = r.Heading, Snippet = r.AbstractText, Url = r.AbstractUrl });
}
catch
{
// Weiter mit nächster Quelle
}
// 2) DuckDuckGo Instant Answer (enzyklopädische Antworten)
if (results.Count < maxResults)
{
try
{
var ddg = await _duckDuckGo.SearchAsync(query, maxResults);
foreach (var r in ddg)
{
var url = string.IsNullOrWhiteSpace(r.AbstractUrl) ? r.RelatedUrls.FirstOrDefault() ?? string.Empty : r.AbstractUrl;
if (string.IsNullOrWhiteSpace(url)) continue;
var snippet = string.IsNullOrWhiteSpace(r.AbstractText)
? string.Join(" | ", r.RelatedTexts)
: r.AbstractText;
Add(new FactSearchResult
{
Title = string.IsNullOrWhiteSpace(r.Heading) ? url : r.Heading,
Snippet = snippet,
Url = url
});
}
}
catch
{
// Weiter
}
}
// 3) Wikipedia-Fallback
if (results.Count < maxResults)
{
try
{
var wiki = await _wikipedia.SearchAsync(query, maxResults);
foreach (var w in wiki)
Add(new FactSearchResult { Title = w.Title, Snippet = w.Snippet, Url = w.Url });
}
catch
{
// Keine Ergebnisse
}
}
return results.Take(maxResults).ToList();
}
public async Task<string> SummarizeAsync(string query, string text, string url)
{
var info = string.IsNullOrWhiteSpace(text) ? query : text;
if (info.Length > 2500) info = info[..2500];
var system = $"Du bist ein Faktenprüfer und Autor. Erstelle eine kurze, präzise Zusammenfassung (maximal 3 Sätze) über das Thema \"{query}\" auf Deutsch. Nutze nur die gegebenen Informationen. Gib NUR die Zusammenfassung zurück, ohne Anführungszeichen.";
var result = await _ollama.Chat(_settings.StyleModel, info, system, 0.3, _settings.StyleBackend);
return (result ?? string.Empty).Trim().Trim('"', '\'', '\n', '\r');
}
private static string SanitizeQuery(string raw)
{
if (string.IsNullOrWhiteSpace(raw)) return raw ?? string.Empty;
var q = raw.Trim();
q = q.Trim('"', '\'', '.', ':');
q = System.Text.RegularExpressions.Regex.Replace(q, @"^Suche\s*:\s*", "", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
q = System.Text.RegularExpressions.Regex.Replace(q, @"^Query\s*:\s*", "", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
return q;
}
private static string GetString(JsonElement element, string property)
{
if (element.TryGetProperty(property, out var value) && value.ValueKind == JsonValueKind.String)
return value.GetString() ?? string.Empty;
return string.Empty;
}
private static McpToolResult Error(string toolName, string message) =>
new() { IsError = true, ToolName = toolName, Content = JsonSerializer.Serialize(new { error = message }) };
}

View file

@ -0,0 +1,9 @@
using AuthorBuddy.Web.Models;
namespace AuthorBuddy.Web.Services;
public interface IDuckDuckGoService
{
Task<List<DuckDuckGoResult>> SearchAsync(string query, int maxResults = 3);
Task<List<DuckDuckGoResult>> SearchWebAsync(string query, int maxResults = 5);
}

11
Services/IExportPlugin.cs Normal file
View file

@ -0,0 +1,11 @@
using AuthorBuddy.Web.Models;
namespace AuthorBuddy.Web.Services;
public interface IExportPlugin
{
string Id { get; }
string Name { get; }
string Description { get; }
Task<PluginResult> ExportAsync(ExportPluginContext context);
}

View file

@ -0,0 +1,8 @@
using AuthorBuddy.Web.Models;
namespace AuthorBuddy.Web.Services;
public interface IFactCheckService
{
Task<List<FactCheckResult>> VerifyAsync(string answer, string? backendName = null, int maxClaims = 3);
}

View file

@ -0,0 +1,12 @@
using AuthorBuddy.Web.Models;
namespace AuthorBuddy.Web.Services;
public interface IFactSearchToolService
{
IReadOnlyList<McpToolDefinition> Tools { get; }
Task<McpToolResult> CallAsync(string toolName, string argumentsJson);
Task<string> CreateSearchQueryAsync(string factText);
Task<List<FactSearchResult>> SearchAsync(string query, int maxResults = 5);
Task<string> SummarizeAsync(string query, string text, string url);
}

View file

@ -0,0 +1,8 @@
namespace AuthorBuddy.Web.Services;
public interface ILlmWikiService
{
Task CompileWikiAsync(int projectId, string rootPath, IProgress<string> progress);
Task<string> QueryWikiAsync(int projectId, string question);
bool WikiExists(int projectId, string rootPath);
}

View file

@ -6,18 +6,20 @@ namespace AuthorBuddy.Web.Services;
public interface IOllamaService
{
bool SupportsModelManagement { get; }
Task<IEnumerable<LLM_Model>> GetLocalModelsAsync();
Task<IEnumerable<LLM_Model>> GetModelsForBackendAsync(string backendName);
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<float[]> GetEmbeddingAsync(string text, string? backendName = null);
Task<bool> TestEmbeddingAsync(string model, string? backendName = null);
Task<string> Chat(string model, string userMessage, string? systemPrompt = null, double? temperature = null, string? backendName = null);
Task ChatStream(string model, string userMessage, Func<string, Task> onToken, string? systemPrompt = null, double? temperature = null, string? backendName = null);
Task<string> AnswerBasedOnFactsAsync(string question, string facts);
Task<(bool Success, long LatencyMs)> TestConnectionAsync();
Task<bool> ModelExistsAsync(string model);
Task<bool> ModelExistsAsync(string model, string? backendName = null);
Task<bool> DeleteModelAsync(string modelName);
Task<(bool Success, int ContextWindow)> GetModelContextWindowAsync(string model);
Task<long> MeasureLatencyAsync(string model);
Task<(bool Success, int ContextWindow)> GetModelContextWindowAsync(string model, string? backendName = null);
Task<long> MeasureLatencyAsync(string model, string? backendName = null);
void Reconfigure(string ollamaUrl);
}

View file

@ -0,0 +1,11 @@
using AuthorBuddy.Web.Models;
namespace AuthorBuddy.Web.Services;
public interface IPluginManager
{
IReadOnlyList<IExportPlugin> GetPlugins();
IExportPlugin? GetPlugin(string id);
Task<PluginConfig?> GetConfigAsync(string pluginId);
Task SaveConfigAsync(PluginConfig config);
}

View file

@ -14,9 +14,16 @@ public interface ISettingsService
Task SaveOllamaUrlAsync(string url);
Task SaveLlamaCppUrlAsync(string url);
Task SaveBackendAsync(string backend);
void SetBackend(string backend);
Task SaveRagModelAsync(string model);
Task SaveStyleModelAsync(string model);
Task SaveSpellingModelAsync(string model);
Task<string?> GetRagBackendAsync();
Task<string?> GetStyleBackendAsync();
Task<string?> GetSpellingBackendAsync();
Task SaveRagBackendAsync(string backend);
Task SaveStyleBackendAsync(string backend);
Task SaveSpellingBackendAsync(string backend);
Task SaveThemeAsync(string theme);
Task<string?> GetLektorSystemPromptAsync();
@ -86,10 +93,24 @@ public interface ISettingsService
Task SaveChatPromptAsync(string prompt);
Task<double> GetChatTempAsync();
Task SaveChatTempAsync(double temp);
Task<bool> GetChatFactCheckEnabledAsync();
Task SaveChatFactCheckEnabledAsync(bool enabled);
Task<string?> GetFactSearchPromptAsync();
Task SaveFactSearchPromptAsync(string prompt);
Task<double> GetFactSearchTempAsync();
Task SaveFactSearchTempAsync(double temp);
Task<string?> GetLanguageAsync();
Task SaveLanguageAsync(string language);
Task<string?> GetOllamaCloudUrlAsync();
Task SaveOllamaCloudUrlAsync(string url);
Task<string?> GetOllamaCloudApiKeyAsync();
Task SaveOllamaCloudApiKeyAsync(string apiKey);
Task<string?> GetPluginConfigAsync(string pluginId);
Task SavePluginConfigAsync(string pluginId, string json);
Task<List<UserEntity>> GetUsersAsync();
Task<UserEntity> GetCurrentUserAsync();
void SetCurrentUser(int userId);

137
Services/LlmWikiService.cs Normal file
View file

@ -0,0 +1,137 @@
using System.Text;
using AuthorBuddy.Web.Models;
namespace AuthorBuddy.Web.Services;
public class LlmWikiService : ILlmWikiService
{
private readonly IOllamaService _ollama;
private readonly OllamaSettings _settings;
private readonly IProjectService _projectService;
public LlmWikiService(IOllamaService ollama, OllamaSettings settings, IProjectService projectService)
{
_ollama = ollama;
_settings = settings;
_projectService = projectService;
}
public bool WikiExists(int projectId, string rootPath)
{
var wikiDir = GetWikiDir(rootPath);
return Directory.Exists(wikiDir) && Directory.GetFiles(wikiDir, "*.md").Length > 0;
}
public async Task CompileWikiAsync(int projectId, string rootPath, IProgress<string> progress)
{
var wikiDir = GetWikiDir(rootPath);
Directory.CreateDirectory(wikiDir);
var files = Directory.GetFiles(rootPath, "*.md", SearchOption.AllDirectories)
.Where(f => !f.Contains("\\wiki\\") && !f.Contains("/wiki/"))
.ToList();
if (files.Count == 0)
{
progress?.Report("Keine Markdown-Dateien gefunden.");
return;
}
var indexEntries = new List<string>();
foreach (var file in files)
{
var fileName = Path.GetFileNameWithoutExtension(file);
var safeName = SanitizeName(fileName);
progress?.Report($"Komprimiere: {fileName}...");
var content = await File.ReadAllTextAsync(file);
if (string.IsNullOrWhiteSpace(content))
{
progress?.Report(" ↳ leer, übersprungen");
continue;
}
var wikiContent = await CompilePage(content, fileName);
var wikiPath = Path.Combine(wikiDir, $"{safeName}.md");
await File.WriteAllTextAsync(wikiPath, wikiContent);
indexEntries.Add($"- [{fileName}]({safeName}.md)");
progress?.Report(" ↳ ✓ Wiki-Seite erstellt");
}
var indexContent = "# Wiki-Index\n\n" +
$"Erstellt am {DateTime.Now:dd.MM.yyyy HH:mm} aus {files.Count} Quellen.\n\n" +
string.Join("\n", indexEntries) + "\n";
await File.WriteAllTextAsync(Path.Combine(wikiDir, "index.md"), indexContent);
progress?.Report($"Wiki kompiliert: {indexEntries.Count} Seiten.");
}
public async Task<string> QueryWikiAsync(int projectId, string question)
{
var project = await _projectService.GetProjectAsync(projectId);
if (project == null)
return "Projekt nicht gefunden.";
var wikiDir = GetWikiDir(project.RootPath);
if (!Directory.Exists(wikiDir))
return "Wiki noch nicht kompiliert. Bitte zuerst das Wiki erstellen.";
var wikiFiles = Directory.GetFiles(wikiDir, "*.md");
var wikiContent = new StringBuilder();
foreach (var file in wikiFiles.OrderBy(f => f))
{
var content = await File.ReadAllTextAsync(file);
wikiContent.AppendLine($"--- {Path.GetFileName(file)} ---");
wikiContent.AppendLine(content);
wikiContent.AppendLine();
}
var prompt = $@"Du bist ein Wiki-Assistent. Beantworte die folgende Frage ausschließlich basierend auf dem untenstehenden Wiki. Wenn die Antwort nicht im Wiki zu finden ist, sage das deutlich.
WIKI:
{wikiContent}
FRAGE: {question}";
return await _ollama.Chat(_settings.StyleModel, prompt, temperature: 0.3, backendName: _settings.StyleBackend);
}
private async Task<string> CompilePage(string content, string title)
{
var prompt = $@"Du bist ein Wiki-Redakteur. Erstelle eine strukturierte Wiki-Seite aus dem folgenden Dokument.
FORMAT:
# {title}
## Zusammenfassung
Kurze Zusammenfassung (2-3 Sätze)
## Kernkonzepte
- Konzept 1: Beschreibung
- Konzept 2: Beschreibung
## Details
Wichtige Fakten und Details aus dem Dokument
## Verknüpfungen
Verweise auf verwandte Themen (falls erkennbar)
DOKUMENT:
{content}";
return await _ollama.Chat(_settings.StyleModel, prompt, temperature: 0.3, backendName: _settings.StyleBackend);
}
private static string GetWikiDir(string rootPath) =>
Path.Combine(rootPath, "wiki");
private static string SanitizeName(string name)
{
var invalid = Path.GetInvalidFileNameChars();
var sanitized = new string(name.Where(c => !invalid.Contains(c)).ToArray());
return string.IsNullOrWhiteSpace(sanitized) ? "unnamed" : sanitized;
}
}

View file

@ -0,0 +1,225 @@
using System.Diagnostics;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.RegularExpressions;
using AuthorBuddy.Web.Models;
namespace AuthorBuddy.Web.Services;
public class OllamaCloudBackend : ILLMBackend
{
private readonly IHttpClientFactory _httpFactory;
private readonly OllamaSettings _settings;
public string Name => "Ollama Cloud";
public bool SupportsModelManagement => false;
public OllamaCloudBackend(OllamaSettings settings, IHttpClientFactory httpFactory)
{
_settings = settings;
_httpFactory = httpFactory;
}
private HttpClient CreateClient()
{
var client = _httpFactory.CreateClient();
client.Timeout = TimeSpan.FromSeconds(300);
client.BaseAddress = new Uri(_settings.OllamaCloudUrl.TrimEnd('/') + "/");
if (!string.IsNullOrEmpty(_settings.OllamaCloudApiKey))
{
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", _settings.OllamaCloudApiKey);
}
return client;
}
public async Task<bool> TestConnectionAsync()
{
try
{
var http = CreateClient();
var sw = Stopwatch.StartNew();
var response = await http.GetAsync("api/tags");
sw.Stop();
return response.IsSuccessStatusCode;
}
catch { return false; }
}
public async Task<IEnumerable<LLM_Model>> GetModelsAsync()
{
try
{
var http = CreateClient();
var response = await http.GetAsync("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 http = CreateClient();
var response = await http.PostAsJsonAsync("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);
var http = CreateClient();
using var response = await http.PostAsync(
"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 static 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 http = CreateClient();
var response = await http.PostAsJsonAsync("api/embeddings", new { model = m, prompt = text });
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)
{
return Task.FromResult(false);
}
public Task<bool> DeleteModelAsync(string name)
{
return Task.FromResult(false);
}
public async Task<int?> GetContextSizeAsync(string model)
{
try
{
var http = CreateClient();
var response = await http.PostAsJsonAsync("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 http = CreateClient();
var sw = Stopwatch.StartNew();
var response = await http.PostAsJsonAsync("api/generate", new { model, prompt = "Hallo", stream = false });
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 http = CreateClient();
var response = http.PostAsJsonAsync("api/embeddings", new { model, prompt = "test" }).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; }
}
}

View file

@ -18,18 +18,19 @@ public class OllamaService : IOllamaService
_opState = opState;
}
private ILLMBackend ActiveBackend =>
_backends.FirstOrDefault(b => b.Name == _settings.Backend) ?? _backends.First();
private readonly object _warmLock = new();
private bool _warmupAttempted;
private async Task EnsureRunningAsync()
private ILLMBackend GetBackend(string? name) =>
_backends.FirstOrDefault(b => b.Name == (name ?? _settings.Backend)) ?? _backends.First();
private async Task EnsureRunningAsync(string? backendName = null)
{
if (ActiveBackend.Name != "Ollama")
var backend = GetBackend(backendName);
if (backend.Name != "Ollama")
return;
var reachable = await ActiveBackend.TestConnectionAsync();
var reachable = await backend.TestConnectionAsync();
if (reachable)
{
await WarmupModelAsync();
@ -62,7 +63,7 @@ public class OllamaService : IOllamaService
for (int i = 0; i < 30; i++)
{
await Task.Delay(1000);
reachable = await ActiveBackend.TestConnectionAsync();
reachable = await backend.TestConnectionAsync();
if (reachable)
{
_opState.AppendThought(" Ollama ist bereit");
@ -83,21 +84,28 @@ public class OllamaService : IOllamaService
_warmupAttempted = true;
}
var modelsToWarm = new[] { _settings.StyleModel, _settings.EmbedModel }
.Where(m => !string.IsNullOrWhiteSpace(m))
.Distinct()
var modelsToWarm = new[]
{
(Model: _settings.StyleModel, Backend: _settings.StyleBackend),
(Model: _settings.EmbedModel, Backend: _settings.RagBackend)
}
.Where(m => !string.IsNullOrWhiteSpace(m.Model))
.DistinctBy(m => m.Model)
.ToList();
if (modelsToWarm.Count == 0) return;
_opState.SetThought("Lade Modelle in den Arbeitsspeicher...");
foreach (var model in modelsToWarm)
foreach (var (model, backendName) in modelsToWarm)
{
_opState.AppendThought($" {model}...");
var backend = GetBackend(backendName);
if (backend.Name != "Ollama")
continue;
_opState.AppendThought($" {model} ({backendName})...");
try
{
var warmCts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
var dummy = await ActiveBackend.ChatAsync(model, " ", null, 0.0);
var dummy = await backend.ChatAsync(model, " ", null, 0.0);
_opState.AppendThought(" ✓");
}
catch (Exception ex)
@ -115,89 +123,183 @@ public class OllamaService : IOllamaService
public async Task<(bool Success, long LatencyMs)> TestConnectionAsync()
{
var backend = GetBackend(null);
await EnsureRunningAsync();
_opState.SetThought($"Teste Verbindung zu {ActiveBackend.Name}...");
_opState.SetThought($"Teste Verbindung zu {backend.Name}...");
var sw = Stopwatch.StartNew();
var result = await ActiveBackend.TestConnectionAsync();
var result = await backend.TestConnectionAsync();
sw.Stop();
_opState.AppendThought(result ? " OK" : " Fehlgeschlagen");
return (result, result ? sw.ElapsedMilliseconds : 0);
}
public bool SupportsModelManagement => GetBackend(null).SupportsModelManagement;
public async Task<IEnumerable<LLM_Model>> GetLocalModelsAsync()
{
var backend = GetBackend(null);
await EnsureRunningAsync();
_opState.SetThought($"Lade Modelle von {ActiveBackend.Name}...");
var models = await ActiveBackend.GetModelsAsync();
_opState.SetThought($"Lade Modelle von {backend.Name}...");
var models = await backend.GetModelsAsync();
_opState.AppendThought($" {models.Count()} gefunden");
return models;
}
public async Task<bool> TestEmbeddingAsync(string model)
public async Task<IEnumerable<LLM_Model>> GetModelsForBackendAsync(string backendName)
{
var backend = GetBackend(backendName);
if (backend.Name == "Ollama")
await EnsureRunningAsync();
return ActiveBackend.TestEmbedding(model);
_opState.SetThought($"Lade Modelle von {backend.Name}...");
var models = await backend.GetModelsAsync();
_opState.AppendThought($" {models.Count()} gefunden");
return models;
}
public async Task<float[]> GetEmbeddingAsync(string text)
public async Task<bool> TestEmbeddingAsync(string model, string? backendName = null)
{
await EnsureRunningAsync();
return await ActiveBackend.GetEmbeddingAsync(text, _settings.EmbedModel);
var backend = GetBackend(backendName);
await EnsureRunningAsync(backendName);
return backend.TestEmbedding(model);
}
public async Task<float[]> GetEmbeddingAsync(string text, string? backendName = null)
{
var backend = GetBackend(backendName);
_opState.SetBusy(true);
_opState.SetThought($"{backend.Name}: Erzeuge Embeddings...");
try
{
await EnsureRunningAsync(backendName);
return await backend.GetEmbeddingAsync(text, _settings.EmbedModel);
}
catch
{
_opState.SetThought("Fehler bei Embeddings");
throw;
}
finally
{
_opState.SetBusy(false);
}
}
public async Task<string> QuickSpellCheckAsync(string text)
{
await EnsureRunningAsync();
return await Chat(_settings.SpellingModel, text, _settings.SpellingPrompt, _settings.SpellingTemp);
_opState.SetBusy(true);
_opState.SetThought("Rechtschreibprüfung läuft...");
try
{
await EnsureRunningAsync(_settings.SpellingBackend);
return await Chat(_settings.SpellingModel, text, _settings.SpellingPrompt, _settings.SpellingTemp, _settings.SpellingBackend);
}
catch
{
_opState.SetThought("Fehler bei Rechtschreibprüfung");
throw;
}
finally
{
_opState.SetBusy(false);
}
}
public async Task<string> StreamStyleCheckAsync(string text)
{
await EnsureRunningAsync();
return await Chat(_settings.StyleModel, text, _settings.LektorSystemPrompt, _settings.LektorTemp);
_opState.SetBusy(true);
_opState.SetThought("Stilprüfung läuft...");
try
{
await EnsureRunningAsync(_settings.StyleBackend);
return await Chat(_settings.StyleModel, text, _settings.LektorSystemPrompt, _settings.LektorTemp, _settings.StyleBackend);
}
catch
{
_opState.SetThought("Fehler bei Stilprüfung");
throw;
}
finally
{
_opState.SetBusy(false);
}
}
public async Task<string> AnswerBasedOnFactsAsync(string question, string facts)
{
await EnsureRunningAsync();
_opState.SetBusy(true);
_opState.SetThought("Faktencheck läuft...");
try
{
await EnsureRunningAsync(_settings.RagBackend);
var prompt = _settings.GetFactCheckPrompt(question, facts);
return await Chat(_settings.EmbedModel, prompt, temperature: _settings.FactCheckTemp);
return await Chat(_settings.EmbedModel, prompt, temperature: _settings.FactCheckTemp, backendName: _settings.RagBackend);
}
catch
{
_opState.SetThought("Fehler bei Faktencheck");
throw;
}
finally
{
_opState.SetBusy(false);
}
}
public async Task<bool> ModelExistsAsync(string model)
public async Task<bool> ModelExistsAsync(string model, string? backendName = null)
{
await EnsureRunningAsync();
return await ActiveBackend.ModelExistsAsync(model);
var backend = GetBackend(backendName);
await EnsureRunningAsync(backendName);
return await backend.ModelExistsAsync(model);
}
public async Task<(bool Success, int ContextWindow)> GetModelContextWindowAsync(string model)
public async Task<(bool Success, int ContextWindow)> GetModelContextWindowAsync(string model, string? backendName = null)
{
await EnsureRunningAsync();
return await ActiveBackend.GetModelContextWindowAsync(model);
var backend = GetBackend(backendName);
await EnsureRunningAsync(backendName);
return await backend.GetModelContextWindowAsync(model);
}
public async Task<long> MeasureLatencyAsync(string model)
public async Task<long> MeasureLatencyAsync(string model, string? backendName = null)
{
await EnsureRunningAsync();
return await ActiveBackend.MeasureLatencyAsync(model);
var backend = GetBackend(backendName);
await EnsureRunningAsync(backendName);
return await backend.MeasureLatencyAsync(model);
}
public async Task<string> Chat(string model, string userMessage, string? systemPrompt = null, double? temperature = null)
public async Task<string> Chat(string model, string userMessage, string? systemPrompt = null, double? temperature = null, string? backendName = null)
{
await EnsureRunningAsync();
_opState.SetThought($"{ActiveBackend.Name}: Sende Anfrage an {model}...");
var result = await ActiveBackend.ChatAsync(model, userMessage, systemPrompt, temperature);
var backend = GetBackend(backendName);
_opState.SetBusy(true);
_opState.SetThought($"{backend.Name}: Sende Anfrage an {model}...");
try
{
await EnsureRunningAsync(backendName);
var result = await backend.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)
catch
{
await EnsureRunningAsync();
_opState.SetThought($"{ActiveBackend.Name}: Verbinde mit Modell {model}...");
_opState.SetThought($"Fehler bei {model}");
throw;
}
finally
{
_opState.SetBusy(false);
}
}
public async Task ChatStream(string model, string userMessage, Func<string, Task> onToken, string? systemPrompt = null, double? temperature = null, string? backendName = null)
{
var backend = GetBackend(backendName);
_opState.SetBusy(true);
_opState.SetThought($"{backend.Name}: Verbinde mit Modell {model}...");
try
{
await EnsureRunningAsync(backendName);
int tokenCount = 0;
await ActiveBackend.ChatStreamAsync(model, userMessage, async chunk =>
await backend.ChatStreamAsync(model, userMessage, async chunk =>
{
tokenCount++;
_opState.AppendThought(chunk);
@ -206,21 +308,33 @@ public class OllamaService : IOllamaService
_opState.AppendThought($" Fertig ({tokenCount} Token)");
}
catch
{
_opState.SetThought($"Fehler bei {model}");
throw;
}
finally
{
_opState.SetBusy(false);
}
}
public async Task<bool> PullModelAsync(string modelName, IProgress<string> progress)
{
var backend = GetBackend(null);
await EnsureRunningAsync();
_opState.SetThought($"Lade Modell {modelName}...");
var result = await ActiveBackend.PullModelAsync(modelName, progress);
var result = await backend.PullModelAsync(modelName, progress);
_opState.AppendThought(result ? " Erfolgreich" : " Fehlgeschlagen");
return result;
}
public async Task<bool> DeleteModelAsync(string modelName)
{
var backend = GetBackend(null);
await EnsureRunningAsync();
_opState.SetThought($"Lösche Modell {modelName}...");
var result = await ActiveBackend.DeleteModelAsync(modelName);
var result = await backend.DeleteModelAsync(modelName);
_opState.AppendThought(result ? " Erfolgreich" : " Fehlgeschlagen");
return result;
}

View file

@ -17,6 +17,12 @@ public class OperationState
Notify();
}
public void SetBusy(bool busy)
{
IsBusy = busy;
Notify();
}
public void SetThought(string text)
{
ThoughtText = text;

38
Services/PluginManager.cs Normal file
View file

@ -0,0 +1,38 @@
using AuthorBuddy.Web.Models;
namespace AuthorBuddy.Web.Services;
public class PluginManager : IPluginManager
{
private readonly List<IExportPlugin> _plugins = new();
private readonly ISettingsService _settings;
public PluginManager(ISettingsService settings, IEnumerable<IExportPlugin> plugins)
{
_settings = settings;
_plugins.AddRange(plugins);
}
public void Register(IExportPlugin plugin)
{
if (!_plugins.Any(p => p.Id == plugin.Id))
_plugins.Add(plugin);
}
public IReadOnlyList<IExportPlugin> GetPlugins() => _plugins.AsReadOnly();
public IExportPlugin? GetPlugin(string id) => _plugins.FirstOrDefault(p => p.Id == id);
public async Task<PluginConfig?> GetConfigAsync(string pluginId)
{
var json = await _settings.GetPluginConfigAsync(pluginId);
if (string.IsNullOrEmpty(json)) return null;
return System.Text.Json.JsonSerializer.Deserialize<PluginConfig>(json);
}
public async Task SaveConfigAsync(PluginConfig config)
{
var json = System.Text.Json.JsonSerializer.Serialize(config);
await _settings.SavePluginConfigAsync(config.PluginId, json);
}
}

View file

@ -1,4 +1,5 @@
using AuthorBuddy.Web.Data;
using AuthorBuddy.Web.Models;
using LiteDB;
using System.Security.Cryptography;
using System.Text;
@ -9,11 +10,13 @@ public class RagService : IRagService
{
private readonly ILiteDatabase _db;
private readonly IOllamaService _ollama;
private readonly OllamaSettings _settings;
public RagService(ILiteDatabase db, IOllamaService ollama)
public RagService(ILiteDatabase db, IOllamaService ollama, OllamaSettings settings)
{
_db = db;
_ollama = ollama;
_settings = settings;
}
public async Task IndexProjectAsync(int projectId, string rootPath, IProgress<string> progress)
@ -43,7 +46,7 @@ public class RagService : IRagService
}
}
var embedding = await _ollama.GetEmbeddingAsync(content);
var embedding = await _ollama.GetEmbeddingAsync(content, _settings.RagBackend);
if (embedding.Length == 0)
{
progress?.Report($" ↳ Fehler beim Embedding, übersprungen");
@ -97,7 +100,7 @@ public class RagService : IRagService
public async Task<List<RagSearchResult>> SearchAsync(int projectId, string query, int topN = 5)
{
var queryVec = await _ollama.GetEmbeddingAsync(query);
var queryVec = await _ollama.GetEmbeddingAsync(query, _settings.RagBackend);
if (queryVec.Length == 0)
return new List<RagSearchResult>();

View file

@ -7,11 +7,16 @@ namespace AuthorBuddy.Web.Services;
public class SettingsService : ISettingsService
{
private const string KeyOllamaUrl = "ollama_url";
private const string KeyOllamaCloudUrl = "ollama_cloud_url";
private const string KeyOllamaCloudApiKey = "ollama_cloud_api_key";
private const string KeyLlamaCppUrl = "llama_cpp_url";
private const string KeyBackend = "llm_backend";
private const string KeyRagModel = "ollama_rag_model";
private const string KeyRagBackend = "ollama_rag_backend";
private const string KeyStyleModel = "ollama_style_model";
private const string KeyStyleBackend = "ollama_style_backend";
private const string KeySpellingModel = "ollama_spelling_model";
private const string KeySpellingBackend = "ollama_spelling_backend";
private const string KeyTheme = "theme";
private const string KeyLektorPrompt = "ollama_lektor_prompt";
private const string KeyFactCheckTemplate = "ollama_fact_check";
@ -45,6 +50,9 @@ public class SettingsService : ISettingsService
private const string KeyQuickMarkdownTemp = "quick_md_temp";
private const string KeyChatPrompt = "chat_prompt";
private const string KeyChatTemp = "chat_temp";
private const string KeyChatFactCheck = "chat_fact_check";
private const string KeyFactSearchPrompt = "fact_search_prompt";
private const string KeyFactSearchTemp = "fact_search_temp";
private const string KeyLanguage = "language";
private const string KeyLastProjectId = "last_project_id";
@ -169,12 +177,16 @@ public class SettingsService : ISettingsService
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?> GetRagBackendAsync() => Task.FromResult(GetGlobal(KeyRagBackend));
public Task<string?> GetStyleBackendAsync() => Task.FromResult(GetGlobal(KeyStyleBackend));
public Task<string?> GetSpellingBackendAsync() => Task.FromResult(GetGlobal(KeySpellingBackend));
public Task<string?> GetAdvocatusDiaboliPromptAsync() => Task.FromResult(Get(KeyAdvocatusDiaboli));
public Task<string?> GetHookFinderPromptAsync() => Task.FromResult(Get(KeyHookFinder));
public Task<string?> GetEli5PromptAsync() => Task.FromResult(Get(KeyEli5));
@ -217,6 +229,24 @@ public class SettingsService : ISettingsService
return Task.CompletedTask;
}
public Task<string?> GetOllamaCloudUrlAsync() => Task.FromResult(GetGlobal(KeyOllamaCloudUrl));
public Task SaveOllamaCloudUrlAsync(string url)
{
SetGlobal(KeyOllamaCloudUrl, url);
_ollamaConfig.OllamaCloudUrl = url;
return Task.CompletedTask;
}
public Task<string?> GetOllamaCloudApiKeyAsync() => Task.FromResult(GetGlobal(KeyOllamaCloudApiKey));
public Task SaveOllamaCloudApiKeyAsync(string apiKey)
{
SetGlobal(KeyOllamaCloudApiKey, apiKey);
_ollamaConfig.OllamaCloudApiKey = apiKey;
return Task.CompletedTask;
}
public Task SaveLlamaCppUrlAsync(string url)
{
SetGlobal(KeyLlamaCppUrl, url);
@ -224,6 +254,11 @@ public class SettingsService : ISettingsService
return Task.CompletedTask;
}
public void SetBackend(string backend)
{
_ollamaConfig.Backend = backend;
}
public Task SaveBackendAsync(string backend)
{
SetGlobal(KeyBackend, backend);
@ -245,6 +280,27 @@ public class SettingsService : ISettingsService
return Task.CompletedTask;
}
public Task SaveRagBackendAsync(string backend)
{
SetGlobal(KeyRagBackend, backend);
_ollamaConfig.RagBackend = backend;
return Task.CompletedTask;
}
public Task SaveStyleBackendAsync(string backend)
{
SetGlobal(KeyStyleBackend, backend);
_ollamaConfig.StyleBackend = backend;
return Task.CompletedTask;
}
public Task SaveSpellingBackendAsync(string backend)
{
SetGlobal(KeySpellingBackend, backend);
_ollamaConfig.SpellingBackend = backend;
return Task.CompletedTask;
}
public Task SaveLektorSystemPromptAsync(string prompt)
{
Set(KeyLektorPrompt, prompt);
@ -386,6 +442,34 @@ public class SettingsService : ISettingsService
return Task.CompletedTask;
}
public Task<bool> GetChatFactCheckEnabledAsync() =>
Task.FromResult(bool.TryParse(Get(KeyChatFactCheck), out var v) ? v : _ollamaConfig.ChatFactCheckEnabled);
public Task SaveChatFactCheckEnabledAsync(bool enabled)
{
Set(KeyChatFactCheck, enabled.ToString());
_ollamaConfig.ChatFactCheckEnabled = enabled;
return Task.CompletedTask;
}
public Task<string?> GetFactSearchPromptAsync() => Task.FromResult(Get(KeyFactSearchPrompt));
public Task SaveFactSearchPromptAsync(string prompt)
{
Set(KeyFactSearchPrompt, prompt);
_ollamaConfig.FactSearchPrompt = prompt;
return Task.CompletedTask;
}
public Task<double> GetFactSearchTempAsync() => Task.FromResult(double.TryParse(Get(KeyFactSearchTemp), out var v) ? v : _ollamaConfig.FactSearchTemp);
public Task SaveFactSearchTempAsync(double temp)
{
Set(KeyFactSearchTemp, temp.ToString("F1"));
_ollamaConfig.FactSearchTemp = temp;
return Task.CompletedTask;
}
public Task SaveTechLastenheftPromptAsync(string prompt)
{
Set(KeyTechLastenheft, prompt);
@ -604,6 +688,35 @@ public class SettingsService : ISettingsService
currentVersion = 2;
}
if (currentVersion < 3)
{
currentVersion = 3;
}
if (currentVersion < 4)
{
var backends = new[] { "Ollama", "Ollama Cloud", "llama.cpp" };
var modelTypes = new[] { KeyRagModel, KeyStyleModel, KeySpellingModel };
foreach (var modelKey in modelTypes)
{
if (settings.FindOne(s => s.Key == modelKey && s.UserId == null && s.ProjectId == null) != null)
continue;
foreach (var backend in backends)
{
var perBackendKey = $"{modelKey}_{backend}";
var existing = settings.FindOne(s => s.Key == perBackendKey && s.UserId == null && s.ProjectId == null);
if (existing != null)
{
settings.Insert(new SettingEntity { Key = modelKey, Value = existing.Value, UserId = null, ProjectId = null });
break;
}
}
}
currentVersion = 4;
}
if (schemaSetting != null)
{
schemaSetting.Value = currentVersion.ToString();
@ -617,7 +730,12 @@ public class SettingsService : ISettingsService
public Task LoadProjectSettingsAsync()
{
var globalKeys = new[] { KeyOllamaUrl, KeyLlamaCppUrl, KeyBackend, KeyRagModel, KeyStyleModel, KeySpellingModel };
var backend = GetGlobal(KeyBackend);
if (backend != null) _ollamaConfig.Backend = backend;
var globalKeys = new[] { KeyOllamaUrl, KeyLlamaCppUrl, KeyBackend,
KeyRagModel, KeyRagBackend, KeyStyleModel, KeyStyleBackend, KeySpellingModel, KeySpellingBackend,
KeyOllamaCloudUrl, KeyOllamaCloudApiKey };
foreach (var key in globalKeys)
{
@ -629,8 +747,13 @@ public class SettingsService : ISettingsService
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 == KeyRagBackend: _ollamaConfig.RagBackend = val; break;
case var k when k == KeyStyleModel: _ollamaConfig.StyleModel = val; break;
case var k when k == KeyStyleBackend: _ollamaConfig.StyleBackend = val; break;
case var k when k == KeySpellingModel: _ollamaConfig.SpellingModel = val; break;
case var k when k == KeySpellingBackend: _ollamaConfig.SpellingBackend = val; break;
case var k when k == KeyOllamaCloudUrl: _ollamaConfig.OllamaCloudUrl = val; break;
case var k when k == KeyOllamaCloudApiKey: _ollamaConfig.OllamaCloudApiKey = val; break;
}
}
@ -691,6 +814,7 @@ public class SettingsService : ISettingsService
public Task InitializeAsync()
{
MigrateSchema();
LoadProjectSettingsAsync();
var settings = _db.GetCollection<SettingEntity>("settings");
@ -700,8 +824,13 @@ public class SettingsService : ISettingsService
(KeyLlamaCppUrl, _ollamaConfig.LlamaCppUrl),
(KeyBackend, _ollamaConfig.Backend),
(KeyRagModel, _ollamaConfig.EmbedModel),
(KeyRagBackend, _ollamaConfig.RagBackend),
(KeyStyleModel, _ollamaConfig.StyleModel),
(KeyStyleBackend, _ollamaConfig.StyleBackend),
(KeySpellingModel, _ollamaConfig.SpellingModel),
(KeySpellingBackend, _ollamaConfig.SpellingBackend),
(KeyOllamaCloudUrl, _ollamaConfig.OllamaCloudUrl),
(KeyOllamaCloudApiKey, _ollamaConfig.OllamaCloudApiKey),
};
foreach (var entry in globalEntries)
@ -709,17 +838,7 @@ public class SettingsService : ISettingsService
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
if (existing == null)
{
settings.Insert(new SettingEntity
{
@ -827,4 +946,16 @@ public class SettingsService : ISettingsService
return Task.CompletedTask;
}
public Task<string?> GetPluginConfigAsync(string pluginId)
{
var val = GetGlobal($"plugin_{pluginId}");
return Task.FromResult(val);
}
public Task SavePluginConfigAsync(string pluginId, string json)
{
SetGlobal($"plugin_{pluginId}", json);
return Task.CompletedTask;
}
}

View file

@ -797,6 +797,22 @@ button.primary {
resize: vertical;
}
.settings-model-row {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.settings-model-row label {
min-width: 100px;
}
.settings-model-row select {
flex: 1;
min-width: 120px;
}
.settings-actions {
display: flex;
gap: 8px;
@ -1743,6 +1759,116 @@ button.primary {
min-width: 40px;
}
.factcheck-section {
margin-top: 10px;
padding-top: 8px;
border-top: 1px dashed var(--border-color);
}
.factcheck-title {
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
color: var(--muted-color, #888);
margin-bottom: 6px;
}
.factcheck-item {
margin-bottom: 8px;
padding: 8px 10px;
background: var(--bg-color, #f8f8f8);
border-radius: 4px;
border-left: 3px solid var(--border-color);
}
.factcheck-header {
margin-bottom: 4px;
}
.factcheck-verdict {
font-size: 13px;
font-weight: bold;
}
.verdict-confirmed {
color: #2e7d32;
}
.verdict-contradicted {
color: #c62828;
}
.verdict-partial {
color: #b26a00;
}
.verdict-none {
color: #757575;
}
.factcheck-claim {
font-size: 13px;
font-style: italic;
color: var(--text-color);
margin-bottom: 4px;
}
.factcheck-reasoning {
font-size: 13px;
color: var(--text-color);
margin-bottom: 4px;
}
.factcheck-sources {
font-size: 12px;
}
.factcheck-source {
color: var(--muted-color, #888);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.factcheck-toggle {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
cursor: pointer;
}
.thinking-block {
margin-bottom: 8px;
font-size: 13px;
}
.thinking-block summary {
cursor: pointer;
font-size: 12px;
color: var(--text-color);
opacity: 0.6;
padding: 4px 0;
user-select: none;
}
.thinking-block summary:hover {
opacity: 1;
}
.thinking-content {
background: var(--border-color);
color: var(--text-color);
padding: 8px 10px;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
line-height: 1.4;
margin-top: 4px;
white-space: pre-wrap;
word-break: break-word;
}
.chat-input {
display: flex;
gap: 8px;
@ -1972,3 +2098,192 @@ button.primary {
flex-wrap: wrap;
}
}
.llm-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.llm-modal {
width: 92%;
max-width: 560px;
background: var(--window-bg);
border: 2px solid var(--border-color);
box-shadow: 4px 4px 8px rgba(0,0,0,0.3);
}
.llm-title {
display: flex;
align-items: center;
gap: 10px;
background: var(--accent-color);
color: white;
padding: 6px 12px;
font-weight: bold;
font-size: 14px;
}
.llm-title-spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(255,255,255,0.4);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
flex-shrink: 0;
}
.llm-body {
padding: 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
.llm-status {
font-size: 13px;
font-weight: bold;
color: var(--text-color);
}
.llm-thoughts {
font-size: 13px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
max-height: 40vh;
overflow-y: auto;
padding: 10px 12px;
border: 1px solid var(--border-color);
background: var(--editor-bg);
color: var(--text-color);
}
.llm-thoughts-empty {
font-style: italic;
color: var(--text-muted, #888);
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.qm-fact-drawer {
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 420px;
max-width: 94vw;
background: var(--window-bg);
border-left: 2px solid var(--border-color);
box-shadow: -4px 0 12px rgba(0,0,0,0.25);
z-index: 9000;
display: flex;
flex-direction: column;
}
.qm-fact-drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--accent-color);
color: white;
padding: 8px 12px;
font-weight: bold;
font-size: 14px;
}
.qm-fact-drawer-body {
padding: 16px;
overflow-y: auto;
flex: 1;
}
.qm-fact-claim {
padding: 8px 10px;
background: var(--editor-bg);
border: 1px solid var(--border-color);
border-left: 3px solid var(--accent-color);
font-size: 13px;
line-height: 1.5;
}
.qm-fact-query-row {
display: flex;
gap: 6px;
margin-bottom: 6px;
}
.qm-fact-query-row input {
flex: 1;
padding: 6px 8px;
border: 1px solid var(--border-color);
background: var(--editor-bg);
color: var(--text-color);
font-size: 13px;
}
.qm-fact-loading {
padding: 12px;
text-align: center;
color: var(--text-muted, #888);
font-style: italic;
font-size: 13px;
}
.qm-fact-error {
padding: 8px 10px;
background: #fdecea;
border: 1px solid #e0b4b4;
color: #a94442;
font-size: 13px;
margin-bottom: 8px;
}
.qm-fact-results {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 4px;
}
.qm-fact-result {
padding: 10px 12px;
border: 1px solid var(--border-color);
background: var(--card-bg);
}
.qm-fact-result a {
font-weight: bold;
font-size: 13px;
word-break: break-word;
}
.qm-fact-snippet {
font-size: 12px;
color: var(--text-color);
margin: 4px 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.qm-fact-result-actions {
text-align: right;
margin-top: 6px;
}
.qm-fact-hint {
color: var(--text-muted, #888);
font-size: 12px;
font-style: italic;
padding: 8px 0;
}