feat: implement AI-driven text streaming and dynamic knowledge graph generation in AiAssistantBubble
This commit is contained in:
@@ -1,36 +1,126 @@
|
||||
@using NexusReader.UI.Shared.Services
|
||||
@using NexusReader.Application.DTOs.AI
|
||||
@inject IQuizStateService QuizState
|
||||
@inject KnowledgeCoordinator Coordinator
|
||||
@implements IDisposable
|
||||
|
||||
<div class="ai-bubble-container">
|
||||
<div class="ai-bubble">
|
||||
<div class="ai-avatar">
|
||||
<div class="avatar-ring"></div>
|
||||
<NexusIcon Name="robot" Size="48" Class="neon-pulse" />
|
||||
<NexusIcon Name="robot" Size="48" Class="@(_isStreaming ? "neon-pulse" : "neon-glow")" />
|
||||
<div class="avatar-label">
|
||||
<span class="name">E-Czytnik</span>
|
||||
<span class="role">Asystent AI</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ai-content">
|
||||
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">@Dialogue</NexusTypography>
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<div class="loading-state">
|
||||
<div class="shimmer">Analizuję fragment...</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">
|
||||
@_displayedText@(_isStreaming ? "▍" : "")
|
||||
</NexusTypography>
|
||||
}
|
||||
|
||||
<div class="ai-actions">
|
||||
<button class="action-btn ghost" @onclick='() => HandleActionClick("more")'>Pokaż więcej informacji</button>
|
||||
<button class="action-btn neon-border" @onclick='() => HandleActionClick("quiz")'>Rozwiąż quiz</button>
|
||||
<button class="action-btn neon-border" @onclick='() => HandleActionClick("quiz")' disabled="@(_isLoading)">Rozwiąż quiz</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="bubble-pointer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@code {
|
||||
[Parameter] public string ContextBlockId { get; set; } = string.Empty;
|
||||
/// <summary>Fallback static dialogue shown when no live AI content is available.</summary>
|
||||
[Parameter] public string Dialogue { get; set; } = string.Empty;
|
||||
[Parameter] public List<string> Actions { get; set; } = new();
|
||||
[Parameter] public EventCallback<string> 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;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
// Only re-fetch when the block context actually changes
|
||||
if (string.IsNullOrEmpty(ContextBlockId) || ContextBlockId == _lastFetchedBlockId)
|
||||
return;
|
||||
|
||||
_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
|
||||
{
|
||||
_packet = await Coordinator.RequestSummaryAndQuizAsync(
|
||||
$"[ID: {ContextBlockId}]\n{Dialogue}");
|
||||
|
||||
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))
|
||||
@@ -43,4 +133,10 @@
|
||||
await OnActionTriggered.InvokeAsync(action);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_streamCts?.Cancel();
|
||||
_streamCts?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user