Initial commit: NexusArchitect Professional Workstation Overhaul
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user