style(ui): refactor reader layout grid, fix focus mode layout collapse, fix SVG rendering dots, reorganize intelligence toolbar (#69)
Reorganized the reader toolbar and layout grid to improve visual consistency and layout robustness in Focus Mode. Fixed outline SVG rendering bugs that caused icons to show as solid dots. Closes #70 --------- Co-authored-by: Marek Jasiński <jasins.marek@gmail.com> Reviewed-on: #69 Co-authored-by: Antigravity <antigravity@google.com> Co-committed-by: Antigravity <antigravity@google.com>
This commit was merged in pull request #69.
This commit is contained in:
@@ -36,3 +36,13 @@ Run test suite:
|
||||
```bash
|
||||
dotnet test --no-restore
|
||||
```
|
||||
|
||||
## 🗄️ Database Migrations
|
||||
|
||||
Automatic database migrations at startup (`MigrateAsync()`) have been disabled to ensure compatibility with Native AOT compilation and prevent locking issues in multi-instance environments.
|
||||
|
||||
To apply database migrations locally, run the EF Core migration command from the solution root:
|
||||
|
||||
```bash
|
||||
dotnet ef database update --project src/NexusReader.Infrastructure --startup-project src/NexusReader.Web
|
||||
```
|
||||
|
||||
@@ -33,10 +33,11 @@
|
||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
|
||||
break;
|
||||
case "search":
|
||||
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" />
|
||||
<circle cx="11" cy="11" r="8" fill="none" stroke="currentColor" stroke-width="2" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
break;
|
||||
case "message-square":
|
||||
<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 d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
break;
|
||||
case "diamond":
|
||||
<path d="M12 3L3 12L12 21L21 12L12 3Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
@@ -56,34 +57,49 @@
|
||||
<circle cx="12" cy="7" r="4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
break;
|
||||
case "settings":
|
||||
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /><circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
|
||||
<circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
break;
|
||||
case "bookmark":
|
||||
<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" />
|
||||
<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
break;
|
||||
case "target":
|
||||
<circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="6" /><circle cx="12" cy="12" r="2" />
|
||||
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" />
|
||||
<circle cx="12" cy="12" r="6" fill="none" stroke="currentColor" stroke-width="2" />
|
||||
<circle cx="12" cy="12" r="2" fill="none" stroke="currentColor" stroke-width="2" />
|
||||
break;
|
||||
case "trash":
|
||||
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M10 11v6M14 11v6" />
|
||||
<polyline points="3 6 5 6 21 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<line x1="10" y1="11" x2="10" y2="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<line x1="14" y1="11" x2="14" y2="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
break;
|
||||
case "mail":
|
||||
<rect width="20" height="16" x="2" y="4" rx="2" /><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
||||
<rect width="20" height="16" x="2" y="4" rx="2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
break;
|
||||
case "lock":
|
||||
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
||||
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
break;
|
||||
case "eye":
|
||||
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" /><circle cx="12" cy="12" r="3" />
|
||||
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
break;
|
||||
case "eye-off":
|
||||
<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24" /><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68" /><path d="M6.61 6.61A13.52 13.52 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61" /><line x1="2" x2="22" y1="2" y2="22" />
|
||||
<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M6.61 6.61A13.52 13.52 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<line x1="2" x2="22" y1="2" y2="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
break;
|
||||
case "arrow-left":
|
||||
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||
<line x1="19" y1="12" x2="5" y2="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<polyline points="12 19 5 12 12 5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
break;
|
||||
case "arrow-right":
|
||||
<path d="M5 12h14M12 5l7 7-7 7" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<polyline points="12 5 19 12 12 19" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
break;
|
||||
case "log-out":
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
@@ -99,6 +115,13 @@
|
||||
<line x1="18" y1="6" x2="6" y2="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
break;
|
||||
case "sun":
|
||||
<circle cx="12" cy="12" r="4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
break;
|
||||
case "moon":
|
||||
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
break;
|
||||
default:
|
||||
<!-- Fallback circle -->
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
|
||||
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,47 @@
|
||||
@namespace NexusReader.UI.Shared.Components.Molecules
|
||||
|
||||
<div class="nexus-callout-box nexus-callout-@Type.ToString().ToLower() @Class">
|
||||
@if (!string.IsNullOrEmpty(Title))
|
||||
{
|
||||
<div class="nexus-callout-header">
|
||||
@if (Type == CalloutType.Warning || Type == CalloutType.Error)
|
||||
{
|
||||
<NexusIcon Name="warning" Size="16" Class="nexus-callout-icon" />
|
||||
}
|
||||
else if (Type == CalloutType.Success)
|
||||
{
|
||||
<NexusIcon Name="check" Size="16" Class="nexus-callout-icon" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<NexusIcon Name="info" Size="16" Class="nexus-callout-icon" />
|
||||
}
|
||||
<span class="nexus-callout-title">@Title</span>
|
||||
</div>
|
||||
}
|
||||
<div class="nexus-callout-body">
|
||||
@ChildContent
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
public enum CalloutType
|
||||
{
|
||||
Info,
|
||||
Warning,
|
||||
Success,
|
||||
Error
|
||||
}
|
||||
|
||||
[Parameter]
|
||||
public CalloutType Type { get; set; } = CalloutType.Info;
|
||||
|
||||
[Parameter]
|
||||
public string? Title { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string Class { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
.nexus-callout-box {
|
||||
padding: 1rem 1.25rem;
|
||||
margin: 1.5rem 0 1.5rem 0;
|
||||
border-radius: 0 8px 8px 0;
|
||||
font-family: var(--nexus-font-sans, sans-serif);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.2s ease;
|
||||
border: 1px solid transparent;
|
||||
border-left-width: 4px;
|
||||
}
|
||||
|
||||
/* Light / Dark default support via variables or custom colors */
|
||||
.nexus-callout-box {
|
||||
background-color: rgba(255, 255, 255, 0.02);
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* Info style */
|
||||
.nexus-callout-info {
|
||||
border-left-color: var(--nexus-neon, #00ff99);
|
||||
}
|
||||
|
||||
/* Warning style */
|
||||
.nexus-callout-warning {
|
||||
border-left-color: #eab308; /* warning yellow */
|
||||
background-color: rgba(234, 179, 8, 0.03);
|
||||
}
|
||||
|
||||
/* Success style */
|
||||
.nexus-callout-success {
|
||||
border-left-color: #10b981; /* success green */
|
||||
background-color: rgba(16, 185, 129, 0.03);
|
||||
}
|
||||
|
||||
/* Error style */
|
||||
.nexus-callout-error {
|
||||
border-left-color: #f43f5e; /* error red */
|
||||
background-color: rgba(244, 63, 94, 0.03);
|
||||
}
|
||||
|
||||
.nexus-callout-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.nexus-callout-info .nexus-callout-header {
|
||||
color: var(--nexus-neon, #00ff99);
|
||||
}
|
||||
|
||||
.nexus-callout-warning .nexus-callout-header {
|
||||
color: #eab308;
|
||||
}
|
||||
|
||||
.nexus-callout-success .nexus-callout-header {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.nexus-callout-error .nexus-callout-header {
|
||||
color: #f43f5e;
|
||||
}
|
||||
|
||||
.nexus-callout-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nexus-callout-body {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Light theme support */
|
||||
.theme-light .nexus-callout-box {
|
||||
background-color: #fcfcfb;
|
||||
border: 1px solid rgba(0, 0, 0, 0.03);
|
||||
border-left-width: 4px;
|
||||
color: #44403c;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.015);
|
||||
}
|
||||
|
||||
.theme-light .nexus-callout-info {
|
||||
border-left-color: #10b981;
|
||||
background-color: rgba(16, 185, 129, 0.04);
|
||||
}
|
||||
|
||||
.theme-light .nexus-callout-info .nexus-callout-header {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.theme-light .nexus-callout-warning {
|
||||
border-left-color: #d97706;
|
||||
background-color: rgba(217, 119, 6, 0.04);
|
||||
}
|
||||
|
||||
.theme-light .nexus-callout-warning .nexus-callout-header {
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.theme-light .nexus-callout-success {
|
||||
border-left-color: #10b981;
|
||||
background-color: rgba(16, 185, 129, 0.04);
|
||||
}
|
||||
|
||||
.theme-light .nexus-callout-success .nexus-callout-header {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.theme-light .nexus-callout-error {
|
||||
border-left-color: #e11d48;
|
||||
background-color: rgba(225, 29, 72, 0.04);
|
||||
}
|
||||
|
||||
.theme-light .nexus-callout-error .nexus-callout-header {
|
||||
color: #e11d48;
|
||||
}
|
||||
|
||||
@@ -1,45 +1,43 @@
|
||||
@using NexusReader.UI.Shared.Services
|
||||
@using NexusReader.Application.Abstractions.Services
|
||||
@inject IFocusModeService FocusMode
|
||||
@inject IKnowledgeService KnowledgeService
|
||||
@inject IIdentityService IdentityService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IThemeService ThemeService
|
||||
@inject IKnowledgeService KnowledgeService
|
||||
@implements IDisposable
|
||||
|
||||
<aside class="intelligence-toolbar">
|
||||
<div class="toolbar-top">
|
||||
<button class="toolbar-item" @onclick='() => NavigationManager.NavigateTo("/")' title="Back to Dashboard">
|
||||
<NexusIcon Name="arrow-left" Size="20" />
|
||||
</button>
|
||||
<button class="toolbar-item active" title="Chat">
|
||||
|
||||
@if (FocusMode.IsFocusModeActive)
|
||||
{
|
||||
<button class="toolbar-item active" @onclick="FocusMode.ToggleAsync" title="Focus Mode Active (Click to Exit)">
|
||||
<NexusIcon Name="target" Size="20" />
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="toolbar-item active" @onclick="FocusMode.ToggleAsync" title="Chat Active (Click to Focus)">
|
||||
<NexusIcon Name="message-square" Size="20" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-middle">
|
||||
<button class="toolbar-item" title="Settings">
|
||||
<NexusIcon Name="settings" Size="20" />
|
||||
</button>
|
||||
<button class="toolbar-item" title="Bookmarks">
|
||||
<NexusIcon Name="bookmark" Size="20" />
|
||||
</button>
|
||||
<button class="toolbar-item" title="Search">
|
||||
<NexusIcon Name="search" Size="20" />
|
||||
</button>
|
||||
<button class="toolbar-item danger" @onclick="HandleClearCache" title="Clear AI Cache">
|
||||
<NexusIcon Name="trash" Size="20" />
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="toolbar-bottom">
|
||||
<button class="toolbar-item @(FocusMode.IsFocusModeActive ? "active focus-active" : "")"
|
||||
@onclick="FocusMode.ToggleAsync" title="Focus Mode (F)">
|
||||
<NexusIcon Name="target" Size="20" />
|
||||
<div class="toolbar-separator"></div>
|
||||
|
||||
<button class="toolbar-item" @onclick="ThemeService.ToggleTheme" title="Przełącz motyw">
|
||||
<NexusIcon Name="@(ThemeService.IsLightMode ? "sun" : "moon")" Size="20" />
|
||||
</button>
|
||||
<button class="toolbar-item" @onclick='() => NavigationManager.NavigateTo("/")' title="Global Hub">
|
||||
<NexusIcon Name="layers" Size="20" />
|
||||
</button>
|
||||
<button class="toolbar-item logout-item" @onclick="HandleLogout" title="Exit">
|
||||
<NexusIcon Name="log-out" Size="20" />
|
||||
|
||||
<div class="toolbar-separator"></div>
|
||||
|
||||
<button class="toolbar-item clear-cache-item" @onclick="HandleClearCache" title="Wyczyść pamięć AI">
|
||||
<NexusIcon Name="trash" Size="20" />
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -48,11 +46,11 @@
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
FocusMode.OnFocusModeChanged += HandleUpdate;
|
||||
ThemeService.OnThemeChanged += HandleThemeChangedAsync;
|
||||
}
|
||||
|
||||
private async Task HandleClearCache()
|
||||
{
|
||||
// For now, a simple console log confirm or just do it
|
||||
Console.WriteLine("[IntelligenceToolbar] Requesting cache clear...");
|
||||
var result = await KnowledgeService.ClearCacheAsync();
|
||||
if (result.IsSuccess)
|
||||
@@ -61,16 +59,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleLogout()
|
||||
{
|
||||
await IdentityService.LogoutAsync();
|
||||
NavigationManager.NavigateTo("/account/logout-form", true);
|
||||
}
|
||||
|
||||
private Task HandleUpdate() => InvokeAsync(StateHasChanged);
|
||||
|
||||
private Task HandleThemeChangedAsync() => InvokeAsync(StateHasChanged);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
FocusMode.OnFocusModeChanged -= HandleUpdate;
|
||||
ThemeService.OnThemeChanged -= HandleThemeChangedAsync;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,26 +71,53 @@
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.toolbar-item.danger:hover {
|
||||
color: #ff4d4d;
|
||||
background: rgba(255, 77, 77, 0.1);
|
||||
|
||||
|
||||
.toolbar-separator {
|
||||
width: 24px;
|
||||
height: 1px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
margin: 0.2rem 0;
|
||||
}
|
||||
|
||||
.toolbar-item.logout-item {
|
||||
margin-top: 1rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
padding-top: 1.5rem;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-radius: 0;
|
||||
color: #444;
|
||||
/* Light mode overrides */
|
||||
.theme-light .intelligence-toolbar {
|
||||
background: #f5f5f4;
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow: inset -2px 0 10px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.toolbar-item.logout-item:hover {
|
||||
color: #ff4d4d;
|
||||
background: none;
|
||||
filter: drop-shadow(0 0 8px rgba(255, 77, 77, 0.4));
|
||||
.theme-light .toolbar-item {
|
||||
color: #78716c;
|
||||
}
|
||||
|
||||
.theme-light .toolbar-item:hover {
|
||||
color: #10b981;
|
||||
background: rgba(16, 185, 129, 0.05);
|
||||
box-shadow: 0 0 10px rgba(16, 185, 129, 0.1);
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.theme-light .toolbar-item.active {
|
||||
color: #10b981;
|
||||
background: rgba(16, 185, 129, 0.08);
|
||||
box-shadow: 0 0 15px rgba(16, 185, 129, 0.15);
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.theme-light .toolbar-item.active::after {
|
||||
background: #10b981;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.theme-light .toolbar-item.focus-active {
|
||||
color: #10b981;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.theme-light .toolbar-separator {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -335,3 +335,175 @@
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Light mode overrides */
|
||||
.theme-light .knowledge-check {
|
||||
background: #fafaf9;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.theme-light .header-title {
|
||||
color: #1c1917;
|
||||
}
|
||||
|
||||
.theme-light .question-text {
|
||||
color: #44403c;
|
||||
}
|
||||
|
||||
.theme-light .option-item {
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.theme-light .option-item:hover {
|
||||
background: #f5f5f4;
|
||||
}
|
||||
|
||||
.theme-light .option-item.selected {
|
||||
border-color: #10b981;
|
||||
background: rgba(16, 185, 129, 0.04);
|
||||
}
|
||||
|
||||
.theme-light .option-letter {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.theme-light .option-text {
|
||||
color: #292524;
|
||||
}
|
||||
|
||||
.theme-light .option-correct {
|
||||
border-color: #10b981 !important;
|
||||
background: rgba(16, 185, 129, 0.08) !important;
|
||||
}
|
||||
|
||||
.theme-light .option-incorrect {
|
||||
border-color: #f43f5e !important;
|
||||
background: rgba(244, 63, 94, 0.08) !important;
|
||||
}
|
||||
|
||||
.theme-light .option-revealed-correct {
|
||||
border-color: #10b981 !important;
|
||||
background: rgba(16, 185, 129, 0.06) !important;
|
||||
box-shadow: 0 0 8px rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
.theme-light .loading-state.shimmer {
|
||||
background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.03), transparent);
|
||||
color: #10b981;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.theme-light .submit-btn {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
color: #78716c;
|
||||
}
|
||||
|
||||
.theme-light .submit-btn:not(:disabled) {
|
||||
background: #10b981;
|
||||
color: #ffffff;
|
||||
border-color: #10b981;
|
||||
}
|
||||
|
||||
.theme-light .submitted-title {
|
||||
color: #1c1917;
|
||||
}
|
||||
|
||||
.theme-light .submitted-text {
|
||||
color: #78716c;
|
||||
}
|
||||
|
||||
.theme-light .score-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.theme-light .score-num {
|
||||
color: #10b981;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.theme-light .score-divider {
|
||||
color: #e7e5e4;
|
||||
}
|
||||
|
||||
.theme-light .score-total {
|
||||
color: #292524;
|
||||
}
|
||||
|
||||
.theme-light .score-percent {
|
||||
color: #78716c;
|
||||
}
|
||||
|
||||
.theme-light .reset-quiz-btn {
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
color: #44403c;
|
||||
}
|
||||
|
||||
.theme-light .reset-quiz-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border-color: #1c1917;
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.theme-light .empty-title {
|
||||
color: #1c1917;
|
||||
}
|
||||
|
||||
.theme-light .empty-text {
|
||||
color: #78716c;
|
||||
}
|
||||
|
||||
.theme-light .empty-icon-wrapper {
|
||||
background: rgba(16, 185, 129, 0.02);
|
||||
border: 1px solid rgba(16, 185, 129, 0.1);
|
||||
box-shadow: 0 0 20px rgba(16, 185, 129, 0.02);
|
||||
}
|
||||
|
||||
.theme-light .empty-quiz-state:hover .empty-icon-wrapper {
|
||||
background: rgba(16, 185, 129, 0.06);
|
||||
border-color: rgba(16, 185, 129, 0.3);
|
||||
box-shadow: 0 0 25px rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
.theme-light .generate-quiz-btn {
|
||||
background: rgba(16, 185, 129, 0.05);
|
||||
border: 1px solid #10b981;
|
||||
color: #10b981;
|
||||
text-shadow: none;
|
||||
box-shadow: 0 0 15px rgba(16, 185, 129, 0.05);
|
||||
}
|
||||
|
||||
.theme-light .generate-quiz-btn:not(:disabled):hover {
|
||||
background: #10b981;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 0 25px rgba(16, 185, 129, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.theme-light .generate-quiz-btn:disabled {
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
color: #a8a29e;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.theme-light .success-icon-wrapper {
|
||||
background: rgba(16, 185, 129, 0.05);
|
||||
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||
box-shadow: 0 0 20px rgba(16, 185, 129, 0.08);
|
||||
}
|
||||
|
||||
.theme-light .success-glow {
|
||||
color: #10b981;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.theme-light .neon-glow {
|
||||
color: #10b981;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,49 +3,47 @@
|
||||
@using NexusReader.Application.DTOs.AI
|
||||
@inject KnowledgeCoordinator Coordinator
|
||||
@inject IReaderInteractionService InteractionService
|
||||
@inject IQuizStateService QuizService
|
||||
@inject IJSRuntime JS
|
||||
|
||||
@if (IsVisible)
|
||||
{
|
||||
<div class="selection-ai-panel expanded @(PositionBelow ? "below" : "")" style="@PanelStyle">
|
||||
<div class="ai-bubble">
|
||||
<div class="ai-avatar">
|
||||
<div class="avatar-ring"></div>
|
||||
<NexusIcon Name="robot" Size="48" Class="neon-pulse" />
|
||||
<div class="avatar-label">
|
||||
<span class="name">E-Czytnik</span>
|
||||
<span class="role">Asystent AI</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ai-content">
|
||||
@if (IsLoading)
|
||||
<div class="selection-ai-panel @(_positionBelow ? "below" : "")" style="@_style">
|
||||
<button id="summary-btn" class="toolbar-btn primary @(IsLoadingSummary ? "loading" : "") @(IsAnyLoading ? "disabled cursor-not-allowed opacity-50" : "")"
|
||||
disabled="@IsAnyLoading"
|
||||
@onclick="RequestSummaryAsync">
|
||||
@if (IsLoadingSummary)
|
||||
{
|
||||
<div class="loading-state">
|
||||
<div class="shimmer">Skanowanie fragmentu...</div>
|
||||
</div>
|
||||
}
|
||||
else if (Packet != null)
|
||||
{
|
||||
<div class="summary-box">
|
||||
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">@Packet.Summary</NexusTypography>
|
||||
</div>
|
||||
<div class="ai-actions">
|
||||
<button class="action-btn neon-border" @onclick="GenerateFullQuiz">Generuj Quiz dla całej strony</button>
|
||||
<button class="action-btn ghost" @onclick="CloseAsync">Zamknij</button>
|
||||
</div>
|
||||
<svg class="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" style="animation: spin 1s linear infinite; width: 14px; height: 14px; color: currentColor; display: inline-block; margin-right: 4px;">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" style="opacity: 0.25;"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" style="opacity: 0.75;"></path>
|
||||
</svg>
|
||||
<span class="btn-text">Podsumowywanie...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="summary-box">
|
||||
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">Wykryto ciekawy fragment! Czy chcesz, abym wygenerował podsumowanie lub quiz z tego rozdziału?</NexusTypography>
|
||||
</div>
|
||||
<div class="ai-actions">
|
||||
<button class="action-btn neon-border" @onclick="RequestSummary">Podsumuj zaznaczenie</button>
|
||||
<button class="action-btn ghost" @onclick="CloseAsync">Pomiń</button>
|
||||
</div>
|
||||
<NexusIcon Name="book-open" Size="14" Class="btn-icon" />
|
||||
<span class="btn-text">Podsumuj</span>
|
||||
}
|
||||
</div>
|
||||
<div class="bubble-pointer"></div>
|
||||
</div>
|
||||
</button>
|
||||
<div class="toolbar-divider"></div>
|
||||
<button id="quiz-btn" class="toolbar-btn secondary @(IsLoadingQuiz ? "loading" : "") @(IsAnyLoading ? "disabled cursor-not-allowed opacity-50" : "")"
|
||||
disabled="@IsAnyLoading"
|
||||
@onclick="GenerateQuizAsync">
|
||||
@if (IsLoadingQuiz)
|
||||
{
|
||||
<svg class="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" style="animation: spin 1s linear infinite; width: 14px; height: 14px; color: currentColor; display: inline-block; margin-right: 4px;">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" style="opacity: 0.25;"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" style="opacity: 0.75;"></path>
|
||||
</svg>
|
||||
<span class="btn-text">Generowanie...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<NexusIcon Name="target" Size="14" Class="btn-icon" />
|
||||
<span class="btn-text">Quiz</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -56,47 +54,145 @@
|
||||
[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;
|
||||
private bool IsLoadingSummary = false;
|
||||
private bool IsLoadingQuiz = false;
|
||||
private bool IsAnyLoading => IsLoadingSummary || IsLoadingQuiz;
|
||||
|
||||
private string _style = "visibility: hidden; opacity: 0; pointer-events: none;";
|
||||
private bool _positionBelow = false;
|
||||
private SelectionCoordinates? _lastCoordinates;
|
||||
|
||||
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;
|
||||
Console.WriteLine($"[SelectionAiPanel] Parameters set. SelectedText: {SelectedText.Length} chars, Coordinates: {Coordinates?.Top}");
|
||||
|
||||
if (Coordinates != _lastCoordinates)
|
||||
{
|
||||
_lastCoordinates = Coordinates;
|
||||
_style = "visibility: hidden; opacity: 0; pointer-events: none;";
|
||||
_positionBelow = false;
|
||||
}
|
||||
|
||||
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;")
|
||||
: "";
|
||||
// Reset loading states when parameters change
|
||||
IsLoadingSummary = false;
|
||||
IsLoadingQuiz = false;
|
||||
}
|
||||
|
||||
private async Task RequestSummary()
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (IsVisible && _style.Contains("visibility: hidden"))
|
||||
{
|
||||
try
|
||||
{
|
||||
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js");
|
||||
var result = await module.InvokeAsync<PositionResult>("positionToolbar");
|
||||
if (result != null)
|
||||
{
|
||||
_style = string.Create(System.Globalization.CultureInfo.InvariantCulture,
|
||||
$"left: {result.Left:F1}px !important; " +
|
||||
$"top: {result.Top:F1}px !important; " +
|
||||
$"visibility: visible !important; " +
|
||||
$"opacity: 1 !important; " +
|
||||
$"pointer-events: auto !important;");
|
||||
_positionBelow = result.Below;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[SelectionAiPanel] Error positioning toolbar: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RequestSummaryAsync()
|
||||
{
|
||||
if (IsAnyLoading) return;
|
||||
IsLoadingSummary = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js");
|
||||
var selectedText = await module.InvokeAsync<string>("getSelectionText");
|
||||
if (string.IsNullOrWhiteSpace(selectedText))
|
||||
{
|
||||
selectedText = SelectedText;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(selectedText))
|
||||
{
|
||||
IsLoading = true;
|
||||
var contextPrompt = !string.IsNullOrWhiteSpace(FullPageContent)
|
||||
? $"ANALYSIS CONTEXT (Full Page Content):\n{FullPageContent}\n\nUSER SELECTION TO SUMMARIZE:\n"
|
||||
: "";
|
||||
|
||||
var result = await Coordinator.RequestSummaryAndQuizAsync($"{contextPrompt}{SelectedText}");
|
||||
Packet = result.IsSuccess ? result.Value : null;
|
||||
IsLoading = false;
|
||||
_ = Coordinator.StartSelectionSummaryAsync($"{contextPrompt}{selectedText}");
|
||||
await CloseAsync();
|
||||
await InteractionService.RequestAssistant();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[SelectionAiPanel] Error requesting summary: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoadingSummary = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task GenerateFullQuiz()
|
||||
private async Task GenerateQuizAsync()
|
||||
{
|
||||
if (IsAnyLoading) return;
|
||||
IsLoadingQuiz = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js");
|
||||
var selectedText = await module.InvokeAsync<string>("getSelectionText");
|
||||
if (string.IsNullOrWhiteSpace(selectedText))
|
||||
{
|
||||
selectedText = SelectedText;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(selectedText))
|
||||
{
|
||||
var contextPrompt = !string.IsNullOrWhiteSpace(FullPageContent)
|
||||
? $"ANALYSIS CONTEXT (Full Page Content):\n{FullPageContent}\n\nUSER SELECTION TO SUMMARIZE:\n"
|
||||
: "";
|
||||
|
||||
var result = await Coordinator.RequestSummaryAndQuizAsync($"{contextPrompt}{selectedText}");
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
IsLoading = true;
|
||||
await Coordinator.RequestSummaryAndQuizAsync(FullPageContent);
|
||||
IsLoading = false;
|
||||
await CloseAsync();
|
||||
await QuizService.RequestQuiz(BlockId);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[SelectionAiPanel] Error generating quiz: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoadingQuiz = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CloseAsync()
|
||||
{
|
||||
Packet = null;
|
||||
await InteractionService.NotifyTextSelected(string.Empty, string.Empty, null!);
|
||||
}
|
||||
|
||||
private class PositionResult
|
||||
{
|
||||
public double Left { get; set; }
|
||||
public double Top { get; set; }
|
||||
public bool Below { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,158 +1,149 @@
|
||||
.selection-ai-panel {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
width: 550px;
|
||||
max-width: 90vw;
|
||||
animation: fadeInScale 0.2s ease-out;
|
||||
pointer-events: auto;
|
||||
position: absolute;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(24, 24, 28, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5), 0 12px 28px rgba(0, 0, 0, 0.4);
|
||||
padding: 4px 6px;
|
||||
gap: 4px;
|
||||
pointer-events: none; /* Controlled by inline styles */
|
||||
user-select: none;
|
||||
animation: fadeInScale 0.18s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
@keyframes fadeInScale {
|
||||
from { opacity: 0; transform: translate(-50%, -90%) scale(0.95); }
|
||||
to { opacity: 1; transform: translate(-50%, -100%) scale(1); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: 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;
|
||||
.selection-ai-panel.below {
|
||||
animation: fadeInScaleBelow 0.18s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.ai-avatar {
|
||||
@keyframes fadeInScaleBelow {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
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 {
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: #e4e4e7; /* zinc-200 */
|
||||
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;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
white-space: nowrap;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.action-btn.ghost {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: #aaa;
|
||||
.toolbar-btn:hover:not(.disabled) {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.action-btn.neon-border {
|
||||
background: rgba(0, 255, 153, 0.1);
|
||||
border: 1px solid #00ff99;
|
||||
color: #00ff99;
|
||||
.toolbar-btn.primary {
|
||||
color: var(--nexus-neon, #00ff99);
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 255, 153, 0.2);
|
||||
.toolbar-btn.primary:hover:not(.disabled) {
|
||||
background: rgba(0, 255, 153, 0.08);
|
||||
box-shadow: 0 0 12px rgba(0, 255, 153, 0.15);
|
||||
}
|
||||
|
||||
.bubble-pointer {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 10px solid transparent;
|
||||
border-right: 10px solid transparent;
|
||||
.toolbar-btn.disabled {
|
||||
opacity: 0.35;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.selection-ai-panel:not(.below) .bubble-pointer {
|
||||
bottom: -10px;
|
||||
border-top: 10px solid rgba(18, 18, 18, 0.95);
|
||||
.toolbar-divider {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.selection-ai-panel.below .bubble-pointer {
|
||||
top: -10px;
|
||||
border-bottom: 10px solid rgba(18, 18, 18, 0.95);
|
||||
.btn-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
padding: 1rem;
|
||||
.spinner-inline {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid currentColor;
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.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 spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
from { background-position: 200% 0; }
|
||||
to { background-position: -200% 0; }
|
||||
.opacity-50 {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
.cursor-not-allowed {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
/* Light mode overrides */
|
||||
.theme-light .selection-ai-panel {
|
||||
background: rgba(254, 254, 254, 0.95);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.theme-light .toolbar-btn {
|
||||
color: #57524e;
|
||||
}
|
||||
|
||||
.theme-light .toolbar-btn:hover:not(.disabled) {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
color: #1c1917;
|
||||
}
|
||||
|
||||
.theme-light .toolbar-btn.primary {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.theme-light .toolbar-btn.primary:hover:not(.disabled) {
|
||||
background: rgba(16, 185, 129, 0.06);
|
||||
box-shadow: 0 0 12px rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
.theme-light .toolbar-divider {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="verification-state" style="@(IsVerifying ? "display:flex;" : "display:none;")">
|
||||
<div class="verification-state" style="@((IsVerifying && !IsIngesting) ? "display:flex;" : "display:none;")">
|
||||
@if (Metadata != null)
|
||||
{
|
||||
<div class="verification-layout">
|
||||
|
||||
@@ -196,52 +196,37 @@
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-family: var(--nexus-font-sans);
|
||||
font-weight: 600;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.5px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-transform: uppercase;
|
||||
::deep .nexus-btn.btn-primary {
|
||||
background: var(--nexus-neon, #00ffaa) !important;
|
||||
color: #050505 !important;
|
||||
border-color: transparent !important;
|
||||
box-shadow: 0 4px 12px rgba(var(--nexus-accent-rgb, 0, 255, 170), 0.2) !important;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--nexus-neon, #00ffaa);
|
||||
color: #050505;
|
||||
box-shadow: 0 4px 12px rgba(var(--nexus-accent-rgb, 0, 255, 170), 0.2);
|
||||
::deep .nexus-btn.btn-primary:hover:not(:disabled) {
|
||||
background: #00e699 !important;
|
||||
transform: translateY(-2px) !important;
|
||||
box-shadow: 0 6px 20px rgba(var(--nexus-accent-rgb, 0, 255, 170), 0.4) !important;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #00e699;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(var(--nexus-accent-rgb, 0, 255, 170), 0.4);
|
||||
::deep .nexus-btn.btn-primary:active:not(:disabled) {
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: translateY(0);
|
||||
::deep .nexus-btn.btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.03) !important;
|
||||
color: var(--nexus-text) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: var(--nexus-text);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
::deep .nexus-btn.btn-secondary:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.08) !important;
|
||||
border-color: rgba(255, 255, 255, 0.3) !important;
|
||||
transform: translateY(-2px) !important;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn-secondary:active {
|
||||
transform: translateY(0);
|
||||
::deep .nexus-btn.btn-secondary:active:not(:disabled) {
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
|
||||
/* Verification State */
|
||||
@@ -357,27 +342,30 @@
|
||||
to { transform: scale(1.2); opacity: 0.8; }
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
filter: grayscale(1);
|
||||
::deep .nexus-btn:disabled:not(.btn-loading) {
|
||||
opacity: 0.4 !important;
|
||||
cursor: not-allowed !important;
|
||||
filter: grayscale(1) !important;
|
||||
}
|
||||
|
||||
.btn-loading {
|
||||
position: relative;
|
||||
::deep .nexus-btn.btn-loading {
|
||||
position: relative !important;
|
||||
color: transparent !important;
|
||||
opacity: 1 !important;
|
||||
cursor: wait !important;
|
||||
filter: none !important;
|
||||
}
|
||||
|
||||
.btn-loading::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
border-top-color: var(--nexus-neon, #00ffaa);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
filter: drop-shadow(0 0 4px var(--nexus-neon, #00ffaa));
|
||||
::deep .nexus-btn.btn-loading::after {
|
||||
content: "" !important;
|
||||
position: absolute !important;
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
border: 2px solid rgba(255, 255, 255, 0.2) !important;
|
||||
border-top-color: var(--nexus-neon, #00ffaa) !important;
|
||||
border-radius: 50% !important;
|
||||
animation: spin 0.8s linear infinite !important;
|
||||
filter: drop-shadow(0 0 4px var(--nexus-neon, #00ffaa)) !important;
|
||||
}
|
||||
|
||||
/* Indexing State */
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.knowledge-graph-container.loading > ::deep svg {
|
||||
.knowledge-graph-container.loading> ::deep svg {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -93,9 +93,20 @@
|
||||
}
|
||||
|
||||
@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)); }
|
||||
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 {
|
||||
@@ -111,9 +122,17 @@
|
||||
}
|
||||
|
||||
@keyframes scan {
|
||||
0% { top: 0; }
|
||||
50% { top: 100%; }
|
||||
100% { top: 0; }
|
||||
0% {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
::deep .nexus-node-active {
|
||||
@@ -124,11 +143,24 @@
|
||||
}
|
||||
|
||||
::deep @keyframes neon-flash {
|
||||
0% { filter: brightness(1) drop-shadow(0 0 0px var(--nexus-neon)); }
|
||||
50% { filter: brightness(3) drop-shadow(0 0 30px var(--nexus-neon)); }
|
||||
100% { filter: brightness(1) drop-shadow(0 0 0px var(--nexus-neon)); }
|
||||
0% {
|
||||
filter: brightness(1) drop-shadow(0 0 0px var(--nexus-neon));
|
||||
}
|
||||
|
||||
50% {
|
||||
filter: brightness(3) drop-shadow(0 0 30px var(--nexus-neon));
|
||||
}
|
||||
|
||||
100% {
|
||||
filter: brightness(1) drop-shadow(0 0 0px var(--nexus-neon));
|
||||
}
|
||||
}
|
||||
|
||||
::deep .neon-flash-node {
|
||||
animation: neon-flash 0.8s ease-out;
|
||||
}
|
||||
|
||||
.knowledge-graph-container ::deep svg {
|
||||
background: radial-gradient(circle, #1a1a1a 0%, #121212 100%);
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
@@ -99,6 +99,7 @@
|
||||
private bool _isMobile = false;
|
||||
private DotNetObjectReference<ReaderCanvas>? _selfReference;
|
||||
private IJSObjectReference? _viewportModule;
|
||||
private IJSObjectReference? _selectionModule;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@@ -201,10 +202,13 @@
|
||||
{
|
||||
try
|
||||
{
|
||||
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js");
|
||||
if (_selectionModule == null)
|
||||
{
|
||||
_selectionModule = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js");
|
||||
}
|
||||
if (_selfReference != null)
|
||||
{
|
||||
await module.InvokeVoidAsync("initSelectionListener", _selfReference, _containerRef);
|
||||
await _selectionModule.InvokeVoidAsync("initSelectionListener", _selfReference, _containerRef);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -440,6 +444,19 @@
|
||||
InteractionService.OnTextSelected -= HandleTextSelected;
|
||||
SyncService.OnProgressReceived -= HandleSyncProgressReceived;
|
||||
|
||||
try
|
||||
{
|
||||
if (_selectionModule != null)
|
||||
{
|
||||
await _selectionModule.InvokeVoidAsync("destroySelectionListener");
|
||||
await _selectionModule.DisposeAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug(ex, "Failed to destroy JS selection listener.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_viewportModule != null)
|
||||
|
||||
@@ -30,18 +30,40 @@
|
||||
background-color: rgba(0, 255, 153, 0.5);
|
||||
}
|
||||
|
||||
.reader-canvas.theme-dark {
|
||||
background-color: #121214;
|
||||
}
|
||||
|
||||
.reader-canvas.theme-light {
|
||||
background-color: #F9F9F9; /* Paper-white requirement */
|
||||
background-color: #f4f1ea;
|
||||
/* Warm light beige/gray background */
|
||||
}
|
||||
|
||||
.reader-flow-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
max-width: 680px;
|
||||
margin: 2rem auto;
|
||||
min-height: calc(100vh - 180px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
position: relative;
|
||||
padding: 0 1.5rem 15rem 1.5rem; /* Large padding-bottom for reachability */
|
||||
padding: 3rem 4rem 15rem 4rem;
|
||||
/* Large padding-bottom for reachability, plus comfortable side margins */
|
||||
border-radius: 12px;
|
||||
box-sizing: border-box;
|
||||
transition: background-color 0.3s, box-shadow 0.3s, border-color 0.3s;
|
||||
}
|
||||
|
||||
.theme-dark .reader-flow-container {
|
||||
background-color: #1a1a1e;
|
||||
border: 1px solid rgba(255, 255, 255, 0.03);
|
||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.theme-light .reader-flow-container {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
box-shadow: 0 4px 20px rgba(139, 130, 115, 0.12);
|
||||
}
|
||||
|
||||
.block-wrapper {
|
||||
@@ -57,23 +79,48 @@
|
||||
line-height: 1.65 !important;
|
||||
letter-spacing: -0.01em !important;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 300;
|
||||
font-weight: 400;
|
||||
text-align: left !important;
|
||||
color: #e4e4e7;
|
||||
/* Off-white with light gray tint */
|
||||
}
|
||||
|
||||
.theme-light ::deep .nexus-ebook {
|
||||
color: #1a1a1a;
|
||||
color: #292524;
|
||||
/* Warm charcoal for legibility */
|
||||
}
|
||||
|
||||
/* Callout Box styling for legacy blockquote segments */
|
||||
::deep .nexus-ebook blockquote {
|
||||
background-color: rgba(255, 255, 255, 0.02);
|
||||
border-left: 4px solid var(--nexus-neon);
|
||||
padding: 1rem 1.25rem;
|
||||
margin: 1.5rem 0 1.5rem 0;
|
||||
border-radius: 0 8px 8px 0;
|
||||
font-size: 1.05rem;
|
||||
color: #e2e8f0;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.theme-light ::deep .nexus-ebook blockquote {
|
||||
background-color: rgba(245, 158, 11, 0.04);
|
||||
border-left: 4px solid #f59e0b;
|
||||
color: #44403c;
|
||||
}
|
||||
|
||||
|
||||
/* Technical Code Block Container */
|
||||
::deep .nexus-ebook pre {
|
||||
background-color: #2d2d2d; /* Dark theme for code for better contrast */
|
||||
background-color: #2d2d2d;
|
||||
/* Dark theme for code for better contrast */
|
||||
color: #e0e0e0;
|
||||
padding: 1.25rem;
|
||||
border-radius: 8px;
|
||||
margin: 2rem 0;
|
||||
overflow-x: auto;
|
||||
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
|
||||
border-left: 4px solid var(--nexus-neon); /* Nexus neon accent */
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
border-left: 4px solid var(--nexus-neon);
|
||||
/* Nexus neon accent */
|
||||
|
||||
/* Dedicated Scrollbar for Code */
|
||||
scrollbar-width: thin;
|
||||
@@ -101,7 +148,8 @@
|
||||
/* Inline Code Highlight */
|
||||
::deep .nexus-ebook p code {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
color: #d63384; /* Classic differentiator for inline code */
|
||||
color: #d63384;
|
||||
/* Classic differentiator for inline code */
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
@@ -153,9 +201,20 @@
|
||||
}
|
||||
|
||||
@keyframes pulse-small {
|
||||
0% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.1); opacity: 0.8; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Chapter Loading Overlay and Spinners */
|
||||
@@ -246,29 +305,48 @@
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from { transform: scale(0.9) translateY(10px); opacity: 0; }
|
||||
to { transform: scale(1) translateY(0); opacity: 1; }
|
||||
from {
|
||||
transform: scale(0.9) translateY(10px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: scale(1) translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* MOBILE READER UI OVERRIDES */
|
||||
@media (max-width: 768px) {
|
||||
.reader-canvas {
|
||||
padding-top: 54px !important;
|
||||
padding-bottom: 80px !important; /* Ensure content is clear of bottom toolbar */
|
||||
padding-bottom: 80px !important;
|
||||
/* Ensure content is clear of bottom toolbar */
|
||||
}
|
||||
|
||||
.reader-flow-container {
|
||||
padding-bottom: 4rem; /* Safe breathing room */
|
||||
padding-bottom: 4rem;
|
||||
/* Safe breathing room */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,7 +416,20 @@
|
||||
}
|
||||
|
||||
.theme-light .nexus-mobile-chapter-title {
|
||||
color: #1a1a1a;
|
||||
color: #292524;
|
||||
}
|
||||
|
||||
.theme-light .nexus-mobile-escape-btn {
|
||||
color: #78716c;
|
||||
}
|
||||
|
||||
.theme-light .nexus-mobile-escape-btn:hover {
|
||||
color: #10b981;
|
||||
background-color: rgba(16, 185, 129, 0.05);
|
||||
}
|
||||
|
||||
.theme-light .nexus-mobile-escape-btn:active {
|
||||
background-color: rgba(16, 185, 129, 0.08);
|
||||
}
|
||||
|
||||
.nexus-chapter-nav-btn {
|
||||
|
||||
@@ -1,64 +1,99 @@
|
||||
.reader-footer {
|
||||
position: relative;
|
||||
height: 50px;
|
||||
background: #F9F9F9;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: min(600px, 90%);
|
||||
height: 54px;
|
||||
background: rgba(24, 24, 27, 0.6);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 9999px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1.5rem;
|
||||
z-index: 10;
|
||||
flex-shrink: 0;
|
||||
z-index: 100;
|
||||
transition: background 0.3s, border-color 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.theme-light .reader-footer {
|
||||
background: rgba(254, 254, 254, 0.75);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.04), 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 1.5rem;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.navigation-controls {
|
||||
display: grid;
|
||||
grid-template-columns: 32px 1fr 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 260px;
|
||||
flex-shrink: 0;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
background: white;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 50%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: #333;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out, border-color 0.2s ease-in-out, transform 0.2s ease-in-out;
|
||||
color: #a1a1aa; /* Zinc-400 default contrast */
|
||||
}
|
||||
|
||||
.nav-btn:hover:not(:disabled) {
|
||||
background: #f0f0f0;
|
||||
transform: translateY(-1px);
|
||||
.nav-btn:hover:not(:disabled),
|
||||
.nav-btn:focus:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: var(--nexus-neon, #00ff99);
|
||||
color: var(--nexus-neon, #00ff99); /* Brand neon green hover/focus signal */
|
||||
transform: scale(1.05);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.nav-btn:disabled {
|
||||
opacity: 0.3;
|
||||
opacity: 0.25;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.theme-light .nav-btn {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border-color: rgba(0, 0, 0, 0.08);
|
||||
color: #78716c; /* Warm stone-500 */
|
||||
}
|
||||
|
||||
.theme-light .nav-btn:hover:not(:disabled),
|
||||
.theme-light .nav-btn:focus:not(:disabled) {
|
||||
background: rgba(16, 185, 129, 0.05);
|
||||
border-color: #10b981;
|
||||
color: #10b981;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.chapter-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
color: #e2e8f0; /* Slate-200 for clean high readability */
|
||||
}
|
||||
|
||||
.theme-light .chapter-info {
|
||||
color: #292524; /* Warm charcoal for legibility */
|
||||
}
|
||||
|
||||
.chapter-title {
|
||||
@@ -68,38 +103,55 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.chapter-count {
|
||||
opacity: 0.5;
|
||||
font-size: 0.75rem;
|
||||
color: #a1a1aa; /* Zinc-400 for secondary info clarity */
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.theme-light .chapter-count {
|
||||
color: #78716c; /* Warm stone-500 secondary info */
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 3px;
|
||||
width: 80px;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin: 0 1rem;
|
||||
margin: 0 0.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.theme-light .progress-container {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: #2ECC71;
|
||||
border-radius: 3px;
|
||||
background: var(--nexus-neon, #00ff99);
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.theme-light .progress-bar {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: #888;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
color: #a1a1aa;
|
||||
flex-shrink: 0;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.theme-light .meta-info {
|
||||
color: #78716c;
|
||||
}
|
||||
|
||||
.battery {
|
||||
@@ -107,3 +159,10 @@
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
/* RWD constraint: floating toolbar visible ONLY on desktop (min-width: 1024px) */
|
||||
@media (max-width: 1023px) {
|
||||
.reader-footer {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,12 @@
|
||||
@inject IIdentityService IdentityService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject Microsoft.Extensions.Logging.ILogger<ReaderLayout> Logger
|
||||
@inject IThemeService ThemeService
|
||||
@inject KnowledgeCoordinator Coordinator
|
||||
@implements IAsyncDisposable
|
||||
|
||||
|
||||
<div class="app-container @_platformClass @(FocusMode.IsFocusModeActive ? "focus-mode-active" : "") @($"active-mobile-tab-{_activeMobileTab.ToString().ToLower()}")">
|
||||
<div class="app-container @_platformClass @(FocusMode.IsFocusModeActive ? "focus-mode-active" : "") @($"active-mobile-tab-{_activeMobileTab.ToString().ToLower()}") @(ThemeService.IsLightMode ? "theme-light" : "theme-dark")">
|
||||
<div class="reader-pane">
|
||||
<main>
|
||||
@Body
|
||||
@@ -62,7 +64,32 @@
|
||||
<span class="panel-title">Contextual Intelligence Panel</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
@if (_selectedNode != null)
|
||||
@if (Coordinator.IsLoadingSelectionSummary)
|
||||
{
|
||||
<div class="skeleton-container">
|
||||
<div class="skeleton-line title"></div>
|
||||
<div class="skeleton-line w-90"></div>
|
||||
<div class="skeleton-line w-80"></div>
|
||||
<div class="skeleton-line w-70"></div>
|
||||
<div class="skeleton-line w-60"></div>
|
||||
</div>
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Coordinator.SelectionSummary))
|
||||
{
|
||||
<div class="node-details">
|
||||
<div class="node-header-section">
|
||||
<div class="summary-badge-row">
|
||||
<span class="node-group-badge current">PODSUMOWANIE</span>
|
||||
<button class="clear-summary-btn" @onclick="ClearSelectionSummary" title="Wyczyść podsumowanie">×</button>
|
||||
</div>
|
||||
<h3 class="node-label">Zaznaczony Fragment</h3>
|
||||
</div>
|
||||
<div class="detail-section summary-section">
|
||||
<p class="node-summary">@Coordinator.SelectionSummary</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (_selectedNode != null)
|
||||
{
|
||||
<div class="node-details">
|
||||
<div class="node-header-section">
|
||||
@@ -165,7 +192,32 @@
|
||||
{
|
||||
<div class="contextual-intelligence-panel">
|
||||
<div class="panel-body">
|
||||
@if (_selectedNode != null)
|
||||
@if (Coordinator.IsLoadingSelectionSummary)
|
||||
{
|
||||
<div class="skeleton-container">
|
||||
<div class="skeleton-line title"></div>
|
||||
<div class="skeleton-line w-90"></div>
|
||||
<div class="skeleton-line w-80"></div>
|
||||
<div class="skeleton-line w-70"></div>
|
||||
<div class="skeleton-line w-60"></div>
|
||||
</div>
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Coordinator.SelectionSummary))
|
||||
{
|
||||
<div class="node-details">
|
||||
<div class="node-header-section">
|
||||
<div class="summary-badge-row">
|
||||
<span class="node-group-badge current">PODSUMOWANIE</span>
|
||||
<button class="clear-summary-btn" @onclick="ClearSelectionSummary" title="Wyczyść podsumowanie">×</button>
|
||||
</div>
|
||||
<h3 class="node-label">Zaznaczony Fragment</h3>
|
||||
</div>
|
||||
<div class="detail-section summary-section">
|
||||
<p class="node-summary">@Coordinator.SelectionSummary</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (_selectedNode != null)
|
||||
{
|
||||
<div class="node-details">
|
||||
<div class="node-header-section">
|
||||
@@ -291,6 +343,8 @@
|
||||
InteractionService.OnAssistantRequested += HandleAssistantRequestedAsync;
|
||||
InteractionService.OnScrollPercentChanged += HandleScrollPercentChanged;
|
||||
GraphService.OnGraphUpdated += HandleGraphUpdatedAsync;
|
||||
ThemeService.OnThemeChanged += HandleThemeChangedAsync;
|
||||
Coordinator.OnSelectionSummaryStateChanged += HandleUpdate;
|
||||
|
||||
var context = PlatformService.GetDeviceContext();
|
||||
if (context.IsSuccess)
|
||||
@@ -305,6 +359,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleThemeChangedAsync() => await InvokeAsync(StateHasChanged);
|
||||
|
||||
private void SetActiveTab(SidebarTab tab)
|
||||
{
|
||||
_activeTab = tab;
|
||||
@@ -329,6 +385,11 @@
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task ClearSelectionSummary()
|
||||
{
|
||||
await Coordinator.ClearSelectionSummaryAsync();
|
||||
}
|
||||
|
||||
private async Task HandleScrollPercentChanged(int percent)
|
||||
{
|
||||
_scrollPercentage = percent;
|
||||
@@ -349,13 +410,25 @@
|
||||
{
|
||||
if (_isMobile)
|
||||
{
|
||||
if (Coordinator.IsLoadingSelectionSummary || !string.IsNullOrEmpty(Coordinator.SelectionSummary))
|
||||
{
|
||||
_activeMobileTab = MobileReaderTab.Concepts;
|
||||
_activeTab = SidebarTab.Knowledge;
|
||||
}
|
||||
OpenAssistant();
|
||||
}
|
||||
else
|
||||
{
|
||||
_activeMobileTab = MobileReaderTab.Concepts;
|
||||
if (Coordinator.IsLoadingSelectionSummary || !string.IsNullOrEmpty(Coordinator.SelectionSummary))
|
||||
{
|
||||
_activeTab = SidebarTab.Knowledge;
|
||||
}
|
||||
else
|
||||
{
|
||||
_activeTab = SidebarTab.Quiz;
|
||||
}
|
||||
}
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
@@ -445,6 +518,8 @@
|
||||
InteractionService.OnAssistantRequested -= HandleAssistantRequestedAsync;
|
||||
InteractionService.OnScrollPercentChanged -= HandleScrollPercentChanged;
|
||||
GraphService.OnGraphUpdated -= HandleGraphUpdatedAsync;
|
||||
ThemeService.OnThemeChanged -= HandleThemeChangedAsync;
|
||||
Coordinator.OnSelectionSummaryStateChanged -= HandleUpdate;
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -4,18 +4,20 @@
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: #121212;
|
||||
background: var(--nexus-bg);
|
||||
}
|
||||
|
||||
|
||||
.reader-pane {
|
||||
background: #F9F9F9;
|
||||
grid-column: 1;
|
||||
background: var(--nexus-bg);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 5;
|
||||
height: 100vh;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
main {
|
||||
@@ -27,30 +29,65 @@ main {
|
||||
}
|
||||
|
||||
.intelligence-sidebar {
|
||||
grid-column: 3;
|
||||
display: grid;
|
||||
grid-template-columns: 50px 1fr;
|
||||
width: 100%; /* controlled by grid */
|
||||
width: 100%;
|
||||
/* controlled by grid */
|
||||
height: 100%;
|
||||
background: #0d0d0d;
|
||||
background: var(--nexus-card);
|
||||
box-shadow: -10px 0 30px rgba(0, 0, 0, 0.3);
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.05);
|
||||
overflow: hidden;
|
||||
z-index: 10;
|
||||
transition: background 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.resizer {
|
||||
width: 4px;
|
||||
grid-column: 2;
|
||||
width: 12px;
|
||||
cursor: col-resize;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
transition: background 0.2s, width 0.2s;
|
||||
background: transparent;
|
||||
z-index: 20;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.05);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.resizer:hover, .app-container.is-resizing .resizer {
|
||||
.resizer::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: 0;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.resizer::after {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 60px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 9999px;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.resizer:hover::before,
|
||||
.app-container.is-resizing .resizer::before {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.resizer:hover::after,
|
||||
.app-container.is-resizing .resizer::after {
|
||||
background: var(--nexus-neon);
|
||||
width: 6px;
|
||||
box-shadow: 0 0 10px var(--nexus-neon);
|
||||
height: 80px;
|
||||
box-shadow: 0 0 12px var(--nexus-neon);
|
||||
}
|
||||
|
||||
.app-container.is-resizing {
|
||||
@@ -63,6 +100,7 @@ main {
|
||||
}
|
||||
|
||||
.app-container.focus-mode-active .intelligence-sidebar {
|
||||
grid-column: 3;
|
||||
grid-template-columns: 50px 0px;
|
||||
}
|
||||
|
||||
@@ -94,7 +132,7 @@ main {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
font-family: var(--nexus-font-sans);
|
||||
font-size: 0.9rem;
|
||||
color: #fff;
|
||||
color: var(--nexus-text);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -149,9 +187,20 @@ main {
|
||||
}
|
||||
|
||||
@keyframes quiz-pulse {
|
||||
0% { filter: drop-shadow(0 0 2px var(--nexus-neon)); transform: scale(1); }
|
||||
50% { filter: drop-shadow(0 0 10px var(--nexus-neon)); transform: scale(1.1); }
|
||||
100% { filter: drop-shadow(0 0 2px var(--nexus-neon)); transform: scale(1); }
|
||||
0% {
|
||||
filter: drop-shadow(0 0 2px var(--nexus-neon));
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
filter: drop-shadow(0 0 10px var(--nexus-neon));
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
100% {
|
||||
filter: drop-shadow(0 0 2px var(--nexus-neon));
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Contextual Intelligence Panel Layout */
|
||||
@@ -226,9 +275,20 @@ main {
|
||||
}
|
||||
|
||||
@keyframes glow-pulse {
|
||||
0% { transform: scale(0.9); opacity: 0.5; }
|
||||
50% { transform: scale(1.1); opacity: 1; }
|
||||
100% { transform: scale(0.9); opacity: 0.5; }
|
||||
0% {
|
||||
transform: scale(0.9);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(0.9);
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
@@ -245,8 +305,15 @@ main {
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; transform: translateY(5px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.node-header-section {
|
||||
@@ -432,9 +499,20 @@ main {
|
||||
}
|
||||
|
||||
@keyframes quiz-pulse-glow {
|
||||
0% { border-color: rgba(0, 240, 255, 0.3); box-shadow: 0 0 5px rgba(0, 240, 255, 0.1); }
|
||||
50% { border-color: var(--nexus-neon, #00f0ff); box-shadow: 0 0 25px rgba(0, 240, 255, 0.3); }
|
||||
100% { border-color: rgba(0, 240, 255, 0.3); box-shadow: 0 0 5px rgba(0, 240, 255, 0.1); }
|
||||
0% {
|
||||
border-color: rgba(0, 240, 255, 0.3);
|
||||
box-shadow: 0 0 5px rgba(0, 240, 255, 0.1);
|
||||
}
|
||||
|
||||
50% {
|
||||
border-color: var(--nexus-neon, #00f0ff);
|
||||
box-shadow: 0 0 25px rgba(0, 240, 255, 0.3);
|
||||
}
|
||||
|
||||
100% {
|
||||
border-color: rgba(0, 240, 255, 0.3);
|
||||
box-shadow: 0 0 5px rgba(0, 240, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Quiz Navigation Header */
|
||||
@@ -479,7 +557,8 @@ main {
|
||||
|
||||
.platform-mobile .reader-pane {
|
||||
width: 100vw !important;
|
||||
height: 100vh !important; /* full viewport height */
|
||||
height: 100vh !important;
|
||||
/* full viewport height */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -506,9 +585,11 @@ main {
|
||||
}
|
||||
|
||||
.platform-mobile .nexus-mobile-reader-tabs {
|
||||
display: none; /* Keep hidden by default */
|
||||
display: none;
|
||||
/* Keep hidden by default */
|
||||
width: 100vw;
|
||||
height: 100vh; /* full viewport height */
|
||||
height: 100vh;
|
||||
/* full viewport height */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -519,7 +600,8 @@ main {
|
||||
|
||||
.app-container.platform-mobile.active-mobile-tab-graph .nexus-mobile-reader-tabs,
|
||||
.app-container.platform-mobile.active-mobile-tab-concepts .nexus-mobile-reader-tabs {
|
||||
display: block; /* Show only when graph or concepts tabs are active */
|
||||
display: block;
|
||||
/* Show only when graph or concepts tabs are active */
|
||||
}
|
||||
|
||||
.nexus-mobile-tab-content {
|
||||
@@ -542,6 +624,7 @@ main {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
@@ -621,9 +704,18 @@ main {
|
||||
}
|
||||
|
||||
@keyframes quiz-pulse-btn-anim {
|
||||
0% { color: rgba(255, 255, 255, 0.5); }
|
||||
50% { color: #f43f5e; text-shadow: 0 0 8px rgba(244, 63, 94, 0.6); }
|
||||
100% { color: rgba(255, 255, 255, 0.5); }
|
||||
0% {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
50% {
|
||||
color: #f43f5e;
|
||||
text-shadow: 0 0 8px rgba(244, 63, 94, 0.6);
|
||||
}
|
||||
|
||||
100% {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-insight-body {
|
||||
@@ -648,3 +740,279 @@ main {
|
||||
}
|
||||
|
||||
/* Obsolescence managed: consolidated mobile toolbar and sheet styled inside respective components */
|
||||
|
||||
/* Theme-specific Overrides for Light Mode */
|
||||
.app-container.theme-light .intelligence-sidebar {
|
||||
background: #f4f1ea;
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow: -10px 0 30px rgba(139, 130, 115, 0.05);
|
||||
}
|
||||
|
||||
.app-container.theme-light .resizer {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.app-container.theme-light .resizer::before {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.app-container.theme-light .resizer::after {
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
box-shadow: 0 2px 8px rgba(139, 130, 115, 0.12);
|
||||
}
|
||||
|
||||
.app-container.theme-light .resizer:hover::before,
|
||||
.app-container.theme-light.is-resizing .resizer::before {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.app-container.theme-light .resizer:hover::after,
|
||||
.app-container.theme-light.is-resizing .resizer::after {
|
||||
background: #10b981;
|
||||
width: 6px;
|
||||
height: 80px;
|
||||
box-shadow: 0 0 12px rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
|
||||
.app-container.theme-light .intelligence-header {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
color: #292524;
|
||||
}
|
||||
|
||||
.app-container.theme-light .close-btn {
|
||||
color: #878378;
|
||||
}
|
||||
|
||||
.app-container.theme-light .close-btn:hover {
|
||||
color: #292524;
|
||||
}
|
||||
|
||||
.app-container.theme-light .visual-workspace {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.app-container.theme-light .contextual-intelligence-panel {
|
||||
background: #f4f1ea;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.app-container.theme-light .panel-header {
|
||||
background: rgba(0, 0, 0, 0.01);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.app-container.theme-light .panel-title {
|
||||
color: #78716c;
|
||||
}
|
||||
|
||||
.app-container.theme-light .no-node-selected {
|
||||
color: #878378;
|
||||
}
|
||||
|
||||
.app-container.theme-light .placeholder-glow {
|
||||
background: radial-gradient(circle, rgba(16, 185, 129, 0.15) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
.app-container.theme-light .node-header-section {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.app-container.theme-light .node-label {
|
||||
color: #292524;
|
||||
}
|
||||
|
||||
.app-container.theme-light .node-details .section-title {
|
||||
color: #78716c;
|
||||
}
|
||||
|
||||
.app-container.theme-light .neon-sub-header {
|
||||
border-left: 2px solid #10b981;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.app-container.theme-light .node-description {
|
||||
color: #292524;
|
||||
}
|
||||
|
||||
.app-container.theme-light .node-summary {
|
||||
color: #44403c;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border-left: 2px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.app-container.theme-light .key-term-item {
|
||||
color: #292524;
|
||||
}
|
||||
|
||||
.app-container.theme-light .term-bullet {
|
||||
color: #10b981;
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.app-container.theme-light .sidebar-footer {
|
||||
background: #f4f1ea;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.app-container.theme-light .open-quiz-btn {
|
||||
background: rgba(16, 185, 129, 0.03);
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
color: #10b981;
|
||||
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.05);
|
||||
}
|
||||
|
||||
.app-container.theme-light .open-quiz-btn:hover {
|
||||
background: rgba(16, 185, 129, 0.08);
|
||||
border-color: #10b981;
|
||||
color: #10b981;
|
||||
box-shadow: 0 0 20px rgba(16, 185, 129, 0.15);
|
||||
}
|
||||
|
||||
.app-container.theme-light .quiz-pulse-btn {
|
||||
animation: quiz-pulse-btn-light 2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes quiz-pulse-btn-light {
|
||||
0% {
|
||||
border-color: rgba(16, 185, 129, 0.3);
|
||||
box-shadow: 0 0 5px rgba(16, 185, 129, 0.05);
|
||||
}
|
||||
|
||||
50% {
|
||||
border-color: #10b981;
|
||||
box-shadow: 0 0 20px rgba(16, 185, 129, 0.2);
|
||||
}
|
||||
|
||||
100% {
|
||||
border-color: rgba(16, 185, 129, 0.3);
|
||||
box-shadow: 0 0 5px rgba(16, 185, 129, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.app-container.theme-light .quiz-nav {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
background: rgba(0, 0, 0, 0.01);
|
||||
}
|
||||
|
||||
.app-container.theme-light .back-to-graph-btn {
|
||||
color: #78716c;
|
||||
}
|
||||
|
||||
.app-container.theme-light .back-to-graph-btn:hover {
|
||||
color: #10b981;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.app-container.theme-light .mobile-insight-body {
|
||||
background: #f4f1ea;
|
||||
}
|
||||
|
||||
.app-container.theme-light .mobile-insight-header {
|
||||
background: #f4f1ea;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.app-container.theme-light .mobile-insight-nav {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.app-container.theme-light .mobile-insight-nav-btn {
|
||||
color: #78716c;
|
||||
}
|
||||
|
||||
.app-container.theme-light .mobile-insight-nav-btn.active {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #10b981;
|
||||
box-shadow: 0 0 10px rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
.app-container.theme-light .skeleton-line {
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, 0.03) 25%, rgba(0, 0, 0, 0.08) 50%, rgba(0, 0, 0, 0.03) 75%);
|
||||
}
|
||||
|
||||
.app-container.theme-light .clear-summary-btn {
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.app-container.theme-light .clear-summary-btn:hover {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
|
||||
/* Skeleton Loader for Selection Summary */
|
||||
.skeleton-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.skeleton-line {
|
||||
height: 0.75rem;
|
||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.03) 25%, rgba(255, 255, 255, 0.08) 50%, rgba(255, 255, 255, 0.03) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-shimmer 1.5s infinite linear;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.skeleton-line.title {
|
||||
height: 1.25rem;
|
||||
width: 60%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.skeleton-line.w-90 {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.skeleton-line.w-80 {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.skeleton-line.w-70 {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.skeleton-line.w-60 {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
@keyframes skeleton-shimmer {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-badge-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Clear Summary Button styling */
|
||||
.clear-summary-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-size: 1.25rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 0.2rem 0.4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.clear-summary-btn:hover {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ public enum MobileReaderTab
|
||||
/// <summary>
|
||||
/// Screen coordinates for text selection popup positioning.
|
||||
/// </summary>
|
||||
public record SelectionCoordinates(double Top, double Left, double Width);
|
||||
public record SelectionCoordinates(double Top, double Left, double Width, double Height, double Bottom, double ViewportWidth);
|
||||
|
||||
/// <summary>
|
||||
/// Represents a message in the KM-RAG global and mobile intelligence chat threads.
|
||||
|
||||
@@ -22,12 +22,21 @@ public sealed partial class KnowledgeCoordinator : IDisposable, IAsyncDisposable
|
||||
|
||||
public string CurrentFullPageContent { get; private set; } = string.Empty;
|
||||
|
||||
public bool IsLoadingSelectionSummary { get; private set; }
|
||||
public string? SelectionSummary { get; private set; }
|
||||
public string? SelectedTextContext { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the knowledge graph has been updated with new data.
|
||||
/// Subscribers must return a Task to enable proper async handling.
|
||||
/// </summary>
|
||||
public event Func<GraphDataDto, Task>? OnGraphUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the selection summary state has changed (loading started, finished, or cleared).
|
||||
/// </summary>
|
||||
public event Func<Task>? OnSelectionSummaryStateChanged;
|
||||
|
||||
public KnowledgeCoordinator(
|
||||
IKnowledgeService knowledgeService,
|
||||
IKnowledgeGraphService graphService,
|
||||
@@ -205,6 +214,51 @@ public sealed partial class KnowledgeCoordinator : IDisposable, IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartSelectionSummaryAsync(string text, string tenantId = "global")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text)) return;
|
||||
|
||||
IsLoadingSelectionSummary = true;
|
||||
SelectionSummary = null;
|
||||
SelectedTextContext = text;
|
||||
if (OnSelectionSummaryStateChanged != null)
|
||||
{
|
||||
await OnSelectionSummaryStateChanged.Invoke();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await RequestSummaryAndQuizAsync(text, tenantId);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
SelectionSummary = result.Value.Summary;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("[KnowledgeCoordinator] Selection summary request failed: {Errors}", string.Join(", ", result.Errors.Select(e => e.Message)));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoadingSelectionSummary = false;
|
||||
if (OnSelectionSummaryStateChanged != null)
|
||||
{
|
||||
await OnSelectionSummaryStateChanged.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ClearSelectionSummaryAsync()
|
||||
{
|
||||
SelectionSummary = null;
|
||||
SelectedTextContext = null;
|
||||
IsLoadingSelectionSummary = false;
|
||||
if (OnSelectionSummaryStateChanged != null)
|
||||
{
|
||||
await OnSelectionSummaryStateChanged.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ClearAsync()
|
||||
{
|
||||
CancelAndDisposeCts(ref _graphCts);
|
||||
@@ -213,6 +267,7 @@ public sealed partial class KnowledgeCoordinator : IDisposable, IAsyncDisposable
|
||||
CurrentFullPageContent = string.Empty;
|
||||
await _graphService.Clear();
|
||||
await _quizService.SetQuiz(null, null);
|
||||
await ClearSelectionSummaryAsync();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -3,37 +3,83 @@
|
||||
:root {
|
||||
--nexus-neon: #00ff99;
|
||||
--nexus-neon-glow: rgba(0, 255, 153, 0.3);
|
||||
--nexus-bg: #121212;
|
||||
--nexus-card: #1a1a1a;
|
||||
--nexus-bg: #121214;
|
||||
--nexus-card: #1a1a1e;
|
||||
--nexus-text: #ffffff;
|
||||
--nexus-paper: #F9F9F9;
|
||||
--nexus-font-sans: 'Inter', sans-serif;
|
||||
--nexus-font-serif: 'Merriweather', serif;
|
||||
|
||||
/* Global Semantic Theme Mapping */
|
||||
--nexus-primary: var(--nexus-neon);
|
||||
--nexus-primary-glow: var(--nexus-neon-glow);
|
||||
--nexus-primary-hover: #00e688;
|
||||
/* Global Selection Style Override */
|
||||
--nexus-selection: rgba(0, 255, 153, 0.25);
|
||||
|
||||
/* Standard Layout Tokens */
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 16px;
|
||||
--radius-xl: 20px;
|
||||
/* Graph Nodes Theme Custom Properties (Dark Mode) */
|
||||
--nexus-graph-bg: radial-gradient(circle, #1a1a1a 0%, #121212 100%);
|
||||
--nexus-graph-link-secondary: rgba(255, 255, 255, 0.2);
|
||||
--nexus-graph-link-default: rgba(255, 255, 255, 0.1);
|
||||
|
||||
/* Safe Area Insets with fallbacks */
|
||||
--safe-area-inset-top: env(safe-area-inset-top, 0px);
|
||||
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
|
||||
--safe-area-inset-left: env(safe-area-inset-left, 0px);
|
||||
--safe-area-inset-right: env(safe-area-inset-right, 0px);
|
||||
--nexus-node-pill-bg: rgba(20, 20, 20, 0.95);
|
||||
|
||||
/* Transitions */
|
||||
--nexus-transition: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--nexus-node-rule: #ff4646;
|
||||
--nexus-node-rule-bg: rgba(255, 70, 70, 0.1);
|
||||
--nexus-node-rule-text: #ff8b8b;
|
||||
|
||||
--nexus-node-definition: #ffb03a;
|
||||
--nexus-node-definition-bg: rgba(255, 176, 58, 0.1);
|
||||
--nexus-node-definition-text: #ffd18c;
|
||||
|
||||
--nexus-node-table: #d946ef;
|
||||
--nexus-node-table-bg: rgba(217, 70, 239, 0.1);
|
||||
--nexus-node-table-text: #f5d0fe;
|
||||
|
||||
--nexus-node-section: #3b82f6;
|
||||
--nexus-node-section-bg: rgba(59, 130, 246, 0.1);
|
||||
--nexus-node-section-text: #93c5fd;
|
||||
|
||||
--nexus-node-bridge: #06b6d4;
|
||||
--nexus-node-bridge-bg: rgba(6, 182, 212, 0.1);
|
||||
--nexus-node-bridge-text: #67e8f9;
|
||||
|
||||
--nexus-node-current: var(--nexus-neon);
|
||||
--nexus-node-current-bg: rgba(0, 255, 153, 0.15);
|
||||
--nexus-node-current-text: #ffffff;
|
||||
|
||||
--nexus-node-concept: #00d2c4;
|
||||
--nexus-node-concept-bg: rgba(0, 210, 196, 0.05);
|
||||
--nexus-node-concept-text: #e0e0e0;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: var(--nexus-selection);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
|
||||
/* Global Semantic Theme Mapping */
|
||||
--nexus-primary: var(--nexus-neon);
|
||||
--nexus-primary-glow: var(--nexus-neon-glow);
|
||||
--nexus-primary-hover: #00e688;
|
||||
|
||||
/* Standard Layout Tokens */
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 16px;
|
||||
--radius-xl: 20px;
|
||||
|
||||
/* Safe Area Insets with fallbacks */
|
||||
--safe-area-inset-top: env(safe-area-inset-top, 0px);
|
||||
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
|
||||
--safe-area-inset-left: env(safe-area-inset-left, 0px);
|
||||
--safe-area-inset-right: env(safe-area-inset-right, 0px);
|
||||
|
||||
/* Transitions */
|
||||
--nexus-transition: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Global Glassmorphism with Fallback */
|
||||
.glass-panel {
|
||||
background: rgba(20, 20, 20, 0.85); /* Darker fallback for readability */
|
||||
background: rgba(20, 20, 20, 0.85);
|
||||
/* Darker fallback for readability */
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: 1.5rem;
|
||||
@@ -61,32 +107,157 @@
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-nexus-primary {
|
||||
background: var(--nexus-neon);
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.btn-nexus-secondary {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-nexus:hover {
|
||||
transform: translateY(-2px);
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.btn-nexus-primary:hover {
|
||||
box-shadow: 0 4px 15px var(--nexus-primary-glow);
|
||||
}
|
||||
|
||||
.btn-nexus-secondary:hover {
|
||||
box-shadow: 0 4px 15px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
|
||||
.theme-light {
|
||||
--nexus-bg: var(--nexus-paper);
|
||||
--nexus-bg: #f4f1ea;
|
||||
--nexus-card: #ffffff;
|
||||
--nexus-text: #121212;
|
||||
--nexus-text: #2d2a26;
|
||||
--nexus-selection: rgba(16, 185, 129, 0.18);
|
||||
|
||||
/* Graph Nodes Theme Custom Properties (Light Mode) */
|
||||
--nexus-graph-bg: radial-gradient(circle, #ffffff 0%, #e8e4da 100%);
|
||||
--nexus-graph-link-secondary: rgba(0, 0, 0, 0.15);
|
||||
--nexus-graph-link-default: rgba(0, 0, 0, 0.08);
|
||||
|
||||
--nexus-node-pill-bg: #fbfafa;
|
||||
|
||||
--nexus-node-rule: #dc2626;
|
||||
--nexus-node-rule-bg: rgba(220, 38, 38, 0.05);
|
||||
--nexus-node-rule-text: #991b1b;
|
||||
|
||||
--nexus-node-definition: #d97706;
|
||||
--nexus-node-definition-bg: rgba(217, 119, 6, 0.05);
|
||||
--nexus-node-definition-text: #92400e;
|
||||
|
||||
--nexus-node-table: #c084fc;
|
||||
--nexus-node-table-bg: rgba(192, 132, 252, 0.05);
|
||||
--nexus-node-table-text: #6b21a8;
|
||||
|
||||
--nexus-node-section: #2563eb;
|
||||
--nexus-node-section-bg: rgba(37, 99, 235, 0.05);
|
||||
--nexus-node-section-text: #1e3a8a;
|
||||
|
||||
--nexus-node-bridge: #0891b2;
|
||||
--nexus-node-bridge-bg: rgba(8, 145, 178, 0.05);
|
||||
--nexus-node-bridge-text: #155e75;
|
||||
|
||||
--nexus-node-current: #10b981;
|
||||
--nexus-node-current-bg: rgba(16, 185, 129, 0.08);
|
||||
--nexus-node-current-text: #064e3b;
|
||||
|
||||
--nexus-node-concept: #0d9488;
|
||||
--nexus-node-concept-bg: rgba(13, 148, 136, 0.03);
|
||||
--nexus-node-concept-text: #115e59;
|
||||
|
||||
--nexus-accent: #10b981;
|
||||
}
|
||||
.theme-light .knowledge-graph-container svg {
|
||||
background: radial-gradient(circle, #ffffff 0%, #e8e4da 100%) !important;
|
||||
}
|
||||
|
||||
.theme-light .graph-controls {
|
||||
background: rgba(254, 254, 253, 0.4) !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08) !important;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.04) !important;
|
||||
}
|
||||
|
||||
.theme-light .zoom-btn {
|
||||
background: rgba(0, 0, 0, 0.02) !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06) !important;
|
||||
color: #78716c !important;
|
||||
}
|
||||
|
||||
.theme-light .zoom-btn:hover {
|
||||
background: rgba(16, 185, 129, 0.05) !important;
|
||||
color: #10b981 !important;
|
||||
border-color: #10b981 !important;
|
||||
}
|
||||
|
||||
.theme-light .loading-state {
|
||||
color: #292524 !important;
|
||||
background: rgba(254, 254, 254, 0.85) !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05) !important;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.05) !important;
|
||||
}
|
||||
|
||||
.theme-light .neon-pulse {
|
||||
color: #10b981 !important;
|
||||
filter: none !important;
|
||||
animation: robot-pulse-light 2s infinite ease-in-out !important;
|
||||
}
|
||||
|
||||
.theme-light .scan-line {
|
||||
background: #10b981 !important;
|
||||
box-shadow: 0 0 10px rgba(16, 185, 129, 0.5) !important;
|
||||
}
|
||||
|
||||
.theme-light .nexus-node-active {
|
||||
stroke: #10b981 !important;
|
||||
filter: drop-shadow(0 0 8px rgba(16, 185, 129, 0.2)) !important;
|
||||
}
|
||||
|
||||
@keyframes robot-pulse-light {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.theme-light ::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.theme-light ::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.theme-light .glass-panel {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
@supports (backdrop-filter: blur(10px)) {
|
||||
.theme-light .glass-panel {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
}
|
||||
|
||||
.theme-light .btn-nexus-secondary {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
color: #292524;
|
||||
}
|
||||
|
||||
.theme-light .btn-nexus-secondary:hover {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
|
||||
|
||||
* {
|
||||
@@ -133,14 +304,14 @@ body {
|
||||
|
||||
|
||||
/* Platform Specific Tweaks */
|
||||
.platform-mobile .nexus-button {
|
||||
.platform-mobile .nexus-btn {
|
||||
min-height: var(--touch-target-size);
|
||||
min-width: var(--touch-target-size);
|
||||
font-size: 1.1rem;
|
||||
padding: 12px 24px;
|
||||
}
|
||||
|
||||
.platform-desktop .nexus-button {
|
||||
.platform-desktop .nexus-btn {
|
||||
min-height: 36px;
|
||||
font-size: 0.9rem;
|
||||
padding: 8px 16px;
|
||||
@@ -164,7 +335,8 @@ h1:focus {
|
||||
}
|
||||
|
||||
/* Preloader Styles */
|
||||
#app-preloader, .app-preloader {
|
||||
#app-preloader,
|
||||
.app-preloader {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -206,12 +378,25 @@ h1:focus {
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(0.95); }
|
||||
}
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
@@ -40,70 +40,70 @@ const getCategoryStyle = d => {
|
||||
// 1. Rule (red/coral)
|
||||
if (type === 'rule') {
|
||||
return {
|
||||
color: '#ff4646',
|
||||
fill: 'rgba(255, 70, 70, 0.1)',
|
||||
color: 'var(--nexus-node-rule, #ff4646)',
|
||||
fill: 'var(--nexus-node-rule-bg, rgba(255, 70, 70, 0.1))',
|
||||
opacity: 0.8,
|
||||
glowKey: 'rule',
|
||||
textColor: '#ff8b8b'
|
||||
textColor: 'var(--nexus-node-rule-text, #ff8b8b)'
|
||||
};
|
||||
}
|
||||
// 2. Definition (gold/amber)
|
||||
if (type === 'definition') {
|
||||
return {
|
||||
color: '#ffb03a',
|
||||
fill: 'rgba(255, 176, 58, 0.1)',
|
||||
color: 'var(--nexus-node-definition, #ffb03a)',
|
||||
fill: 'var(--nexus-node-definition-bg, rgba(255, 176, 58, 0.1))',
|
||||
opacity: 0.8,
|
||||
glowKey: 'definition',
|
||||
textColor: '#ffd18c'
|
||||
textColor: 'var(--nexus-node-definition-text, #ffd18c)'
|
||||
};
|
||||
}
|
||||
// 3. Table (purple/magenta)
|
||||
if (type === 'table') {
|
||||
return {
|
||||
color: '#d946ef',
|
||||
fill: 'rgba(217, 70, 239, 0.1)',
|
||||
color: 'var(--nexus-node-table, #d946ef)',
|
||||
fill: 'var(--nexus-node-table-bg, rgba(217, 70, 239, 0.1))',
|
||||
opacity: 0.8,
|
||||
glowKey: 'table',
|
||||
textColor: '#f5d0fe'
|
||||
textColor: 'var(--nexus-node-table-text, #f5d0fe)'
|
||||
};
|
||||
}
|
||||
// 4. Section (blue/indigo)
|
||||
if (type === 'section') {
|
||||
return {
|
||||
color: '#3b82f6',
|
||||
fill: 'rgba(59, 130, 246, 0.1)',
|
||||
color: 'var(--nexus-node-section, #3b82f6)',
|
||||
fill: 'var(--nexus-node-section-bg, rgba(59, 130, 246, 0.1))',
|
||||
opacity: 0.8,
|
||||
glowKey: 'section',
|
||||
textColor: '#93c5fd'
|
||||
textColor: 'var(--nexus-node-section-text, #93c5fd)'
|
||||
};
|
||||
}
|
||||
// 5. Bridge (cyan/comparison)
|
||||
if (group === 'bridge') {
|
||||
return {
|
||||
color: '#06b6d4',
|
||||
fill: 'rgba(6, 182, 212, 0.1)',
|
||||
color: 'var(--nexus-node-bridge, #06b6d4)',
|
||||
fill: 'var(--nexus-node-bridge-bg, rgba(6, 182, 212, 0.1))',
|
||||
opacity: 0.7,
|
||||
glowKey: 'bridge',
|
||||
textColor: '#67e8f9'
|
||||
textColor: 'var(--nexus-node-bridge-text, #67e8f9)'
|
||||
};
|
||||
}
|
||||
// 6. Current (active/focus landmark - neon green)
|
||||
if (group === 'current') {
|
||||
return {
|
||||
color: 'var(--nexus-neon)',
|
||||
fill: 'rgba(0, 255, 153, 0.15)',
|
||||
color: 'var(--nexus-node-current, var(--nexus-neon))',
|
||||
fill: 'var(--nexus-node-current-bg, rgba(0, 255, 153, 0.15))',
|
||||
opacity: 0.9,
|
||||
glowKey: 'current',
|
||||
textColor: '#ffffff'
|
||||
textColor: 'var(--nexus-node-current-text, #ffffff)'
|
||||
};
|
||||
}
|
||||
// 7. Concept / Default (subtle cool steel blue/teal)
|
||||
return {
|
||||
color: '#00d2c4',
|
||||
fill: 'rgba(0, 210, 196, 0.05)',
|
||||
color: 'var(--nexus-node-concept, #00d2c4)',
|
||||
fill: 'var(--nexus-node-concept-bg, rgba(0, 210, 196, 0.05))',
|
||||
opacity: 0.4,
|
||||
glowKey: 'concept',
|
||||
textColor: '#e0e0e0'
|
||||
textColor: 'var(--nexus-node-concept-text, #e0e0e0)'
|
||||
};
|
||||
};
|
||||
|
||||
@@ -132,7 +132,7 @@ const getNodeGlyph = d => {
|
||||
function updateNodeAppearances() {
|
||||
if (!node) return;
|
||||
|
||||
node.each(function(d) {
|
||||
node.each(function (d) {
|
||||
const g = d3.select(this);
|
||||
const rect = g.select(".node-pill");
|
||||
const text = g.select("text");
|
||||
@@ -208,7 +208,7 @@ export function mount(containerId, data, dotNetHelper) {
|
||||
.attr("viewBox", [0, 0, width, height])
|
||||
.attr("width", "100%")
|
||||
.attr("height", "100%")
|
||||
.style("background", "radial-gradient(circle, #1a1a1a 0%, #121212 100%)");
|
||||
.style("background", "var(--nexus-graph-bg, radial-gradient(circle, #1a1a1a 0%, #121212 100%))");
|
||||
|
||||
// Radial gradients for Nebula effects
|
||||
const defs = svgElement.append("defs");
|
||||
@@ -223,13 +223,13 @@ export function mount(containerId, data, dotNetHelper) {
|
||||
radialGradient.append("stop").attr("offset", "100%").attr("stop-color", "var(--nexus-neon)").attr("stop-opacity", 0);
|
||||
|
||||
const colors = {
|
||||
'rule': '#ff4646',
|
||||
'definition': '#ffb03a',
|
||||
'table': '#d946ef',
|
||||
'section': '#3b82f6',
|
||||
'bridge': '#06b6d4',
|
||||
'current': 'var(--nexus-neon)',
|
||||
'concept': '#00d2c4'
|
||||
'rule': 'var(--nexus-node-rule, #ff4646)',
|
||||
'definition': 'var(--nexus-node-definition, #ffb03a)',
|
||||
'table': 'var(--nexus-node-table, #d946ef)',
|
||||
'section': 'var(--nexus-node-section, #3b82f6)',
|
||||
'bridge': 'var(--nexus-node-bridge, #06b6d4)',
|
||||
'current': 'var(--nexus-node-current, var(--nexus-neon))',
|
||||
'concept': 'var(--nexus-node-concept, #00d2c4)'
|
||||
};
|
||||
|
||||
Object.entries(colors).forEach(([key, color]) => {
|
||||
@@ -377,9 +377,9 @@ export function updateData(data) {
|
||||
enter => enter.append("path")
|
||||
.attr("stroke", d => {
|
||||
if (d.type === 'Defines' || d.type === 'maps_to') return 'var(--nexus-accent, #00ffaa)';
|
||||
if (d.type === 'Next' || d.type === 'relates_to') return 'rgba(255,255,255,0.2)';
|
||||
if (d.type === 'Next' || d.type === 'relates_to') return 'var(--nexus-graph-link-secondary, rgba(255,255,255,0.2))';
|
||||
if (d.type === 'Contains' || d.type === 'contains') return 'var(--nexus-neon)';
|
||||
return 'rgba(255,255,255,0.1)';
|
||||
return 'var(--nexus-graph-link-default, rgba(255,255,255,0.1))';
|
||||
})
|
||||
.attr("fill", "none")
|
||||
.attr("stroke-width", d => (d.type === 'Defines' || d.type === 'maps_to') ? 2 : 1)
|
||||
@@ -413,7 +413,7 @@ export function updateData(data) {
|
||||
|
||||
g.append("rect")
|
||||
.attr("class", "node-pill")
|
||||
.attr("fill", "rgba(20, 20, 20, 0.95)")
|
||||
.attr("fill", "var(--nexus-node-pill-bg, rgba(20, 20, 20, 0.95))")
|
||||
.attr("stroke", d => getCategoryStyle(d).color)
|
||||
.attr("stroke-width", d => getNodeGroup(d) === 'current' ? 2 : 1.2);
|
||||
|
||||
|
||||
@@ -1,8 +1,71 @@
|
||||
export function positionToolbar() {
|
||||
const toolbarElement = document.querySelector('.selection-ai-panel');
|
||||
if (!toolbarElement) return;
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (selection.isCollapsed) return;
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
const rects = range.getClientRects();
|
||||
if (!rects || rects.length === 0) return;
|
||||
|
||||
const firstRect = rects[0];
|
||||
const combinedRect = range.getBoundingClientRect();
|
||||
|
||||
// Find the canvas container (which is the positioned parent)
|
||||
const canvasElement = document.querySelector('.reader-canvas');
|
||||
let canvasRect = { top: 0, left: 0 };
|
||||
let scrollTop = 0;
|
||||
let scrollLeft = 0;
|
||||
|
||||
if (canvasElement) {
|
||||
canvasRect = canvasElement.getBoundingClientRect();
|
||||
scrollTop = canvasElement.scrollTop;
|
||||
scrollLeft = canvasElement.scrollLeft;
|
||||
}
|
||||
|
||||
const toolbarWidth = toolbarElement.offsetWidth;
|
||||
const toolbarHeight = toolbarElement.offsetHeight;
|
||||
|
||||
// Oblicz środek zaznaczenia w poziomie
|
||||
const left = (combinedRect.left - canvasRect.left) + scrollLeft + (combinedRect.width / 2) - (toolbarWidth / 2);
|
||||
|
||||
// Warunek brzegowy (Top Screen Fallback)
|
||||
const relativeTop = firstRect.top - toolbarHeight - 14;
|
||||
|
||||
let top;
|
||||
let below = false;
|
||||
if (relativeTop < 0) {
|
||||
// Pozwól wskoczyć POD zaznaczony tekst
|
||||
top = (combinedRect.bottom - canvasRect.top) + scrollTop + 12;
|
||||
below = true;
|
||||
toolbarElement.classList.add('below');
|
||||
} else {
|
||||
top = (firstRect.top - canvasRect.top) + scrollTop - toolbarHeight - 14;
|
||||
toolbarElement.classList.remove('below');
|
||||
}
|
||||
|
||||
toolbarElement.style.left = `${left}px`;
|
||||
toolbarElement.style.top = `${top}px`;
|
||||
|
||||
return {
|
||||
left: left,
|
||||
top: top,
|
||||
below: below
|
||||
};
|
||||
}
|
||||
let currentHandleSelection = null;
|
||||
let currentMouseUpHandler = null;
|
||||
let currentContainer = null;
|
||||
|
||||
export function initSelectionListener(dotNetHelper, container) {
|
||||
if (!container) return;
|
||||
|
||||
console.log("[SelectionHandler] Initializing...");
|
||||
|
||||
// Clean up any existing listeners first
|
||||
destroySelectionListener();
|
||||
|
||||
const handleSelection = () => {
|
||||
const selection = window.getSelection();
|
||||
const text = selection.toString().trim();
|
||||
@@ -17,25 +80,59 @@ export function initSelectionListener(dotNetHelper, container) {
|
||||
const blockNode = node.closest('[id]');
|
||||
|
||||
if (blockNode) {
|
||||
const rect = range.getBoundingClientRect();
|
||||
const rects = range.getClientRects();
|
||||
const firstRect = rects && rects.length > 0 ? rects[0] : null;
|
||||
const lastRect = rects && rects.length > 0 ? rects[rects.length - 1] : null;
|
||||
const combinedRect = range.getBoundingClientRect();
|
||||
|
||||
console.log("[SelectionHandler] Selection at screen coords:", rect.top, rect.left);
|
||||
const topVal = firstRect ? firstRect.top : combinedRect.top;
|
||||
const bottomVal = lastRect ? lastRect.bottom : combinedRect.bottom;
|
||||
|
||||
console.log("[SelectionHandler] Selection coords (first/last/combined):", topVal, bottomVal, combinedRect.left);
|
||||
|
||||
dotNetHelper.invokeMethodAsync('HandleTextSelected',
|
||||
text,
|
||||
blockNode.id,
|
||||
{
|
||||
Top: rect.top,
|
||||
Left: rect.left,
|
||||
Width: rect.width
|
||||
Top: topVal,
|
||||
Left: combinedRect.left,
|
||||
Width: combinedRect.width,
|
||||
Height: combinedRect.height,
|
||||
Bottom: bottomVal,
|
||||
ViewportWidth: window.innerWidth
|
||||
});
|
||||
|
||||
// Reposition the toolbar if already present
|
||||
setTimeout(positionToolbar, 0);
|
||||
}
|
||||
} else {
|
||||
dotNetHelper.invokeMethodAsync('HandleSelectionCleared');
|
||||
}
|
||||
};
|
||||
|
||||
// Use multiple triggers for maximum reliability
|
||||
document.addEventListener('selectionchange', handleSelection);
|
||||
container.addEventListener('mouseup', () => setTimeout(handleSelection, 10));
|
||||
const mouseUpHandler = () => setTimeout(handleSelection, 10);
|
||||
|
||||
currentHandleSelection = handleSelection;
|
||||
currentMouseUpHandler = mouseUpHandler;
|
||||
currentContainer = container;
|
||||
|
||||
document.addEventListener('selectionchange', currentHandleSelection);
|
||||
currentContainer.addEventListener('mouseup', currentMouseUpHandler);
|
||||
}
|
||||
|
||||
export function destroySelectionListener() {
|
||||
if (currentHandleSelection) {
|
||||
document.removeEventListener('selectionchange', currentHandleSelection);
|
||||
currentHandleSelection = null;
|
||||
}
|
||||
if (currentMouseUpHandler && currentContainer) {
|
||||
currentContainer.removeEventListener('mouseup', currentMouseUpHandler);
|
||||
currentMouseUpHandler = null;
|
||||
currentContainer = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getSelectionText() {
|
||||
return window.getSelection().toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -204,7 +204,8 @@ using (var scope = app.Services.CreateScope())
|
||||
logger.LogInformation("Próba połączenia z bazą danych (próba {Attempt}/{MaxRetries})...", i + 1, maxRetries);
|
||||
}
|
||||
|
||||
await dbContext.Database.MigrateAsync();
|
||||
|
||||
|
||||
await DbInitializer.SeedAsync(services);
|
||||
await TriggerBackgroundProcessingForUnindexedBooksAsync(services);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user