diff --git a/src/NexusReader.Application/Abstractions/Services/IKnowledgeService.cs b/src/NexusReader.Application/Abstractions/Services/IKnowledgeService.cs index f3d1100..b6f603f 100644 --- a/src/NexusReader.Application/Abstractions/Services/IKnowledgeService.cs +++ b/src/NexusReader.Application/Abstractions/Services/IKnowledgeService.cs @@ -6,5 +6,7 @@ namespace NexusReader.Application.Abstractions.Services; public interface IKnowledgeService { Task> GetKnowledgeAsync(string text, CancellationToken cancellationToken = default); + Task> GetGraphDataAsync(string text, CancellationToken cancellationToken = default); + Task> GetSummaryAndQuizAsync(string text, CancellationToken cancellationToken = default); Task ClearCacheAsync(CancellationToken cancellationToken = default); } diff --git a/src/NexusReader.Application/DTOs/AI/KnowledgePacket.cs b/src/NexusReader.Application/DTOs/AI/KnowledgePacket.cs index a6a1c23..d0b1f8b 100644 --- a/src/NexusReader.Application/DTOs/AI/KnowledgePacket.cs +++ b/src/NexusReader.Application/DTOs/AI/KnowledgePacket.cs @@ -18,4 +18,5 @@ public record KnowledgePacket [JsonPropertyName("concepts")] public List Concepts { get; init; } = new(); [JsonPropertyName("quizzes")] public List Quizzes { get; init; } = new(); [JsonPropertyName("graph")] public NexusReader.Application.Queries.Graph.GraphDataDto? Graph { get; init; } + [JsonPropertyName("summary")] public string? Summary { get; init; } } diff --git a/src/NexusReader.Infrastructure/Helpers/JsonRepairHelper.cs b/src/NexusReader.Infrastructure/Helpers/JsonRepairHelper.cs new file mode 100644 index 0000000..7e4b3d0 --- /dev/null +++ b/src/NexusReader.Infrastructure/Helpers/JsonRepairHelper.cs @@ -0,0 +1,69 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace NexusReader.Infrastructure.Helpers; + +public static class JsonRepairHelper +{ + public static string Repair(string json) + { + if (string.IsNullOrWhiteSpace(json)) return json; + json = json.Trim(); + + // 1. If it doesn't end with } or ], it's definitely truncated + if (!json.EndsWith("}") && !json.EndsWith("]")) + { + // Try to find the last "clean" closing point before the truncation + // We look for a comma, a closing brace, or a closing bracket that is followed by noise + int lastGoodComma = json.LastIndexOf(','); + int lastGoodBrace = json.LastIndexOf('}'); + int lastGoodBracket = json.LastIndexOf(']'); + + int cutoff = Math.Max(lastGoodComma, Math.Max(lastGoodBrace, lastGoodBracket)); + + if (cutoff > 0) + { + // Prune the "garbage" at the end + json = json.Substring(0, cutoff); + } + + // Now apply the standard stack-based closing logic + var stack = new Stack(); + bool inString = false; + bool escaped = false; + + foreach (char c in json) + { + if (escaped) { escaped = false; continue; } + if (c == '\\') { escaped = true; continue; } + if (c == '"') { inString = !inString; continue; } + if (inString) continue; + + if (c == '{' || c == '[') stack.Push(c); + else if (c == '}' || c == ']') + { + if (stack.Count > 0) + { + var last = stack.Peek(); + if ((c == '}' && last == '{') || (c == ']' && last == '[')) + stack.Pop(); + } + } + } + + var builder = new StringBuilder(json); + if (inString) builder.Append('"'); + + while (stack.Count > 0) + { + var c = stack.Pop(); + if (c == '{') builder.Append("}"); + else if (c == '[') builder.Append("]"); + } + + return builder.ToString(); + } + + return json; + } +} diff --git a/src/NexusReader.Infrastructure/Services/KnowledgeService.cs b/src/NexusReader.Infrastructure/Services/KnowledgeService.cs index 0643d39..ad74f13 100644 --- a/src/NexusReader.Infrastructure/Services/KnowledgeService.cs +++ b/src/NexusReader.Infrastructure/Services/KnowledgeService.cs @@ -35,29 +35,37 @@ public class KnowledgeService : IKnowledgeService } public async Task> GetKnowledgeAsync(string text, CancellationToken cancellationToken = default) + { + return await GetKnowledgeInternalAsync(text, PromptRegistry.KnowledgeExtractionSystemPrompt, "full", cancellationToken); + } + + public async Task> GetGraphDataAsync(string text, CancellationToken cancellationToken = default) + { + return await GetKnowledgeInternalAsync(text, PromptRegistry.GraphExtractionPrompt, "graph", cancellationToken); + } + + public async Task> GetSummaryAndQuizAsync(string text, CancellationToken cancellationToken = default) + { + return await GetKnowledgeInternalAsync(text, PromptRegistry.SummaryAndQuizPrompt, "summary_quiz", cancellationToken); + } + + private async Task> GetKnowledgeInternalAsync(string text, string systemPrompt, string cacheSuffix, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(text)) { return Result.Fail("Input text is empty."); } - Console.WriteLine($"[KnowledgeService] Starting extraction for text: {text.Substring(0, Math.Min(text.Length, 50))}..."); + Console.WriteLine($"[KnowledgeService] Starting extraction ({cacheSuffix}) for text sample: {text.Substring(0, Math.Min(text.Length, 50))}..."); - // Normalize text to ensure consistent hashing and reduce token noise var normalizedText = ContentHasher.Normalize(text); - - // Phase 4: Request Pre-processing (Token Saving) if (normalizedText.Length > _settings.MaxInputLength) { - Console.WriteLine($"[KnowledgeService] Error: Input too long ({normalizedText.Length} > {_settings.MaxInputLength})"); - return Result.Fail($"Input text is too long ({normalizedText.Length} characters after normalization). Max allowed is {_settings.MaxInputLength}."); + normalizedText = normalizedText.Substring(0, _settings.MaxInputLength); + Console.WriteLine($"[KnowledgeService] WARNING: Input text truncated to {_settings.MaxInputLength} chars."); } - // Simple token estimation (4 chars per token) - var estimatedTokens = normalizedText.Length / 4; - Console.WriteLine($"[KnowledgeService] Processing request with ~{estimatedTokens} tokens."); - - var hash = ContentHasher.ComputeHash(normalizedText); + var hash = ContentHasher.ComputeHash(normalizedText) + "_" + cacheSuffix; // 1. Check Cache var cached = await _dbContext.SemanticKnowledgeCache @@ -65,28 +73,19 @@ public class KnowledgeService : IKnowledgeService if (cached != null) { - Console.WriteLine($"[KnowledgeService] Cache hit for hash: {hash}"); try { - var packet = JsonSerializer.Deserialize(cached.JsonData); - if (packet != null) - { - return Result.Ok(packet); - } - } - catch (JsonException ex) - { - Console.WriteLine($"[KnowledgeService] Cache deserialization error: {ex.Message}"); + var packet = JsonSerializer.Deserialize(cached.JsonData, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + if (packet != null) return Result.Ok(packet); } + catch { } } // 2. Call AI Client try { - Console.WriteLine($"[KnowledgeService] Calling Gemini AI with Model: {_settings.Model}..."); var options = new ChatOptions { - // ResponseFormat = ChatResponseFormat.Json, // Disabled due to GeminiMappingException in current library version Temperature = (float)_settings.Temperature, MaxOutputTokens = _settings.MaxOutputTokens }; @@ -94,61 +93,46 @@ public class KnowledgeService : IKnowledgeService var response = await _retryPipeline.ExecuteAsync(async ct => await _chatClient.GetResponseAsync(new List { - new ChatMessage(ChatRole.System, PromptRegistry.KnowledgeExtractionSystemPrompt), + new ChatMessage(ChatRole.System, systemPrompt), new ChatMessage(ChatRole.User, normalizedText) }, options, cancellationToken: ct), cancellationToken); - var jsonResponse = response.Text; - if (string.IsNullOrWhiteSpace(jsonResponse)) + var rawResponse = response.Text?.Trim() ?? string.Empty; + if (string.IsNullOrWhiteSpace(rawResponse)) return Result.Fail("AI returned an empty response."); + + // Cleanup markdown code blocks and repair truncation + var jsonResponse = rawResponse.Replace("```json", "").Replace("```", "").Trim(); + jsonResponse = JsonRepairHelper.Repair(jsonResponse); + + try { - Console.WriteLine("[KnowledgeService] AI returned empty response."); - return Result.Fail("AI returned an empty response."); + var knowledgePacket = JsonSerializer.Deserialize(jsonResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + if (knowledgePacket == null) return Result.Fail("Failed to deserialize AI response."); + + // 3. Save to Cache + var cacheEntry = new SemanticKnowledgeCache + { + ContentHash = hash, + JsonData = jsonResponse, + ModelId = _settings.Model, + PromptVersion = PromptVersion, + CreatedAt = DateTime.UtcNow + }; + + if (cached == null) _dbContext.SemanticKnowledgeCache.Add(cacheEntry); + else { cached.JsonData = jsonResponse; cached.CreatedAt = DateTime.UtcNow; } + + await _dbContext.SaveChangesAsync(cancellationToken); + return Result.Ok(knowledgePacket); } - - Console.WriteLine($"[KnowledgeService] AI Response received ({jsonResponse.Length} chars)."); - - // Cleanup potential markdown if Gemini still adds it despite options - jsonResponse = jsonResponse.Replace("```json", "").Replace("```", "").Trim(); - - var knowledgePacket = JsonSerializer.Deserialize(jsonResponse); - if (knowledgePacket == null) + catch (JsonException ex) { - Console.WriteLine("[KnowledgeService] Failed to deserialize JSON response."); - return Result.Fail("Failed to deserialize AI response."); + Console.WriteLine($"[KnowledgeService] JSON Error: {ex.Message}. Raw length: {rawResponse.Length}"); + return Result.Fail($"Failed to deserialize AI response: {ex.Message}"); } - - // 3. Save to Cache - Console.WriteLine("[KnowledgeService] Saving result to cache..."); - var cacheEntry = new SemanticKnowledgeCache - { - ContentHash = hash, - JsonData = jsonResponse, - ModelId = _settings.Model, - PromptVersion = PromptVersion, - CreatedAt = DateTime.UtcNow - }; - - if (cached == null) - { - _dbContext.SemanticKnowledgeCache.Add(cacheEntry); - } - else - { - cached.JsonData = jsonResponse; - cached.CreatedAt = DateTime.UtcNow; - } - - await _dbContext.SaveChangesAsync(cancellationToken); - Console.WriteLine("[KnowledgeService] Extraction successful."); - - return Result.Ok(knowledgePacket); } catch (Exception ex) { - Console.WriteLine($"[KnowledgeService] CRITICAL ERROR: {ex.GetType().Name}: {ex.Message}"); - if (ex.InnerException != null) - Console.WriteLine($"[KnowledgeService] Inner Error: {ex.InnerException.Message}"); - return Result.Fail(new Error("Failed to extract knowledge from AI").CausedBy(ex)); } } @@ -160,12 +144,10 @@ public class KnowledgeService : IKnowledgeService Console.WriteLine("[KnowledgeService] Clearing SemanticKnowledgeCache..."); _dbContext.SemanticKnowledgeCache.RemoveRange(_dbContext.SemanticKnowledgeCache); await _dbContext.SaveChangesAsync(cancellationToken); - Console.WriteLine("[KnowledgeService] Cache cleared successfully."); return Result.Ok(); } catch (Exception ex) { - Console.WriteLine($"[KnowledgeService] Error clearing cache: {ex.Message}"); return Result.Fail($"Failed to clear cache: {ex.Message}"); } } diff --git a/src/NexusReader.Infrastructure/Services/PromptRegistry.cs b/src/NexusReader.Infrastructure/Services/PromptRegistry.cs index 08e1023..7b283ad 100644 --- a/src/NexusReader.Infrastructure/Services/PromptRegistry.cs +++ b/src/NexusReader.Infrastructure/Services/PromptRegistry.cs @@ -10,4 +10,15 @@ public static class PromptRegistry "\"quizzes\": [ { \"question\": \"string\", \"options\": [ \"string\" ], \"correct_index\": 0 } ], " + "\"graph\": { \"nodes\": [ { \"id\": \"string\", \"label\": \"string\", \"group\": \"concept\" } ], \"links\": [ { \"source\": \"string\", \"target\": \"string\", \"value\": 1 } ] } " + "}."; + + public const string GraphExtractionPrompt = + "You are an expert at information architecture. Extract key concepts and their relationships from the text to build a knowledge graph. " + + "CRITICAL: Each paragraph in the user text starts with [ID: some-id]. You MUST use these exact IDs as the 'id' for the nodes representing those blocks. " + + "Include a 'current' node representing the block content itself if applicable. " + + "CRITICAL: Limit the result to a MAXIMUM of 15 most relevant connections. " + + "Return ONLY minified JSON. Schema: { \"graph\": { \"nodes\": [ { \"id\": \"string\", \"label\": \"string\", \"group\": \"concept|current\" } ], \"links\": [ { \"source\": \"string\", \"target\": \"string\", \"value\": 1 } ] } }"; + + public const string SummaryAndQuizPrompt = + "You are an expert educator. Provide a concise summary of the text and generate a challenging quiz (3-5 questions). " + + "Return ONLY minified JSON. Schema: { \"summary\": \"string\", \"quizzes\": [ { \"question\": \"string\", \"options\": [ \"string\" ], \"correct_index\": 0 } ] }"; } diff --git a/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor b/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor new file mode 100644 index 0000000..b7f4513 --- /dev/null +++ b/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor @@ -0,0 +1,96 @@ +@using NexusReader.UI.Shared.Services +@using NexusReader.Application.DTOs.AI +@inject KnowledgeCoordinator Coordinator +@inject IReaderInteractionService InteractionService + +@if (IsVisible) +{ +
+
+
+
+ +
+ E-Czytnik + Asystent AI +
+
+
+ @if (IsLoading) + { +
+
Skanowanie fragmentu...
+
+ } + else if (Packet != null) + { +
+ @Packet.Summary +
+
+ + +
+ } + else + { +
+ Wykryto ciekawy fragment! Czy chcesz, abym wygenerował podsumowanie lub quiz z tego rozdziału? +
+
+ + +
+ } +
+
+
+
+} + +@code { + [Parameter] public string SelectedText { get; set; } = string.Empty; + [Parameter] public string BlockId { get; set; } = string.Empty; + [Parameter] public SelectionCoordinates? Coordinates { get; set; } + [Parameter] public string FullPageContent { get; set; } = string.Empty; + + private bool IsVisible => !string.IsNullOrEmpty(SelectedText) && Coordinates != null; + private bool IsLoading = false; + private KnowledgePacket? Packet; + private bool PositionBelow => Coordinates != null && Coordinates.Top < 320; + + protected override void OnParametersSet() + { + Console.WriteLine($"[SelectionAiPanel] Parameters set. SelectedText: {SelectedText.Length} chars, Coordinates: {Coordinates?.Top}, PositionBelow: {PositionBelow}"); + // Reset packet when selection changes + Packet = null; + } + + private string PanelStyle => Coordinates != null + ? string.Create(System.Globalization.CultureInfo.InvariantCulture, + $"top: {(PositionBelow ? Coordinates.Top + 35 : Coordinates.Top - 15):F1}px !important; " + + $"left: {Math.Clamp(Coordinates.Left + Coordinates.Width / 2, 280, 1600):F1}px !important; " + + $"transform: translate(-50%, {(PositionBelow ? "0" : "-100%")}) !important;") + : ""; + + private async Task RequestSummary() + { + IsLoading = true; + Packet = await Coordinator.RequestSummaryAndQuizAsync(SelectedText); + IsLoading = false; + } + + private async Task GenerateFullQuiz() + { + IsLoading = true; + await Coordinator.RequestSummaryAndQuizAsync(FullPageContent); + IsLoading = false; + Close(); + } + + private void Close() + { + Packet = null; + InteractionService.NotifyTextSelected(string.Empty, string.Empty, null!); + } +} diff --git a/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor.css b/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor.css new file mode 100644 index 0000000..f99e021 --- /dev/null +++ b/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor.css @@ -0,0 +1,158 @@ +.selection-ai-panel { + position: fixed; + z-index: 9999; + width: 550px; + max-width: 90vw; + animation: fadeInScale 0.2s ease-out; + pointer-events: auto; +} + +@keyframes fadeInScale { + from { opacity: 0; transform: translate(-50%, -90%) scale(0.95); } + to { opacity: 1; transform: translate(-50%, -100%) scale(1); } +} + +.ai-bubble { + position: relative; + display: flex; + flex-direction: row; + gap: 1.5rem; + padding: 1.5rem; + background: rgba(18, 18, 18, 0.95); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 16px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); + color: #fff; +} + +.ai-avatar { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + min-width: 100px; +} + +.avatar-label { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.avatar-label .name { + font-size: 0.8rem; + font-weight: 600; + color: #fff; +} + +.avatar-label .role { + font-size: 0.7rem; + opacity: 0.6; +} + +.neon-pulse { + color: #00ff99; + filter: drop-shadow(0 0 8px #00ff99); + animation: pulse 2s infinite ease-in-out; +} + +@keyframes pulse { + 0% { transform: scale(1); filter: drop-shadow(0 0 8px #00ff99); } + 50% { transform: scale(1.05); filter: drop-shadow(0 0 15px #00ff99); } + 100% { transform: scale(1); filter: drop-shadow(0 0 8px #00ff99); } +} + +.ai-content { + display: flex; + flex-direction: column; + gap: 1rem; + justify-content: center; +} + +.summary-box { + font-size: 0.95rem; + line-height: 1.5; + color: #e0e0e0; + max-height: 40vh; + overflow-y: auto; + padding-right: 8px; +} + +.summary-box::-webkit-scrollbar { + width: 4px; +} + +.summary-box::-webkit-scrollbar-thumb { + background: rgba(0, 255, 153, 0.3); + border-radius: 2px; +} + +.ai-actions { + display: flex; + gap: 1rem; +} + +.action-btn { + padding: 0.5rem 1.2rem; + border-radius: 20px; + font-size: 0.85rem; + cursor: pointer; + transition: all 0.2s ease; + font-family: inherit; +} + +.action-btn.ghost { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.2); + color: #aaa; +} + +.action-btn.neon-border { + background: rgba(0, 255, 153, 0.1); + border: 1px solid #00ff99; + color: #00ff99; +} + +.action-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 255, 153, 0.2); +} + +.bubble-pointer { + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 10px solid transparent; + border-right: 10px solid transparent; +} + +.selection-ai-panel:not(.below) .bubble-pointer { + bottom: -10px; + border-top: 10px solid rgba(18, 18, 18, 0.95); +} + +.selection-ai-panel.below .bubble-pointer { + top: -10px; + border-bottom: 10px solid rgba(18, 18, 18, 0.95); +} + +.loading-state { + padding: 1rem; +} + +.shimmer { + background: linear-gradient(90deg, transparent, rgba(0, 255, 153, 0.2), transparent); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + padding: 0.5rem; + border-radius: 4px; +} + +@keyframes shimmer { + from { background-position: 200% 0; } + to { background-position: -200% 0; } +} diff --git a/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor b/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor index 2fa79a7..a010a46 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor +++ b/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor @@ -7,13 +7,17 @@ @inject IJSRuntime JS @inject IFocusModeService FocusMode @inject IKnowledgeGraphService GraphService +@inject IReaderInteractionService InteractionService
- @if (GraphData == null) + @if (GraphService.IsLoading || GraphService.CurrentGraphData == null) {
- - Analyzing Chapter Nodes... +
+ +
+
+ Mapowanie relacji rozdziału...
} else @@ -31,7 +35,6 @@ [Parameter] public EventCallback OnNodeSelected { get; set; } private string ContainerId = "d3-graph-container"; - private GraphDataDto? GraphData; private IJSObjectReference? _module; private DotNetObjectReference? _dotNetHelper; @@ -40,12 +43,22 @@ FocusMode.OnFocusModeChanged += HandleFocusSimulation; GraphService.OnGraphUpdated += HandleGraphUpdate; GraphService.OnActiveNodeChanged += HandleActiveNodeChange; + GraphService.OnLoadingChanged += HandleLoadingChange; } private async void HandleGraphUpdate() { if (_module == null) return; - await _module.InvokeVoidAsync("updateData", GraphService.CurrentGraphData); + + if (GraphService.CurrentGraphData == null) + { + await _module.InvokeVoidAsync("clear"); + } + else + { + await _module.InvokeVoidAsync("updateData", GraphService.CurrentGraphData); + } + await InvokeAsync(StateHasChanged); } @@ -55,16 +68,20 @@ await _module.InvokeVoidAsync("setActiveNode", nodeId); } + private async void HandleLoadingChange(bool isLoading) + { + await InvokeAsync(StateHasChanged); + } + protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { - var result = await Mediator.Send(new GetKnowledgeGraphQuery()); - if (result.IsSuccess) + await InitializeGraphAsync(); + + if (GraphService.CurrentGraphData != null) { - GraphData = result.Value; - StateHasChanged(); - await InitializeGraphAsync(); + HandleGraphUpdate(); } } } @@ -73,7 +90,7 @@ { _module = await JS.InvokeAsync("import", "./_content/NexusReader.UI.Shared/js/knowledgeGraph.js"); _dotNetHelper = DotNetObjectReference.Create(this); - await _module.InvokeVoidAsync("mount", ContainerId, GraphData, _dotNetHelper); + await _module.InvokeVoidAsync("mount", ContainerId, GraphService.CurrentGraphData, _dotNetHelper); } private async Task ZoomIn() => await (_module?.InvokeVoidAsync("zoomIn") ?? ValueTask.CompletedTask); @@ -83,6 +100,8 @@ [JSInvokable] public async Task OnNodeClicked(string nodeId) { + InteractionService.NotifyNodeSelected(nodeId); + if (OnNodeSelected.HasDelegate) { await OnNodeSelected.InvokeAsync(nodeId); @@ -106,6 +125,10 @@ public async ValueTask DisposeAsync() { FocusMode.OnFocusModeChanged -= HandleFocusSimulation; + GraphService.OnGraphUpdated -= HandleGraphUpdate; + GraphService.OnActiveNodeChanged -= HandleActiveNodeChange; + GraphService.OnLoadingChanged -= HandleLoadingChange; + try { if (_module is not null) @@ -114,14 +137,7 @@ await _module.DisposeAsync(); } } - catch (JSDisconnectedException) - { - // Ignored, the circuit is already closed - } - catch (TaskCanceledException) - { - // Ignored, the circuit is already closed - } + catch { } _dotNetHelper?.Dispose(); } diff --git a/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor.css b/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor.css index d9665c7..ed59fd1 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor.css +++ b/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor.css @@ -46,19 +46,50 @@ font-size: 0.8rem; } - .loading-state { display: flex; flex-direction: column; align-items: center; - gap: 1rem; - animation: pulse 2s infinite ease-in-out; + gap: 1.5rem; + color: #fff; + text-align: center; } -@keyframes pulse { - 0% { opacity: 0.6; } - 50% { opacity: 1; } - 100% { opacity: 0.6; } +.preloader-robot { + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +.neon-pulse { + color: var(--nexus-neon); + filter: drop-shadow(0 0 10px var(--nexus-neon)); + animation: robot-pulse 2s infinite ease-in-out; +} + +@keyframes robot-pulse { + 0% { transform: scale(1); filter: drop-shadow(0 0 10px var(--nexus-neon)); } + 50% { transform: scale(1.1); filter: drop-shadow(0 0 25px var(--nexus-neon)); } + 100% { transform: scale(1); filter: drop-shadow(0 0 10px var(--nexus-neon)); } +} + +.scan-line { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 2px; + background: var(--nexus-neon); + box-shadow: 0 0 15px var(--nexus-neon); + animation: scan 2s infinite linear; + opacity: 0.8; +} + +@keyframes scan { + 0% { top: 0; } + 50% { top: 100%; } + 100% { top: 0; } } ::deep .nexus-node-active { @@ -67,9 +98,3 @@ filter: drop-shadow(0 0 12px var(--nexus-neon)); transition: all 0.3s ease; } - -.neon-glow { - color: var(--nexus-neon); - filter: drop-shadow(0 0 5px var(--nexus-neon)); -} - diff --git a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor index 873da36..1e77c90 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor +++ b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor @@ -9,6 +9,7 @@ @inject IFocusModeService FocusMode @inject IReaderNavigationService NavigationService @inject KnowledgeCoordinator Coordinator +@inject IReaderInteractionService InteractionService
@if (ViewModel == null) @@ -19,36 +20,45 @@ } else { -
+
@foreach (var block in ViewModel.Blocks) { -
+
@if (block is TextSegmentBlock textSegment) { @((MarkupString)textSegment.Content) } - else if (block is AiActionTriggerBlock aiTrigger) - { - - }
}
} + +
@code { private ReaderPageViewModel? ViewModel; private string StatusMessage = "Loading chapter..."; + private string _selectedText = string.Empty; + private string _selectedBlockId = string.Empty; + private SelectionCoordinates? _selectionCoords; + private string? _highlightedBlockId; + private bool _isJsInitialized; + private ElementReference _containerRef; + protected override void OnInitialized() { ThemeService.OnThemeChanged += StateHasChanged; NavigationService.OnNavigationChanged += OnNavigationChanged; + + InteractionService.OnScrollToBlockRequested += HandleScrollRequested; + InteractionService.OnHighlightBlockRequested += HandleHighlightRequested; + InteractionService.OnTextSelected += HandleTextSelected; } protected override async Task OnParametersSetAsync() @@ -58,19 +68,33 @@ private async void OnNavigationChanged() { + _isJsInitialized = false; + _selectedText = string.Empty; + _selectionCoords = null; await LoadChapterAsync(NavigationService.CurrentChapterIndex); StateHasChanged(); - await InitializeObserverAsync(); } protected override async Task OnAfterRenderAsync(bool firstRender) { - if (firstRender) + if (ViewModel != null && !_isJsInitialized) { + _isJsInitialized = true; await InitializeObserverAsync(); + await InitializeSelectionListenerAsync(); } } + private async Task InitializeSelectionListenerAsync() + { + try + { + var module = await JS.InvokeAsync("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js"); + await module.InvokeVoidAsync("initSelectionListener", DotNetObjectReference.Create(this), _containerRef); + } + catch { } + } + private async Task InitializeObserverAsync() { try @@ -87,6 +111,49 @@ Coordinator.OnBlockReached(blockId, content); } + [JSInvokable] + public void HandleTextSelected(string text, string blockId, SelectionCoordinates coords) + { + Console.WriteLine($"[ReaderCanvas] Text selected: {text} at {coords.Top},{coords.Left}"); + _selectedText = text; + _selectedBlockId = blockId; + _selectionCoords = coords; + StateHasChanged(); + } + + [JSInvokable] + public void HandleSelectionCleared() + { + _selectedText = string.Empty; + _selectionCoords = null; + StateHasChanged(); + } + + private void HandleScrollRequested(string blockId) + { + _ = ScrollToNodeAsync(blockId); + } + + private async void HandleHighlightRequested(string blockId) + { + _highlightedBlockId = blockId; + StateHasChanged(); + await Task.Delay(3000); // Highlight for 3 seconds + if (_highlightedBlockId == blockId) + { + _highlightedBlockId = null; + StateHasChanged(); + } + } + + private string GetFullPageContent() + { + if (ViewModel == null) return string.Empty; + return string.Join("\n\n", ViewModel.Blocks + .OfType() + .Select(b => $"[ID: {b.Id}]\n{b.Content}")); + } + private async Task LoadChapterAsync(int index) { ViewModel = null; @@ -97,6 +164,9 @@ { ViewModel = result.Value; NavigationService.UpdateMetadata(ViewModel.CurrentChapterIndex, ViewModel.TotalChapters, ViewModel.ChapterTitle); + + // Trigger full page graph generation after loading + _ = Coordinator.ProcessFullPageAsync(GetFullPageContent()); } else { @@ -122,5 +192,9 @@ { ThemeService.OnThemeChanged -= StateHasChanged; NavigationService.OnNavigationChanged -= OnNavigationChanged; + + InteractionService.OnScrollToBlockRequested -= HandleScrollRequested; + InteractionService.OnHighlightBlockRequested -= HandleHighlightRequested; + InteractionService.OnTextSelected -= HandleTextSelected; } } diff --git a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css index 0828078..b718676 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css +++ b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css @@ -3,10 +3,64 @@ margin: 0 auto; padding: 2rem 1rem; transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; } .reader-flow-container { display: flex; flex-direction: column; gap: 1.5rem; + position: relative; +} + +.block-wrapper { + transition: all 0.5s ease; + border-radius: 8px; + padding: 8px; + border: 1px solid transparent; +} + +.block-wrapper.highlighted { + background: rgba(0, 243, 255, 0.08); + box-shadow: 0 0 20px rgba(0, 243, 255, 0.15); + border-color: #00f3ff; + transform: scale(1.01); +} + +.ai-sparkle-trigger { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: rgba(18, 18, 18, 0.6); + border: 1px solid rgba(0, 255, 153, 0.3); + border-radius: 20px; + cursor: pointer; + margin: 1rem 0; + transition: all 0.3s ease; + position: relative; +} + +.ai-sparkle-trigger:hover { + background: rgba(0, 255, 153, 0.1); + border-color: #00ff99; + transform: translateY(-2px); +} + +.sparkle-tooltip { + font-size: 0.75rem; + color: #fff; + opacity: 0.7; +} + +.neon-pulse { + color: #00ff99; + filter: drop-shadow(0 0 5px #00ff99); + animation: pulse-small 2s infinite; +} + +@keyframes pulse-small { + 0% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.1); opacity: 0.8; } + 100% { transform: scale(1); opacity: 1; } } \ No newline at end of file diff --git a/src/NexusReader.UI.Shared/Services/IKnowledgeGraphService.cs b/src/NexusReader.UI.Shared/Services/IKnowledgeGraphService.cs index 9993be4..6db8c10 100644 --- a/src/NexusReader.UI.Shared/Services/IKnowledgeGraphService.cs +++ b/src/NexusReader.UI.Shared/Services/IKnowledgeGraphService.cs @@ -6,10 +6,14 @@ public interface IKnowledgeGraphService { GraphDataDto? CurrentGraphData { get; } string? ActiveNodeId { get; } + bool IsLoading { get; } event Action? OnGraphUpdated; event Action? OnActiveNodeChanged; - + event Action? OnLoadingChanged; + void UpdateGraph(GraphDataDto newData); void SetActiveNode(string nodeId); + void SetLoading(bool isLoading); + void Clear(); } diff --git a/src/NexusReader.UI.Shared/Services/IQuizStateService.cs b/src/NexusReader.UI.Shared/Services/IQuizStateService.cs index e3a5ffc..571719e 100644 --- a/src/NexusReader.UI.Shared/Services/IQuizStateService.cs +++ b/src/NexusReader.UI.Shared/Services/IQuizStateService.cs @@ -13,7 +13,7 @@ public interface IQuizStateService event Action? OnQuizUpdated; void RequestQuiz(string blockId); - void SetQuiz(string blockId, QuizDto quiz); + void SetQuiz(string? blockId, QuizDto quiz); void SetHydrating(bool hydrating); void MarkQuizAsSeen(); } diff --git a/src/NexusReader.UI.Shared/Services/IReaderInteractionService.cs b/src/NexusReader.UI.Shared/Services/IReaderInteractionService.cs new file mode 100644 index 0000000..c8ead52 --- /dev/null +++ b/src/NexusReader.UI.Shared/Services/IReaderInteractionService.cs @@ -0,0 +1,16 @@ +namespace NexusReader.UI.Shared.Services; + +public interface IReaderInteractionService +{ + event Action? OnNodeSelected; + event Action? OnScrollToBlockRequested; + event Action? OnHighlightBlockRequested; + event Action? OnTextSelected; + + void NotifyNodeSelected(string nodeId); + void RequestScrollToBlock(string blockId); + void RequestHighlightBlock(string blockId); + void NotifyTextSelected(string text, string blockId, SelectionCoordinates coords); +} + +public record SelectionCoordinates(double Top, double Left, double Width); diff --git a/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs b/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs index 629eeb2..60c4737 100644 --- a/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs +++ b/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs @@ -2,6 +2,7 @@ using NexusReader.Application.Abstractions.Services; using NexusReader.Application.Queries.Graph; using NexusReader.Application.Queries.Quiz; using NexusReader.UI.Shared.Services; +using NexusReader.Application.DTOs.AI; namespace NexusReader.UI.Shared.Services; @@ -11,131 +12,94 @@ public sealed class KnowledgeCoordinator : IDisposable private readonly IKnowledgeGraphService _graphService; private readonly IQuizStateService _quizService; private readonly IPlatformService _platformService; + private readonly IReaderInteractionService _interactionService; - private CancellationTokenSource? _debounceCts; + public event Action? OnGraphUpdated; public KnowledgeCoordinator( IKnowledgeService knowledgeService, IKnowledgeGraphService graphService, IQuizStateService quizService, - IPlatformService platformService) + IPlatformService platformService, + IReaderInteractionService interactionService) { _knowledgeService = knowledgeService; _graphService = graphService; _quizService = quizService; _platformService = platformService; + _interactionService = interactionService; + + _interactionService.OnNodeSelected += HandleNodeSelected; + } + + private void HandleNodeSelected(string nodeId) + { + _interactionService.RequestScrollToBlock(nodeId); + _interactionService.RequestHighlightBlock(nodeId); + } + + public async Task ProcessFullPageAsync(string fullContent) + { + if (string.IsNullOrWhiteSpace(fullContent)) return; + + Console.WriteLine("[KnowledgeCoordinator] Generating full page graph..."); + + _graphService.Clear(); + _graphService.SetLoading(true); + + try + { + var result = await _knowledgeService.GetGraphDataAsync(fullContent); + if (result.IsSuccess) + { + var packet = result.Value; + if (packet.Graph != null) + { + _graphService.UpdateGraph(packet.Graph); + OnGraphUpdated?.Invoke(packet.Graph); + await _platformService.VibrateSuccessAsync(); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[KnowledgeCoordinator] Error generating graph: {ex.Message}"); + } } public void OnBlockReached(string blockId, string content) { - Console.WriteLine($"[KnowledgeCoordinator] Block reached: {blockId}"); - - // 1. Skip extraction for the title page (usually the first block or contains 'title') - if (blockId.Equals("seg-0", StringComparison.OrdinalIgnoreCase) || - blockId.Contains("title", StringComparison.OrdinalIgnoreCase) || - content.Length < 50) // Title pages are usually short - { - Console.WriteLine($"[KnowledgeCoordinator] Skipping extraction for title page/short block: {blockId}"); - _graphService.SetActiveNode(blockId); - return; - } - - // 2. Update active node immediately for "TU JESTEŚ" logic + // Only update active node for "TU JESTEŚ" logic, do NOT trigger highlight here _graphService.SetActiveNode(blockId); - - // 3. Debounce the AI extraction to prevent spamming while scrolling - _debounceCts?.Cancel(); - _debounceCts = new CancellationTokenSource(); - var token = _debounceCts.Token; - - _ = DebounceAndExtractAsync(blockId, content, token); } - private async Task DebounceAndExtractAsync(string blockId, string content, CancellationToken token) - { - try - { - await Task.Delay(1000, token); - if (token.IsCancellationRequested) return; - - Console.WriteLine($"[KnowledgeCoordinator] Triggering extraction for block: {blockId}"); - await ProcessKnowledgeExtractionAsync(blockId, content, token); - } - catch (OperationCanceledException) { } - catch (Exception ex) - { - Console.WriteLine($"[KnowledgeCoordinator] Unexpected error in task: {ex.Message}"); - } - } - - private async Task ProcessKnowledgeExtractionAsync(string blockId, string content, CancellationToken ct) + public async Task RequestSummaryAndQuizAsync(string content) { _quizService.SetHydrating(true); - - var result = await _knowledgeService.GetKnowledgeAsync(content, ct); - - if (result.IsSuccess && !ct.IsCancellationRequested) + try { - Console.WriteLine($"[KnowledgeCoordinator] Extraction success for block: {blockId}. Updating state..."); - var packet = result.Value; - - // Update Quiz State - var quizQuestions = packet.Quizzes - .Select(q => new QuizQuestionDto(q.Question, q.Options, q.CorrectIndex)) - .ToList(); - _quizService.SetQuiz(blockId, new QuizDto(quizQuestions)); - - // Update Graph State - GraphDataDto graphData; - if (packet.Graph != null && packet.Graph.Nodes != null && packet.Graph.Nodes.Any()) + var result = await _knowledgeService.GetSummaryAndQuizAsync(content); + if (result.IsSuccess) { - // Use AI-generated graph - graphData = packet.Graph; - - // Ensure current block is linked to the first concept or added if missing - if (!graphData.Nodes.Any(n => n.Id == blockId)) - { - graphData.Nodes.Add(new GraphNodeDto(blockId, "TU JESTEŚ", "current")); - if (graphData.Nodes.Count > 1) - { - graphData.Links.Add(new GraphLinkDto(blockId, graphData.Nodes[0].Id, 1)); - } - } - } - else - { - // Fallback: Transform Concepts to GraphData if AI didn't provide a graph - var nodes = packet.Concepts - .Select(c => new GraphNodeDto(c.Title.ToLowerInvariant(), c.Title, "concept")) + var packet = result.Value; + var quizQuestions = packet.Quizzes + .Select(q => new QuizQuestionDto(q.Question, q.Options, q.CorrectIndex)) .ToList(); - nodes.Add(new GraphNodeDto(blockId, "TU JESTEŚ", "current")); - - var links = packet.Concepts - .Select(c => new GraphLinkDto(blockId, c.Title.ToLowerInvariant(), 1)) - .ToList(); - - graphData = new GraphDataDto { Nodes = nodes, Links = links }; + _quizService.SetQuiz(null, new QuizDto(quizQuestions)); + await _platformService.VibrateSuccessAsync(); + return packet; } - - _graphService.UpdateGraph(graphData); - - // Visual/Haptic Feedback - await _platformService.VibrateSuccessAsync(); } - else + finally { - if (!ct.IsCancellationRequested) - { - Console.WriteLine($"[KnowledgeCoordinator] Extraction failed or returned empty for block: {blockId}. Error: {result.Errors.FirstOrDefault()?.Message}"); - } _quizService.SetHydrating(false); } + return null; } public void Dispose() { - _debounceCts?.Cancel(); - _debounceCts?.Dispose(); + _interactionService.OnNodeSelected -= HandleNodeSelected; } } diff --git a/src/NexusReader.UI.Shared/Services/KnowledgeGraphService.cs b/src/NexusReader.UI.Shared/Services/KnowledgeGraphService.cs index ad8970a..a96b85b 100644 --- a/src/NexusReader.UI.Shared/Services/KnowledgeGraphService.cs +++ b/src/NexusReader.UI.Shared/Services/KnowledgeGraphService.cs @@ -6,13 +6,17 @@ public sealed class KnowledgeGraphService : IKnowledgeGraphService { public GraphDataDto? CurrentGraphData { get; private set; } public string? ActiveNodeId { get; private set; } + public bool IsLoading { get; private set; } public event Action? OnGraphUpdated; public event Action? OnActiveNodeChanged; + public event Action? OnLoadingChanged; public void UpdateGraph(GraphDataDto newData) { CurrentGraphData = newData; + IsLoading = false; + OnLoadingChanged?.Invoke(false); OnGraphUpdated?.Invoke(); } @@ -22,4 +26,18 @@ public sealed class KnowledgeGraphService : IKnowledgeGraphService ActiveNodeId = nodeId; OnActiveNodeChanged?.Invoke(nodeId); } + + public void SetLoading(bool isLoading) + { + IsLoading = isLoading; + OnLoadingChanged?.Invoke(isLoading); + } + + public void Clear() + { + CurrentGraphData = null; + ActiveNodeId = null; + IsLoading = false; + OnGraphUpdated?.Invoke(); + } } diff --git a/src/NexusReader.UI.Shared/Services/QuizStateService.cs b/src/NexusReader.UI.Shared/Services/QuizStateService.cs index db42467..81a3435 100644 --- a/src/NexusReader.UI.Shared/Services/QuizStateService.cs +++ b/src/NexusReader.UI.Shared/Services/QuizStateService.cs @@ -18,7 +18,7 @@ public sealed class QuizStateService : IQuizStateService OnQuizRequested?.Invoke(blockId); } - public void SetQuiz(string blockId, QuizDto quiz) + public void SetQuiz(string? blockId, QuizDto quiz) { CurrentQuizBlockId = blockId; CurrentQuiz = quiz; diff --git a/src/NexusReader.UI.Shared/Services/ReaderInteractionService.cs b/src/NexusReader.UI.Shared/Services/ReaderInteractionService.cs new file mode 100644 index 0000000..e26e045 --- /dev/null +++ b/src/NexusReader.UI.Shared/Services/ReaderInteractionService.cs @@ -0,0 +1,29 @@ +namespace NexusReader.UI.Shared.Services; + +public sealed class ReaderInteractionService : IReaderInteractionService +{ + public event Action? OnNodeSelected; + public event Action? OnScrollToBlockRequested; + public event Action? OnHighlightBlockRequested; + public event Action? OnTextSelected; + + public void NotifyNodeSelected(string nodeId) + { + OnNodeSelected?.Invoke(nodeId); + } + + public void RequestScrollToBlock(string blockId) + { + OnScrollToBlockRequested?.Invoke(blockId); + } + + public void RequestHighlightBlock(string blockId) + { + OnHighlightBlockRequested?.Invoke(blockId); + } + + public void NotifyTextSelected(string text, string blockId, SelectionCoordinates coords) + { + OnTextSelected?.Invoke(text, blockId, coords); + } +} diff --git a/src/NexusReader.UI.Shared/wwwroot/js/knowledgeGraph.js b/src/NexusReader.UI.Shared/wwwroot/js/knowledgeGraph.js index e6ca24b..eb46cd1 100644 --- a/src/NexusReader.UI.Shared/wwwroot/js/knowledgeGraph.js +++ b/src/NexusReader.UI.Shared/wwwroot/js/knowledgeGraph.js @@ -99,6 +99,10 @@ export function mount(containerId, data, dotNetHelper) { export function updateData(data) { if (!simulation || !rootGroup) return; + if (!data || !data.nodes) { + clear(); + return; + } // Keep existing node positions if they match by ID const oldNodes = new Map(simulation.nodes().map(d => [d.id, d])); @@ -261,3 +265,14 @@ export function zoomReset() { } } +export function clear() { + if (!rootGroup) return; + rootGroup.select(".links-layer").selectAll("path").remove(); + rootGroup.select(".nodes-layer").selectAll("g.node-group").remove(); + if (badge) badge.style("display", "none"); + if (simulation) { + simulation.nodes([]); + simulation.force("link").links([]); + simulation.stop(); + } +} diff --git a/src/NexusReader.UI.Shared/wwwroot/js/selectionHandler.js b/src/NexusReader.UI.Shared/wwwroot/js/selectionHandler.js new file mode 100644 index 0000000..f054250 --- /dev/null +++ b/src/NexusReader.UI.Shared/wwwroot/js/selectionHandler.js @@ -0,0 +1,41 @@ +export function initSelectionListener(dotNetHelper, container) { + if (!container) return; + + console.log("[SelectionHandler] Initializing..."); + + const handleSelection = () => { + const selection = window.getSelection(); + const text = selection.toString().trim(); + + if (text.length > 3) { + const range = selection.getRangeAt(0); + + // Look for the closest block-wrapper + let node = range.commonAncestorContainer; + if (node.nodeType !== 1) node = node.parentElement; + + const blockNode = node.closest('[id]'); + + if (blockNode) { + const rect = range.getBoundingClientRect(); + + console.log("[SelectionHandler] Selection at screen coords:", rect.top, rect.left); + + dotNetHelper.invokeMethodAsync('HandleTextSelected', + text, + blockNode.id, + { + Top: rect.top, + Left: rect.left, + Width: rect.width + }); + } + } else { + dotNetHelper.invokeMethodAsync('HandleSelectionCleared'); + } + }; + + // Use multiple triggers for maximum reliability + document.addEventListener('selectionchange', handleSelection); + container.addEventListener('mouseup', () => setTimeout(handleSelection, 10)); +} diff --git a/src/NexusReader.Web.Client/Program.cs b/src/NexusReader.Web.Client/Program.cs index 1ae8023..cc9d139 100644 --- a/src/NexusReader.Web.Client/Program.cs +++ b/src/NexusReader.Web.Client/Program.cs @@ -14,6 +14,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/src/NexusReader.Web.Client/Services/WasmKnowledgeService.cs b/src/NexusReader.Web.Client/Services/WasmKnowledgeService.cs index 9f1af24..afadcc8 100644 --- a/src/NexusReader.Web.Client/Services/WasmKnowledgeService.cs +++ b/src/NexusReader.Web.Client/Services/WasmKnowledgeService.cs @@ -15,25 +15,36 @@ public class WasmKnowledgeService : IKnowledgeService } public async Task> GetKnowledgeAsync(string text, CancellationToken cancellationToken = default) + { + return await CallKnowledgeApiAsync("/api/knowledge", text, cancellationToken); + } + + public async Task> GetGraphDataAsync(string text, CancellationToken cancellationToken = default) + { + return await CallKnowledgeApiAsync("/api/knowledge/graph", text, cancellationToken); + } + + public async Task> GetSummaryAndQuizAsync(string text, CancellationToken cancellationToken = default) + { + return await CallKnowledgeApiAsync("/api/knowledge/summary", text, cancellationToken); + } + + private async Task> CallKnowledgeApiAsync(string endpoint, string text, CancellationToken cancellationToken) { try { - Console.WriteLine($"[WasmKnowledgeService] Calling API for extraction (Text length: {text.Length})..."); - var response = await _httpClient.PostAsJsonAsync("/api/knowledge", new { text }, cancellationToken); + var response = await _httpClient.PostAsJsonAsync(endpoint, new { text }, cancellationToken); if (response.IsSuccessStatusCode) { - Console.WriteLine("[WasmKnowledgeService] API Response success. Deserializing..."); var packet = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); return packet != null ? Result.Ok(packet) : Result.Fail("Failed to deserialize knowledge packet."); } var errorBody = await response.Content.ReadAsStringAsync(cancellationToken); - Console.WriteLine($"[WasmKnowledgeService] API Error ({response.StatusCode}): {errorBody}"); return Result.Fail($"Server error ({response.StatusCode}): {errorBody}"); } catch (Exception ex) { - Console.WriteLine($"[WasmKnowledgeService] Exception: {ex.Message}"); return Result.Fail(new Error($"Network or parsing error: {ex.Message}").CausedBy(ex)); } } diff --git a/src/NexusReader.Web.New/Program.cs b/src/NexusReader.Web.New/Program.cs index ee2c6cc..79e9c3a 100644 --- a/src/NexusReader.Web.New/Program.cs +++ b/src/NexusReader.Web.New/Program.cs @@ -25,6 +25,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddApplication(); @@ -73,9 +74,21 @@ app.MapPost("/api/knowledge", async (KnowledgeRequest request, IKnowledgeService { var result = await knowledgeService.GetKnowledgeAsync(request.Text); if (result.IsSuccess) return Results.Ok(result.Value); - - var errorMsg = result.Errors.FirstOrDefault()?.Message ?? "Unknown server error"; - return Results.BadRequest(errorMsg); + return Results.BadRequest(result.Errors.FirstOrDefault()?.Message ?? "Unknown server error"); +}); + +app.MapPost("/api/knowledge/graph", async (KnowledgeRequest request, IKnowledgeService knowledgeService) => +{ + var result = await knowledgeService.GetGraphDataAsync(request.Text); + if (result.IsSuccess) return Results.Ok(result.Value); + return Results.BadRequest(result.Errors.FirstOrDefault()?.Message ?? "Unknown server error"); +}); + +app.MapPost("/api/knowledge/summary", async (KnowledgeRequest request, IKnowledgeService knowledgeService) => +{ + var result = await knowledgeService.GetSummaryAndQuizAsync(request.Text); + if (result.IsSuccess) return Results.Ok(result.Value); + return Results.BadRequest(result.Errors.FirstOrDefault()?.Message ?? "Unknown server error"); }); app.MapDelete("/api/knowledge", async (IKnowledgeService knowledgeService) => diff --git a/src/NexusReader.Web.New/appsettings.json b/src/NexusReader.Web.New/appsettings.json index be18923..0af7df8 100644 --- a/src/NexusReader.Web.New/appsettings.json +++ b/src/NexusReader.Web.New/appsettings.json @@ -13,7 +13,7 @@ "Google": { "ApiKey": "PLACEHOLDER", "Model": "gemini-2.5-flash-lite", - "MaxOutputTokens": 4096 + "MaxOutputTokens": 8192 } } } diff --git a/src/NexusReader.Web.New/nexus.db b/src/NexusReader.Web.New/nexus.db deleted file mode 100644 index 86c2fa8..0000000 Binary files a/src/NexusReader.Web.New/nexus.db and /dev/null differ