@using NexusReader.UI.Shared.Services @using NexusReader.Application.DTOs.AI @inject IQuizStateService QuizState @inject KnowledgeCoordinator Coordinator @implements IDisposable
E-Czytnik Asystent AI
@if (_isLoading) {
Analizuję fragment...
} else { @_displayedText@(_isStreaming ? "▍" : "") }
@code { [Parameter] public string ContextBlockId { get; set; } = string.Empty; /// Fallback static dialogue shown when no live AI content is available. [Parameter] public string Dialogue { get; set; } = string.Empty; [Parameter] public List Actions { get; set; } = new(); [Parameter] public string FullPageContent { get; set; } = string.Empty; [Parameter] public EventCallback OnActionTriggered { get; set; } private string _displayedText = string.Empty; private bool _isLoading = false; private bool _isStreaming = false; private string _lastFetchedBlockId = string.Empty; private KnowledgePacket? _packet; private CancellationTokenSource? _streamCts; private bool _isInteractive; protected override async Task OnParametersSetAsync() { if (!_isInteractive) return; // Only re-fetch when the block context actually changes if (string.IsNullOrEmpty(ContextBlockId) || ContextBlockId == _lastFetchedBlockId) return; _lastFetchedBlockId = ContextBlockId; await FetchAndStreamAsync(); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { _isInteractive = true; if (!string.IsNullOrEmpty(ContextBlockId)) { _lastFetchedBlockId = ContextBlockId; await FetchAndStreamAsync(); } } } private async Task FetchAndStreamAsync() { // Cancel any in-progress stream _streamCts?.Cancel(); _streamCts = new CancellationTokenSource(); var token = _streamCts.Token; _isLoading = true; _isStreaming = false; _displayedText = string.Empty; _packet = null; StateHasChanged(); try { var contentToAnalyze = !string.IsNullOrWhiteSpace(FullPageContent) ? FullPageContent : $"[ID: {ContextBlockId}]\n{Dialogue}"; var result = await Coordinator.RequestSummaryAndQuizAsync(contentToAnalyze); _packet = result.IsSuccess ? result.Value : null; var summary = _packet?.Summary; if (string.IsNullOrWhiteSpace(summary)) { // Fall back to the static Dialogue parameter _displayedText = string.IsNullOrEmpty(Dialogue) ? "Brak danych do analizy." : Dialogue; _isLoading = false; StateHasChanged(); return; } _isLoading = false; _isStreaming = true; // Word-by-word reveal (streaming simulation) var words = summary.Split(' '); foreach (var word in words) { if (token.IsCancellationRequested) break; _displayedText += (string.IsNullOrEmpty(_displayedText) ? "" : " ") + word; StateHasChanged(); await Task.Delay(40, token); // ~25 words/sec } } catch (OperationCanceledException) { // Superseded by a newer block — silently drop } catch (Exception ex) { _displayedText = string.IsNullOrEmpty(Dialogue) ? "Błąd analizy." : Dialogue; Console.WriteLine($"[AiAssistantBubble] Error fetching summary: {ex.Message}"); } finally { _isStreaming = false; StateHasChanged(); } } private async Task HandleActionClick(string action) { if (action.Contains("quiz", StringComparison.OrdinalIgnoreCase)) { await QuizState.RequestQuiz(ContextBlockId); } if (OnActionTriggered.HasDelegate) { await OnActionTriggered.InvokeAsync(action); } } public void Dispose() { _streamCts?.Cancel(); _streamCts?.Dispose(); } }