327 lines
13 KiB
Plaintext
327 lines
13 KiB
Plaintext
@page "/intelligence"
|
|
@attribute [Authorize]
|
|
@using NexusReader.Application.DTOs.AI
|
|
@using NexusReader.Application.Abstractions.Services
|
|
@using NexusReader.Application.DTOs.User
|
|
@using NexusReader.UI.Shared.Components.Atoms
|
|
@using System.Net.Http.Json
|
|
@inject HttpClient Http
|
|
@inject IKnowledgeService KnowledgeService
|
|
@inject AuthenticationStateProvider AuthStateProvider
|
|
|
|
<div class="intelligence-page">
|
|
<header class="intelligence-header">
|
|
<div class="header-title-section">
|
|
<h1 class="neon-glow-text">Global Intelligence</h1>
|
|
<p class="subtitle">Interrogate, explore, and synthesize grounded knowledge from your library using Polyglot KM-RAG</p>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="intelligence-layout glass-panel">
|
|
<div class="chat-thread-container">
|
|
@if (_chatMessages.Count == 0)
|
|
{
|
|
<div class="welcome-state">
|
|
<div class="welcome-icon">
|
|
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
|
</svg>
|
|
</div>
|
|
<h3>Start Interrogating Your Library</h3>
|
|
<p>Ask complex questions across your entire ebook collection. The KM-RAG engine dynamically builds semantic maps, resolves dependencies, and formulates high-fidelity, grounded answers with interactive popover citations.</p>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="chat-bubbles-scroll">
|
|
@foreach (var message in _chatMessages)
|
|
{
|
|
<div class="message-row @(message.Sender == "User" ? "user-row" : "ai-row")" key="@message.Id">
|
|
<div class="message-avatar">
|
|
@if (message.Sender == "User")
|
|
{
|
|
<i class="bi bi-person-fill"></i>
|
|
}
|
|
else
|
|
{
|
|
<i class="bi bi-robot"></i>
|
|
}
|
|
</div>
|
|
<div class="message-bubble @(message.Sender == "User" ? "user-bubble" : "ai-bubble")">
|
|
<div class="message-header">
|
|
<span class="sender-name">@message.Sender</span>
|
|
<span class="message-time">@message.Timestamp.ToString("HH:mm")</span>
|
|
</div>
|
|
<div class="message-content">
|
|
@foreach (var segment in message.Segments)
|
|
{
|
|
@if (segment.IsCitation)
|
|
{
|
|
<NexusCitationMarker SourceId="@segment.CitationId" Citations="@message.Citations" />
|
|
}
|
|
else
|
|
{
|
|
@RenderMarkdown(segment.Text)
|
|
}
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (_isLoading)
|
|
{
|
|
<div class="message-row ai-row">
|
|
<div class="message-avatar">
|
|
<i class="bi bi-robot"></i>
|
|
</div>
|
|
<div class="message-bubble ai-bubble pending-bubble">
|
|
<div class="message-header">
|
|
<span class="sender-name">AI</span>
|
|
<span class="message-time">Thinking...</span>
|
|
</div>
|
|
<div class="message-content">
|
|
<div class="typing-indicator">
|
|
<span></span>
|
|
<span></span>
|
|
<span></span>
|
|
</div>
|
|
<span class="loading-label">Analyzing conceptual graphs and synthesizing response...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<div class="chat-input-controls">
|
|
<div class="input-panel-wrapper">
|
|
<div class="scope-bar">
|
|
<div class="scope-selector">
|
|
<label for="book-select"><i class="bi bi-compass"></i> Scope:</label>
|
|
<select id="book-select" class="nexus-select" @bind="_selectedBookId">
|
|
<option value="">All Books (Global Search)</option>
|
|
@if (_books != null)
|
|
{
|
|
@foreach (var book in _books)
|
|
{
|
|
<option value="@book.Id">@book.Title</option>
|
|
}
|
|
}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="input-field-group">
|
|
<input class="nexus-input"
|
|
placeholder="Ask a question about your books..."
|
|
@bind="_question"
|
|
@bind:event="oninput"
|
|
@onkeyup="HandleKeyUp"
|
|
disabled="@_isLoading" />
|
|
<button class="btn-nexus btn-nexus-primary search-btn"
|
|
disabled="@(string.IsNullOrWhiteSpace(_question) || _isLoading)"
|
|
@onclick="AskQuestionAsync">
|
|
@if (_isLoading)
|
|
{
|
|
<div class="btn-spinner"></div>
|
|
}
|
|
else
|
|
{
|
|
<span><i class="bi bi-send-fill"></i></span>
|
|
}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@code {
|
|
private string _question = string.Empty;
|
|
private string _selectedBookId = string.Empty;
|
|
private bool _isLoading;
|
|
private List<LastReadBookDto>? _books;
|
|
private List<ChatMessage> _chatMessages = new();
|
|
|
|
public class ChatMessage
|
|
{
|
|
public string Id { get; set; } = Guid.NewGuid().ToString();
|
|
public string Sender { get; set; } = string.Empty; // "User" or "AI"
|
|
public string Text { get; set; } = string.Empty;
|
|
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
|
public List<ResponseSegment> Segments { get; set; } = new();
|
|
public List<CitationDto> Citations { get; set; } = new();
|
|
}
|
|
|
|
public class ResponseSegment
|
|
{
|
|
public string Text { get; set; } = string.Empty;
|
|
public bool IsCitation { get; set; }
|
|
public string CitationId { get; set; } = string.Empty;
|
|
}
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
try
|
|
{
|
|
_books = await Http.GetFromJsonAsync<List<LastReadBookDto>>("api/library/books");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[Intelligence] Failed to load books for scope selector: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private async Task HandleKeyUp(KeyboardEventArgs e)
|
|
{
|
|
if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(_question) && !_isLoading)
|
|
{
|
|
await AskQuestionAsync();
|
|
}
|
|
}
|
|
|
|
private async Task AskQuestionAsync()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(_question) || _isLoading) return;
|
|
|
|
var userQuestion = _question;
|
|
_question = string.Empty; // Clear input field immediately
|
|
_isLoading = true;
|
|
|
|
// Add user query message
|
|
_chatMessages.Add(new ChatMessage
|
|
{
|
|
Sender = "User",
|
|
Text = userQuestion,
|
|
Segments = new List<ResponseSegment> { new ResponseSegment { Text = userQuestion, IsCitation = false } }
|
|
});
|
|
|
|
StateHasChanged();
|
|
|
|
try
|
|
{
|
|
Guid? ebookId = null;
|
|
if (!string.IsNullOrEmpty(_selectedBookId) && Guid.TryParse(_selectedBookId, out var parsedId))
|
|
{
|
|
ebookId = parsedId;
|
|
}
|
|
|
|
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
|
var tenantId = authState.User.FindFirst("TenantId")?.Value ?? "global";
|
|
|
|
var result = await KnowledgeService.AskQuestionAsync(userQuestion, tenantId, ebookId);
|
|
if (result.IsSuccess)
|
|
{
|
|
var response = result.Value;
|
|
_chatMessages.Add(new ChatMessage
|
|
{
|
|
Sender = "AI",
|
|
Text = response.Answer,
|
|
Segments = ParseSegments(response.Answer),
|
|
Citations = response.Citations
|
|
});
|
|
}
|
|
else
|
|
{
|
|
var errMsg = $"Error: {result.Errors.FirstOrDefault()?.Message ?? "An error occurred."}";
|
|
_chatMessages.Add(new ChatMessage
|
|
{
|
|
Sender = "AI",
|
|
Text = errMsg,
|
|
Segments = new List<ResponseSegment> { new ResponseSegment { Text = errMsg, IsCitation = false } }
|
|
});
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var errMsg = $"Network/API Error: {ex.Message}";
|
|
_chatMessages.Add(new ChatMessage
|
|
{
|
|
Sender = "AI",
|
|
Text = errMsg,
|
|
Segments = new List<ResponseSegment> { new ResponseSegment { Text = errMsg, IsCitation = false } }
|
|
});
|
|
}
|
|
finally
|
|
{
|
|
_isLoading = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private List<ResponseSegment> ParseSegments(string text)
|
|
{
|
|
var segments = new List<ResponseSegment>();
|
|
if (string.IsNullOrEmpty(text)) return segments;
|
|
|
|
// Matches [Source ID: some-id] OR raw GUIDs in brackets [e225e58f-7539-cd51-e0ab-82741ec7e65c]
|
|
var regex = new System.Text.RegularExpressions.Regex(
|
|
@"\[Source ID:\s*([^\]]+)\]|\[([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})\]",
|
|
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
|
var matches = regex.Matches(text);
|
|
|
|
int lastIndex = 0;
|
|
foreach (System.Text.RegularExpressions.Match match in matches)
|
|
{
|
|
if (match.Index > lastIndex)
|
|
{
|
|
segments.Add(new ResponseSegment
|
|
{
|
|
Text = text.Substring(lastIndex, match.Index - lastIndex),
|
|
IsCitation = false
|
|
});
|
|
}
|
|
|
|
var citationId = match.Groups[1].Success
|
|
? match.Groups[1].Value.Trim()
|
|
: match.Groups[2].Value.Trim();
|
|
|
|
segments.Add(new ResponseSegment
|
|
{
|
|
IsCitation = true,
|
|
CitationId = citationId
|
|
});
|
|
|
|
lastIndex = match.Index + match.Length;
|
|
}
|
|
|
|
if (lastIndex < text.Length)
|
|
{
|
|
segments.Add(new ResponseSegment
|
|
{
|
|
Text = text.Substring(lastIndex),
|
|
IsCitation = false
|
|
});
|
|
}
|
|
|
|
return segments;
|
|
}
|
|
|
|
private MarkupString RenderMarkdown(string text)
|
|
{
|
|
if (string.IsNullOrEmpty(text)) return new MarkupString(string.Empty);
|
|
|
|
// 1. HTML Encode to prevent XSS
|
|
var html = System.Net.WebUtility.HtmlEncode(text);
|
|
|
|
// 2. Bold: **text** -> <strong>text</strong>
|
|
html = System.Text.RegularExpressions.Regex.Replace(html, @"\*\*(.*?)\*\*", "<strong>$1</strong>");
|
|
|
|
// 3. Italic: *text* -> <em>text</em>
|
|
html = System.Text.RegularExpressions.Regex.Replace(html, @"\*(.*?)\*", "<em>$1</em>");
|
|
|
|
// 4. Code blocks: ```language ... ``` -> <pre class="nexus-code-block"><code>...</code></pre>
|
|
html = System.Text.RegularExpressions.Regex.Replace(html, @"```(?:[a-zA-Z0-9+#]+)?\s*([\s\S]*?)\s*```", "<pre class=\"nexus-code-block\"><code>$1</code></pre>");
|
|
|
|
// 5. Inline Code: `code` -> <code class="nexus-inline-code">code</code>
|
|
html = System.Text.RegularExpressions.Regex.Replace(html, @"`(.*?)`", "<code class=\"nexus-inline-code\">$1</code>");
|
|
|
|
// 6. Newlines: \n -> <br />
|
|
html = html.Replace("\n", "<br />");
|
|
|
|
return new MarkupString(html);
|
|
}
|
|
}
|