AuthorBuddy/Components/Pages/QuickMarkdown.razor
2026-08-30 08:11:01 +02:00

734 lines
25 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@page "/quick-markdown"
@attribute [Authorize(Roles = "Administrator,Blogger")]
@inject IOllamaService OllamaService
@inject OllamaSettings Settings
@inject IJSRuntime JS
@inject IAuthService AuthService
@inject ISettingsService SettingsService
@inject IQuickMarkdownService QuickMarkdownService
@inject IPluginManager PluginManager
@inject NavigationManager Navigation
@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>
<div class="qm-layout">
<div class="qm-input-panel">
<div class="form-group">
<label>Prompt-Verwaltung</label>
<div class="qm-prompt-selector">
<select class="qm-prompt-select" value="@_selectedPromptId" @onchange="SwitchPrompt">
<option value="0">— Standard-Prompt —</option>
@foreach (var p in _savedPrompts)
{
<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>
@if (_showPromptNameInput)
{
<div class="qm-prompt-name-row">
<input @bind="_newPromptName" placeholder="Name für den Prompt…" />
<button class="btn btn-sm btn-primary" @onclick="ConfirmSavePrompt">Speichern</button>
<button class="btn btn-sm" @onclick="CancelSavePrompt">Abbrechen</button>
</div>
}
</div>
<div class="form-group">
<label>System-Prompt</label>
<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" @bind:event="oninput" @bind:after="PersistPromptDraft" />
<div class="qm-range-labels">
<span>präzise (0)</span>
<span>kreativ (2)</span>
</div>
</div>
<div class="form-group">
<label>Text</label>
<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">
<button class="btn btn-primary" @onclick="Generate" disabled="@_generateDisabled">
@if (_isBusy)
{
<span>Generiere…</span>
}
else
{
<span>Markdown erzeugen</span>
}
</button>
<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>
<div class="qm-output-panel">
<div class="form-group">
<label>Ausgabe</label>
<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>
</div>
@if (_savedEntries.Count > 0)
{
<div class="qm-saved-list">
<h4>Gespeicherte Texte</h4>
@foreach (var e in _savedEntries)
{
<div class="qm-saved-item" @onclick="() => LoadEntry(e.Id)">
<div class="qm-saved-meta">
<span class="qm-saved-date">@e.Timestamp.ToLocalTime().ToString("g")</span>
<span class="qm-saved-preview">@(e.InputText.Length > 60 ? e.InputText[..60] + "…" : e.InputText)</span>
</div>
<button class="btn btn-sm" @onclick="() => DeleteEntry(e.Id)" title="Löschen">🗑</button>
</div>
}
</div>
}
@if (!string.IsNullOrEmpty(_error))
{
<div class="auth-error">@_error</div>
}
@if (!string.IsNullOrEmpty(_modelNotice))
{
<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;
private string _error = string.Empty;
private string _modelNotice = string.Empty;
private bool _isBusy;
private bool _generateDisabled => _isBusy || string.IsNullOrWhiteSpace(_inputText);
private bool _saveEntryDisabled => string.IsNullOrWhiteSpace(_inputText) && string.IsNullOrWhiteSpace(_output);
private double _temperature = 0.3;
private int _userId;
private List<QuickMarkdownEntry> _savedEntries = new();
private List<QuickMarkdownPrompt> _savedPrompts = new();
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()
{
var user = await AuthService.GetCurrentUserAsync();
if (user == null || !user.HasAnyRole(UserRole.Administrator, UserRole.Blogger))
{
Navigation.NavigateTo("/");
return;
}
_userId = user.Id;
_savedPrompts = await QuickMarkdownService.GetPromptsAsync(_userId);
_systemPrompt = await SettingsService.GetQuickMarkdownPromptAsync()
?? 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");
_selectedPromptId = id;
if (id == 0)
{
_systemPrompt = Settings.GetQuickMarkdownDefaultPrompt(Settings.Language);
_temperature = Settings.QuickMarkdownTemp;
}
else
{
var prompt = _savedPrompts.FirstOrDefault(p => p.Id == id);
if (prompt != null)
{
_systemPrompt = prompt.PromptText;
_temperature = prompt.Temperature;
}
}
await PersistPromptDraft();
}
private void ShowSavePromptDialog()
{
_newPromptName = string.Empty;
_showPromptNameInput = true;
}
private async Task ConfirmSavePrompt()
{
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)
{
existing.PromptText = _systemPrompt;
existing.Temperature = _temperature;
existing.Timestamp = DateTime.UtcNow;
await QuickMarkdownService.SavePromptAsync(existing);
_selectedPromptId = existing.Id;
}
else
{
var prompt = new QuickMarkdownPrompt
{
UserId = _userId,
Name = _newPromptName,
PromptText = _systemPrompt,
Temperature = _temperature,
Timestamp = DateTime.UtcNow
};
await QuickMarkdownService.SavePromptAsync(prompt);
_selectedPromptId = prompt.Id;
}
}
_savedPrompts = await QuickMarkdownService.GetPromptsAsync(_userId);
_showPromptNameInput = false;
_newPromptName = string.Empty;
await PersistPromptDraft();
}
private void CancelSavePrompt()
{
_showPromptNameInput = false;
_newPromptName = string.Empty;
}
private void ToggleFullscreen()
{
_fullscreenMode = !_fullscreenMode;
}
private async Task DeleteCurrentPrompt()
{
if (_selectedPromptId == 0)
return;
await QuickMarkdownService.DeletePromptAsync(_selectedPromptId);
_savedPrompts = await QuickMarkdownService.GetPromptsAsync(_userId);
_selectedPromptId = 0;
_systemPrompt = Settings.GetQuickMarkdownDefaultPrompt(Settings.Language);
_temperature = Settings.QuickMarkdownTemp;
await PersistPromptDraft();
}
private async Task SaveEntry()
{
if (string.IsNullOrWhiteSpace(_inputText) && string.IsNullOrWhiteSpace(_output))
return;
var entry = new QuickMarkdownEntry
{
UserId = _userId,
InputText = _inputText,
OutputText = _output,
SystemPrompt = _systemPrompt,
Temperature = _temperature,
Timestamp = DateTime.UtcNow
};
await QuickMarkdownService.SaveAsync(entry);
_savedEntries = await QuickMarkdownService.GetAllAsync(_userId);
}
private async Task LoadEntry(int id)
{
var entry = await QuickMarkdownService.GetByIdAsync(id);
if (entry == null) return;
_inputText = entry.InputText;
_output = entry.OutputText;
if (!string.IsNullOrEmpty(entry.SystemPrompt))
{
_systemPrompt = entry.SystemPrompt;
_temperature = entry.Temperature;
}
await PersistDraftAsync();
}
private async Task DeleteEntry(int id)
{
await QuickMarkdownService.DeleteAsync(id);
_savedEntries = await QuickMarkdownService.GetAllAsync(_userId);
}
private async Task Generate()
{
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, Settings.StyleBackend);
_output = result;
}
catch (Exception ex)
{
_error = $"Fehler bei der Generierung: {ex.Message}";
}
finally
{
_isBusy = false;
}
}
private async Task CopyOutput()
{
if (string.IsNullOrEmpty(_output)) return;
await JS.InvokeVoidAsync("navigator.clipboard.writeText", _output);
}
private void ClearOutput()
{
_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;
}
}
}