Initial commit: NexusArchitect Professional Workstation Overhaul

This commit is contained in:
Debian
2026-04-24 20:27:22 +02:00
commit f3e94c4f42
193 changed files with 5809 additions and 0 deletions
@@ -0,0 +1,112 @@
@using MediatR
@using NexusReader.Application.Queries.Graph
@using Microsoft.JSInterop
@using NexusReader.UI.Shared.Services
@implements IAsyncDisposable
@inject IMediator Mediator
@inject IJSRuntime JS
@inject IFocusModeService FocusMode
<div class="knowledge-graph-container" id="@ContainerId">
@if (GraphData == null)
{
<div class="loading-state">
<NexusIcon Name="robot" Size="48" Class="neon-glow" />
<NexusTypography>Analyzing Chapter Nodes...</NexusTypography>
</div>
}
else
{
<div class="graph-controls">
<button class="zoom-btn" @onclick="ZoomIn" title="Zoom In">+</button>
<button class="zoom-btn" @onclick="ZoomOut" title="Zoom Out"></button>
<button class="zoom-btn reset" @onclick="ZoomReset" title="Reset">⟲</button>
</div>
}
</div>
@code {
[Parameter] public EventCallback<string> OnNodeSelected { get; set; }
private string ContainerId = "d3-graph-container";
private GraphDataDto? GraphData;
private IJSObjectReference? _module;
private DotNetObjectReference<KnowledgeGraph>? _dotNetHelper;
protected override void OnInitialized()
{
FocusMode.OnFocusModeChanged += HandleFocusSimulation;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var result = await Mediator.Send(new GetKnowledgeGraphQuery());
if (result.IsSuccess)
{
GraphData = result.Value;
StateHasChanged();
await InitializeGraphAsync();
}
}
}
private async Task InitializeGraphAsync()
{
_module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/knowledgeGraph.js");
_dotNetHelper = DotNetObjectReference.Create(this);
await _module.InvokeVoidAsync("mount", ContainerId, GraphData, _dotNetHelper);
}
private async Task ZoomIn() => await (_module?.InvokeVoidAsync("zoomIn") ?? ValueTask.CompletedTask);
private async Task ZoomOut() => await (_module?.InvokeVoidAsync("zoomOut") ?? ValueTask.CompletedTask);
private async Task ZoomReset() => await (_module?.InvokeVoidAsync("zoomReset") ?? ValueTask.CompletedTask);
[JSInvokable]
public async Task OnNodeClicked(string nodeId)
{
if (OnNodeSelected.HasDelegate)
{
await OnNodeSelected.InvokeAsync(nodeId);
}
}
private async void HandleFocusSimulation()
{
if (_module == null) return;
try
{
if (FocusMode.IsFocusModeActive)
await _module.InvokeVoidAsync("pause");
else
await _module.InvokeVoidAsync("resume");
}
catch { }
}
public async ValueTask DisposeAsync()
{
FocusMode.OnFocusModeChanged -= HandleFocusSimulation;
try
{
if (_module is not null)
{
await _module.InvokeVoidAsync("unmount", ContainerId);
await _module.DisposeAsync();
}
}
catch (JSDisconnectedException)
{
// Ignored, the circuit is already closed
}
catch (TaskCanceledException)
{
// Ignored, the circuit is already closed
}
_dotNetHelper?.Dispose();
}
}
@@ -0,0 +1,75 @@
.knowledge-graph-container {
width: 100%;
height: 50vh;
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent;
overflow: hidden;
position: relative;
}
.graph-controls {
position: absolute;
bottom: 1rem;
right: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
z-index: 10;
}
.zoom-btn {
width: 28px;
height: 28px;
background: rgba(18, 18, 18, 0.8);
backdrop-filter: blur(4px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px;
color: #888;
font-size: 1rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.zoom-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--nexus-neon);
border-color: var(--nexus-neon);
}
.zoom-btn.reset {
font-size: 0.8rem;
}
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
animation: pulse 2s infinite ease-in-out;
}
@keyframes pulse {
0% { opacity: 0.6; }
50% { opacity: 1; }
100% { opacity: 0.6; }
}
::deep .nexus-node-active {
stroke: var(--nexus-neon) !important;
stroke-width: 2px !important;
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));
}
@@ -0,0 +1,79 @@
@using MediatR
@using NexusReader.Application.Queries.Reader
@using Microsoft.JSInterop
@using NexusReader.UI.Shared.Services
@implements IDisposable
@inject IMediator Mediator
@inject IJSRuntime JS
@inject IThemeService ThemeService
@inject IFocusModeService FocusMode
<div class="reader-canvas theme-light">
@if (ViewModel == null)
{
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">@StatusMessage</NexusTypography>
}
else
{
<div class="reader-flow-container">
@foreach (var block in ViewModel.Blocks)
{
<div id="@block.Id" class="block-wrapper">
@if (block is TextSegmentBlock textSegment)
{
<NexusTypography Variant="NexusTypography.TypographyVariant.Ebook">@textSegment.Content</NexusTypography>
}
else if (block is AiActionTriggerBlock aiTrigger)
{
<AiAssistantBubble
ContextBlockId="@block.Id"
Dialogue="@aiTrigger.Dialogue"
Actions="@aiTrigger.ActionOptions"
OnActionTriggered="HandleAiAction" />
}
</div>
}
</div>
}
</div>
@code {
private ReaderPageViewModel? ViewModel;
private string StatusMessage = "Loading chapter...";
protected override async Task OnInitializedAsync()
{
ThemeService.OnThemeChanged += StateHasChanged;
var result = await Mediator.Send(new GetReaderPageQuery());
if (result.IsSuccess)
{
ViewModel = result.Value;
}
else
{
StatusMessage = "Failed to load chapter content.";
}
}
private void HandleAiAction(string action)
{
Console.WriteLine($"Action Triggered from Bubble: {action}");
}
public async Task ScrollToNodeAsync(string id)
{
try
{
await JS.InvokeVoidAsync("eval", $"document.getElementById('{id}')?.scrollIntoView({{ behavior: 'smooth', block: 'center' }});");
}
catch { }
}
public void Dispose()
{
ThemeService.OnThemeChanged -= StateHasChanged;
}
}
@@ -0,0 +1,12 @@
.reader-canvas {
max-width: 800px;
margin: 0 auto;
padding: 2rem 1rem;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.reader-flow-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
@@ -0,0 +1,21 @@
<footer class="reader-footer">
<div class="footer-content">
<div class="page-info">
<span class="label">Postęp:</span>
<span class="value">@Progress%</span>
</div>
<div class="progress-container">
<div class="progress-bar" style="width: @Progress%"></div>
</div>
<div class="meta-info">
<span class="time">1:30</span>
<span class="battery">45% 🔋</span>
</div>
</div>
</footer>
@code {
[Parameter] public int Progress { get; set; } = 45;
}
@@ -0,0 +1,53 @@
.reader-footer {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 40px;
background: #F9F9F9;
border-top: 1px solid rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
padding: 0 1.5rem;
z-index: 10;
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
font-family: var(--nexus-font-sans);
font-size: 0.75rem;
color: #666;
}
.progress-container {
flex: 1;
height: 4px;
background: rgba(0, 0, 0, 0.1);
margin: 0 2rem;
border-radius: 2px;
overflow: hidden;
max-width: 400px;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #00ff99 0%, #00d4ff 100%);
border-radius: 2px;
}
.page-info, .meta-info {
display: flex;
gap: 0.5rem;
align-items: center;
}
.label {
opacity: 0.7;
}
.value {
font-weight: 600;
}