AuthorBuddy/Components/Pages/OllamaAssistant.razor
2026-05-31 19:31:27 +02:00

280 lines
12 KiB
Text

@page "/assistant"
@attribute [Authorize]
@inject IOllamaService OllamaService
@inject ISettingsService SettingsService
@inject IRagService RagService
@inject IProjectService ProjectService
@inject NavigationManager Navigation
@inject ILocalizationService Loc
@inject OperationState OpState
<PageTitle>@Loc["page.title.assistant"]</PageTitle>
<div class="assistant-page">
<div class="assistant-toolbar">
<select value="@selectedMode" @onchange="OnModeChanged">
<option value="lektorat">@Loc["assistant.mode.lektorat"]</option>
<option value="advocatus">@Loc["assistant.mode.advocatus"]</option>
<option value="hookfinder">@Loc["assistant.mode.hookfinder"]</option>
<option value="eli5">@Loc["assistant.mode.eli5"]</option>
<option value="coauthor">@Loc["assistant.mode.coauthor"]</option>
<option value="rag">@Loc["assistant.mode.rag"]</option>
@if (currentTemplate == ProjectTemplate.Technical)
{
<option disabled style="font-style:italic;">── Requirements ──</option>
<option value="tech_lastenheft">@Loc["assistant.mode.tech_lastenheft"]</option>
<option value="tech_pflichtenheft">@Loc["assistant.mode.tech_pflichtenheft"]</option>
<option value="tech_usecase">@Loc["assistant.mode.tech_usecase"]</option>
<option value="tech_stakeholder">@Loc["assistant.mode.tech_stakeholder"]</option>
<option value="tech_glossar">@Loc["assistant.mode.tech_glossar"]</option>
<option value="tech_qualitaet">@Loc["assistant.mode.tech_qualitaet"]</option>
<option value="tech_risiko">@Loc["assistant.mode.tech_risiko"]</option>
}
</select>
<button @onclick="RunAssistant" disabled="@(() => isRunning || string.IsNullOrWhiteSpace(inputText))">@Loc["assistant.button.execute"]</button>
@if (!string.IsNullOrEmpty(outputText))
{
<button @onclick="SendToEditor" class="primary">@Loc["assistant.button.send_to_editor"]</button>
}
<button @onclick='() => showPromptEditor = !showPromptEditor' class="btn-small">
@(showPromptEditor ? Loc["assistant.button.hide_prompt"] : Loc["assistant.button.edit_prompt"])
</button>
<span style="flex:1;font-size:12px;color:var(--border-color);text-align:right;">@statusText</span>
@if (isRunning)
{
<progress value="@progress" max="100"></progress>
}
</div>
@if (showPromptEditor && selectedMode != "rag")
{
<div class="assistant-prompt-editor">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
<strong style="font-size:13px;">@Loc["assistant.label.system_prompt"]</strong>
<button class="btn-small" @onclick="ResetPrompt">@Loc["assistant.button.reset_prompt"]</button>
</div>
<textarea class="assistant-prompt-textarea" @bind="promptOverride" rows="4"></textarea>
<div class="temp-slider" style="margin-top:6px;">
<label class="temp-label">@Loc["settings.temperature.label"] <span class="temp-value">@temperatureOverride.ToString("F1")</span></label>
<input type="range" min="0" max="2" step="0.1" value="@Fmt(temperatureOverride)" @oninput="OnTempChanged" />
</div>
</div>
}
<div class="assistant-panels">
<div class="assistant-input-panel">
<h4 style="display:inline">@Loc["assistant.label.input_text"]</h4>
<CopyCreateToolbar Text="@inputText" DefaultFileName="assistent_eingabe" />
<textarea class="assistant-textarea" @bind="inputText" placeholder="@Loc["assistant.placeholder.input"]"></textarea>
</div>
<div class="assistant-output-panel">
<h4 style="display:inline">@Loc["assistant.label.result"]</h4>
<CopyCreateToolbar Text="@outputText" DefaultFileName="assistent_ausgabe" />
<div class="assistant-output">
@if (!string.IsNullOrEmpty(outputText))
{
<pre>@outputText</pre>
}
else if (!string.IsNullOrEmpty(errorMessage))
{
<div class="assistant-error">@errorMessage</div>
}
else
{
<span class="assistant-hint">@Loc["assistant.hint.empty_result"]</span>
}
</div>
</div>
</div>
</div>
@code {
private string selectedMode = "lektorat";
private string inputText = string.Empty;
private string outputText = string.Empty;
private string errorMessage = string.Empty;
private string statusText = string.Empty;
private bool isRunning;
private double progress;
private bool showPromptEditor;
private string promptOverride = string.Empty;
private double temperatureOverride = 0.7;
private ProjectTemplate currentTemplate = ProjectTemplate.Novel;
protected override async Task OnInitializedAsync()
{
statusText = Loc["common.status.ready"];
await LoadPromptForMode();
var pid = SettingsService.CurrentProjectId;
if (pid != null)
{
var project = await ProjectService.GetProjectAsync(pid.Value);
if (project != null)
currentTemplate = project.Template;
}
var pending = SettingsService.PendingAssistantInput;
if (!string.IsNullOrEmpty(pending))
{
inputText = pending;
SettingsService.PendingAssistantInput = null;
}
}
private async Task OnModeChanged(ChangeEventArgs e)
{
selectedMode = e.Value?.ToString() ?? "lektorat";
await LoadPromptForMode();
}
private async Task LoadPromptForMode()
{
if (selectedMode == "rag")
{
promptOverride = string.Empty;
temperatureOverride = 0.3;
return;
}
var (prompt, temp) = selectedMode switch
{
"lektorat" => (await SettingsService.GetLektorSystemPromptAsync() ?? string.Empty, await SettingsService.GetLektorTempAsync()),
"advocatus" => (await SettingsService.GetAdvocatusDiaboliPromptAsync() ?? string.Empty, await SettingsService.GetAdvocatusDiaboliTempAsync()),
"hookfinder" => (await SettingsService.GetHookFinderPromptAsync() ?? string.Empty, await SettingsService.GetHookFinderTempAsync()),
"eli5" => (await SettingsService.GetEli5PromptAsync() ?? string.Empty, await SettingsService.GetEli5TempAsync()),
"coauthor" => (await SettingsService.GetCoAuthorBrainstormingPromptAsync() ?? string.Empty, await SettingsService.GetCoAuthorTempAsync()),
"tech_lastenheft" => (await SettingsService.GetTechLastenheftPromptAsync() ?? string.Empty, await SettingsService.GetTechLastenheftTempAsync()),
"tech_pflichtenheft" => (await SettingsService.GetTechPflichtenheftPromptAsync() ?? string.Empty, await SettingsService.GetTechPflichtenheftTempAsync()),
"tech_usecase" => (await SettingsService.GetTechUseCasePromptAsync() ?? string.Empty, await SettingsService.GetTechUseCaseTempAsync()),
"tech_stakeholder" => (await SettingsService.GetTechStakeholderPromptAsync() ?? string.Empty, await SettingsService.GetTechStakeholderTempAsync()),
"tech_glossar" => (await SettingsService.GetTechGlossarPromptAsync() ?? string.Empty, await SettingsService.GetTechGlossarTempAsync()),
"tech_qualitaet" => (await SettingsService.GetTechQualitaetPromptAsync() ?? string.Empty, await SettingsService.GetTechQualitaetTempAsync()),
"tech_risiko" => (await SettingsService.GetTechRisikoPromptAsync() ?? string.Empty, await SettingsService.GetTechRisikoTempAsync()),
_ => (string.Empty, 0.0)
};
promptOverride = prompt;
temperatureOverride = temp;
}
private async Task ResetPrompt()
{
await LoadPromptForMode();
}
private void OnTempChanged(ChangeEventArgs e)
{
if (e.Value is string s && double.TryParse(s, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var v))
temperatureOverride = Math.Round(v, 1);
}
private string Fmt(double v) => v.ToString("F1", System.Globalization.CultureInfo.InvariantCulture);
private async Task RunAssistant()
{
if (string.IsNullOrWhiteSpace(inputText)) return;
isRunning = true;
progress = 50;
outputText = string.Empty;
errorMessage = string.Empty;
statusText = Loc["assistant.status.starting"];
OpState.SetStatus(Loc["assistant.status.starting"], true);
OpState.SetThought("");
try
{
if (selectedMode == "rag")
{
var projectId = SettingsService.CurrentProjectId;
if (projectId == null)
{
errorMessage = Loc["assistant.error.no_project"];
return;
}
OpState.SetStatus(Loc["assistant.status.searching"], true);
progress = 30;
var results = await RagService.SearchAsync(projectId.Value, inputText, 5);
if (results.Count == 0)
{
errorMessage = Loc["assistant.error.no_facts"];
return;
}
var facts = string.Join("\n\n---\n\n", results.Select(r => r.Text));
progress = 70;
OpState.SetStatus(Loc["assistant.status.processing"], true);
outputText = await OllamaService.AnswerBasedOnFactsAsync(inputText, facts);
OpState.SetThought(outputText);
statusText = Loc["assistant.status.done"];
OpState.SetStatus(Loc["assistant.status.done"], false);
}
else
{
var systemPrompt = promptOverride;
if (string.IsNullOrEmpty(systemPrompt))
{
errorMessage = Loc["assistant.error.no_prompt"];
return;
}
if (selectedMode is "coauthor" or "hookfinder" or "lektorat")
{
var projectId = SettingsService.CurrentProjectId;
if (projectId != null)
{
var project = await ProjectService.GetProjectAsync(projectId.Value);
if (project != null)
{
// Use Exposé file from category system
var categories = await ProjectService.GetFileCategoriesAsync(projectId.Value);
var exposeFile = categories.FirstOrDefault(c => c.Value == FileCategory.Exposee);
if (!string.IsNullOrEmpty(exposeFile.Key) && File.Exists(exposeFile.Key))
{
var exposee = await File.ReadAllTextAsync(exposeFile.Key);
if (!string.IsNullOrWhiteSpace(exposee))
{
systemPrompt += "\n\n" + Loc["assistant.exposee.context"] + "\n" + exposee;
}
}
}
}
}
var model = await SettingsService.GetStyleModelAsync() ?? "llama3.2";
OpState.SetStatus(Loc["assistant.status.processing"], true);
statusText = Loc["assistant.status.processing"];
await OllamaService.ChatStream(model, inputText, async chunk =>
{
outputText += chunk;
OpState.AppendThought(chunk);
await InvokeAsync(StateHasChanged);
}, systemPrompt, temperatureOverride);
statusText = Loc["assistant.status.done"];
OpState.SetStatus(Loc["assistant.status.done"], false);
}
}
catch (Exception ex)
{
errorMessage = $"{Loc["common.error"]} {ex.Message}";
statusText = Loc["common.status.error"];
OpState.SetStatus($"{Loc["common.error"]} {ex.Message}", false);
}
finally
{
isRunning = false;
progress = 0;
}
}
private void SendToEditor()
{
SettingsService.PendingAssistantOutput = outputText;
Navigation.NavigateTo("/editor");
}
}