2026-05-20 06:12:54 +02:00
|
|
|
@page "/assistant"
|
2026-05-21 11:04:42 +02:00
|
|
|
@attribute [Authorize]
|
2026-05-20 06:12:54 +02:00
|
|
|
@inject IOllamaService OllamaService
|
|
|
|
|
@inject ISettingsService SettingsService
|
2026-05-31 19:31:27 +02:00
|
|
|
@inject IRagService RagService
|
|
|
|
|
@inject IProjectService ProjectService
|
2026-05-20 06:12:54 +02:00
|
|
|
@inject NavigationManager Navigation
|
2026-05-25 06:38:16 +02:00
|
|
|
@inject ILocalizationService Loc
|
2026-05-31 19:31:27 +02:00
|
|
|
@inject OperationState OpState
|
2026-05-20 06:12:54 +02:00
|
|
|
|
2026-05-25 06:38:16 +02:00
|
|
|
<PageTitle>@Loc["page.title.assistant"]</PageTitle>
|
2026-05-20 06:12:54 +02:00
|
|
|
|
|
|
|
|
<div class="assistant-page">
|
|
|
|
|
<div class="assistant-toolbar">
|
2026-05-31 19:31:27 +02:00
|
|
|
<select value="@selectedMode" @onchange="OnModeChanged">
|
2026-05-25 06:38:16 +02:00
|
|
|
<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>
|
2026-05-31 19:31:27 +02:00
|
|
|
<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>
|
|
|
|
|
}
|
2026-05-20 06:12:54 +02:00
|
|
|
</select>
|
2026-05-25 06:38:16 +02:00
|
|
|
<button @onclick="RunAssistant" disabled="@(() => isRunning || string.IsNullOrWhiteSpace(inputText))">@Loc["assistant.button.execute"]</button>
|
2026-05-20 07:27:41 +02:00
|
|
|
@if (!string.IsNullOrEmpty(outputText))
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
<button @onclick="SendToEditor" class="primary">@Loc["assistant.button.send_to_editor"]</button>
|
2026-05-20 07:27:41 +02:00
|
|
|
}
|
2026-05-31 19:31:27 +02:00
|
|
|
<button @onclick='() => showPromptEditor = !showPromptEditor' class="btn-small">
|
|
|
|
|
@(showPromptEditor ? Loc["assistant.button.hide_prompt"] : Loc["assistant.button.edit_prompt"])
|
|
|
|
|
</button>
|
2026-05-20 06:12:54 +02:00
|
|
|
<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>
|
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
@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>
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 06:12:54 +02:00
|
|
|
<div class="assistant-panels">
|
|
|
|
|
<div class="assistant-input-panel">
|
2026-05-31 19:31:27 +02:00
|
|
|
<h4 style="display:inline">@Loc["assistant.label.input_text"]</h4>
|
|
|
|
|
<CopyCreateToolbar Text="@inputText" DefaultFileName="assistent_eingabe" />
|
2026-05-25 06:38:16 +02:00
|
|
|
<textarea class="assistant-textarea" @bind="inputText" placeholder="@Loc["assistant.placeholder.input"]"></textarea>
|
2026-05-20 06:12:54 +02:00
|
|
|
</div>
|
|
|
|
|
<div class="assistant-output-panel">
|
2026-05-31 19:31:27 +02:00
|
|
|
<h4 style="display:inline">@Loc["assistant.label.result"]</h4>
|
|
|
|
|
<CopyCreateToolbar Text="@outputText" DefaultFileName="assistent_ausgabe" />
|
2026-05-20 06:12:54 +02:00
|
|
|
<div class="assistant-output">
|
|
|
|
|
@if (!string.IsNullOrEmpty(outputText))
|
|
|
|
|
{
|
|
|
|
|
<pre>@outputText</pre>
|
|
|
|
|
}
|
|
|
|
|
else if (!string.IsNullOrEmpty(errorMessage))
|
|
|
|
|
{
|
|
|
|
|
<div class="assistant-error">@errorMessage</div>
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
<span class="assistant-hint">@Loc["assistant.hint.empty_result"]</span>
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@code {
|
2026-05-20 07:27:41 +02:00
|
|
|
private string selectedMode = "lektorat";
|
2026-05-20 06:12:54 +02:00
|
|
|
private string inputText = string.Empty;
|
|
|
|
|
private string outputText = string.Empty;
|
|
|
|
|
private string errorMessage = string.Empty;
|
2026-05-20 07:27:41 +02:00
|
|
|
private string statusText = string.Empty;
|
2026-05-20 06:12:54 +02:00
|
|
|
private bool isRunning;
|
|
|
|
|
private double progress;
|
2026-05-31 19:31:27 +02:00
|
|
|
private bool showPromptEditor;
|
|
|
|
|
private string promptOverride = string.Empty;
|
|
|
|
|
private double temperatureOverride = 0.7;
|
|
|
|
|
private ProjectTemplate currentTemplate = ProjectTemplate.Novel;
|
2026-05-20 06:12:54 +02:00
|
|
|
|
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
statusText = Loc["common.status.ready"];
|
2026-05-31 19:31:27 +02:00
|
|
|
await LoadPromptForMode();
|
|
|
|
|
|
|
|
|
|
var pid = SettingsService.CurrentProjectId;
|
|
|
|
|
if (pid != null)
|
|
|
|
|
{
|
|
|
|
|
var project = await ProjectService.GetProjectAsync(pid.Value);
|
|
|
|
|
if (project != null)
|
|
|
|
|
currentTemplate = project.Template;
|
|
|
|
|
}
|
2026-05-20 07:27:41 +02:00
|
|
|
|
|
|
|
|
var pending = SettingsService.PendingAssistantInput;
|
|
|
|
|
if (!string.IsNullOrEmpty(pending))
|
|
|
|
|
{
|
|
|
|
|
inputText = pending;
|
|
|
|
|
SettingsService.PendingAssistantInput = null;
|
|
|
|
|
}
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
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);
|
|
|
|
|
|
2026-05-20 06:12:54 +02:00
|
|
|
private async Task RunAssistant()
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(inputText)) return;
|
|
|
|
|
|
|
|
|
|
isRunning = true;
|
|
|
|
|
progress = 50;
|
|
|
|
|
outputText = string.Empty;
|
|
|
|
|
errorMessage = string.Empty;
|
2026-05-31 19:31:27 +02:00
|
|
|
statusText = Loc["assistant.status.starting"];
|
|
|
|
|
OpState.SetStatus(Loc["assistant.status.starting"], true);
|
|
|
|
|
OpState.SetThought("");
|
2026-05-20 06:12:54 +02:00
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
if (selectedMode == "rag")
|
2026-05-20 06:12:54 +02:00
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
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);
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
2026-05-31 19:31:27 +02:00
|
|
|
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"];
|
2026-05-20 06:12:54 +02:00
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
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);
|
|
|
|
|
}
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
errorMessage = $"{Loc["common.error"]} {ex.Message}";
|
|
|
|
|
statusText = Loc["common.status.error"];
|
2026-05-31 19:31:27 +02:00
|
|
|
OpState.SetStatus($"{Loc["common.error"]} {ex.Message}", false);
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
isRunning = false;
|
|
|
|
|
progress = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-20 07:27:41 +02:00
|
|
|
|
|
|
|
|
private void SendToEditor()
|
|
|
|
|
{
|
|
|
|
|
SettingsService.PendingAssistantOutput = outputText;
|
|
|
|
|
Navigation.NavigateTo("/editor");
|
|
|
|
|
}
|
2026-05-20 06:12:54 +02:00
|
|
|
}
|