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:
2026-06-05 09:51:29 +00:00
committed by Marek Jaisński
parent 081c6f7940
commit f18663426b
24 changed files with 2022 additions and 571 deletions
+10
View File
@@ -36,3 +36,13 @@ Run test suite:
```bash ```bash
dotnet test --no-restore 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
```
@@ -2,106 +2,129 @@
@switch (Name.ToLowerInvariant()) @switch (Name.ToLowerInvariant())
{ {
case "home": case "home":
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<polyline points="9 22 9 12 15 12 15 22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <polyline points="9 22 9 12 15 12 15 22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break; break;
case "map": case "map":
<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="8" y1="2" x2="8" y2="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <line x1="8" y1="2" x2="8" y2="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="16" y1="6" x2="16" y2="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <line x1="16" y1="6" x2="16" y2="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break; break;
case "share": case "share":
case "share-2": case "share-2":
<circle cx="18" cy="5" r="3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <circle cx="18" cy="5" r="3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<circle cx="6" cy="12" r="3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <circle cx="6" cy="12" r="3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<circle cx="18" cy="19" r="3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <circle cx="18" cy="19" r="3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <line x1="15.41" y1="6.51" x2="8.59" y2="10.49" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break; break;
case "help-circle": case "help-circle":
<circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="12" y1="17" x2="12.01" y2="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <line x1="12" y1="17" x2="12.01" y2="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break; break;
case "robot": case "robot":
<path d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h5a2 2 0 0 1 2 2v2a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2V9c0-1.1.9-2 2-2h5V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2zM8 11v4h8v-4H8zm-2 0H4v4h2v-4zm14 0h-2v4h2v-4z" /> <path d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h5a2 2 0 0 1 2 2v2a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2V9c0-1.1.9-2 2-2h5V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2zM8 11v4h8v-4H8zm-2 0H4v4h2v-4zm14 0h-2v4h2v-4z" />
break; break;
case "play": case "play":
<path d="M8 5v14l11-7z" /> <path d="M8 5v14l11-7z" />
break; break;
case "check": case "check":
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" /> <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
break; break;
case "search": 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; break;
case "message-square": 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; break;
case "diamond": case "diamond":
<path d="M12 3L3 12L12 21L21 12L12 3Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <path d="M12 3L3 12L12 21L21 12L12 3Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break; break;
case "layout": case "layout":
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <rect width="18" height="18" x="3" y="3" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="3" y1="9" x2="21" y2="9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <line x1="3" y1="9" x2="21" y2="9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<line x1="9" y1="21" x2="9" y2="9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <line x1="9" y1="21" x2="9" y2="9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break; break;
case "book": case "book":
case "book-open": case "book-open":
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break; break;
case "user": case "user":
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<circle cx="12" cy="7" r="4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <circle cx="12" cy="7" r="4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break; break;
case "settings": 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; break;
case "bookmark": 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; break;
case "target": 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; break;
case "trash": 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; break;
case "mail": 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; break;
case "lock": 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; break;
case "eye": 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; break;
case "eye-off": 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; break;
case "arrow-left": 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; break;
case "arrow-right": 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; break;
case "log-out": 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" /> <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" />
break; break;
case "chevron-left": case "chevron-left":
<polyline points="15 18 9 12 15 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <polyline points="15 18 9 12 15 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break; break;
case "chevron-right": case "chevron-right":
<polyline points="9 18 15 12 9 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <polyline points="9 18 15 12 9 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break; break;
case "x": case "x":
case "close": case "close":
<line x1="18" y1="6" x2="6" y2="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <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" /> <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; break;
default: default:
<!-- Fallback circle --> <!-- Fallback circle -->
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />
break; break;
} }
</svg> </svg>

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.UI.Shared.Services
@using NexusReader.Application.Abstractions.Services @using NexusReader.Application.Abstractions.Services
@inject IFocusModeService FocusMode @inject IFocusModeService FocusMode
@inject IKnowledgeService KnowledgeService
@inject IIdentityService IdentityService @inject IIdentityService IdentityService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IThemeService ThemeService
@inject IKnowledgeService KnowledgeService
@implements IDisposable
<aside class="intelligence-toolbar"> <aside class="intelligence-toolbar">
<div class="toolbar-top"> <div class="toolbar-top">
<button class="toolbar-item" @onclick='() => NavigationManager.NavigateTo("/")' title="Back to Dashboard"> <button class="toolbar-item" @onclick='() => NavigationManager.NavigateTo("/")' title="Back to Dashboard">
<NexusIcon Name="arrow-left" Size="20" /> <NexusIcon Name="arrow-left" Size="20" />
</button> </button>
<button class="toolbar-item active" title="Chat">
<NexusIcon Name="message-square" Size="20" /> @if (FocusMode.IsFocusModeActive)
</button> {
</div> <button class="toolbar-item active" @onclick="FocusMode.ToggleAsync" title="Focus Mode Active (Click to Exit)">
<NexusIcon Name="target" Size="20" />
<div class="toolbar-middle"> </button>
<button class="toolbar-item" title="Settings"> }
<NexusIcon Name="settings" Size="20" /> else
</button> {
<button class="toolbar-item" title="Bookmarks"> <button class="toolbar-item active" @onclick="FocusMode.ToggleAsync" title="Chat Active (Click to Focus)">
<NexusIcon Name="bookmark" Size="20" /> <NexusIcon Name="message-square" Size="20" />
</button> </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>
<div class="toolbar-bottom"> <div class="toolbar-bottom">
<button class="toolbar-item @(FocusMode.IsFocusModeActive ? "active focus-active" : "")" <div class="toolbar-separator"></div>
@onclick="FocusMode.ToggleAsync" title="Focus Mode (F)">
<NexusIcon Name="target" Size="20" /> <button class="toolbar-item" @onclick="ThemeService.ToggleTheme" title="Przełącz motyw">
<NexusIcon Name="@(ThemeService.IsLightMode ? "sun" : "moon")" Size="20" />
</button> </button>
<button class="toolbar-item" @onclick='() => NavigationManager.NavigateTo("/")' title="Global Hub">
<NexusIcon Name="layers" Size="20" /> <div class="toolbar-separator"></div>
</button>
<button class="toolbar-item logout-item" @onclick="HandleLogout" title="Exit"> <button class="toolbar-item clear-cache-item" @onclick="HandleClearCache" title="Wyczyść pamięć AI">
<NexusIcon Name="log-out" Size="20" /> <NexusIcon Name="trash" Size="20" />
</button> </button>
</div> </div>
</aside> </aside>
@@ -48,11 +46,11 @@
protected override void OnInitialized() protected override void OnInitialized()
{ {
FocusMode.OnFocusModeChanged += HandleUpdate; FocusMode.OnFocusModeChanged += HandleUpdate;
ThemeService.OnThemeChanged += HandleThemeChangedAsync;
} }
private async Task HandleClearCache() private async Task HandleClearCache()
{ {
// For now, a simple console log confirm or just do it
Console.WriteLine("[IntelligenceToolbar] Requesting cache clear..."); Console.WriteLine("[IntelligenceToolbar] Requesting cache clear...");
var result = await KnowledgeService.ClearCacheAsync(); var result = await KnowledgeService.ClearCacheAsync();
if (result.IsSuccess) 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 HandleUpdate() => InvokeAsync(StateHasChanged);
private Task HandleThemeChangedAsync() => InvokeAsync(StateHasChanged);
public void Dispose() public void Dispose()
{ {
FocusMode.OnFocusModeChanged -= HandleUpdate; FocusMode.OnFocusModeChanged -= HandleUpdate;
ThemeService.OnThemeChanged -= HandleThemeChangedAsync;
} }
} }
@@ -71,26 +71,53 @@
transform: rotate(180deg); 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 { /* Light mode overrides */
margin-top: 1rem; .theme-light .intelligence-toolbar {
border-top: 1px solid rgba(255, 255, 255, 0.08); background: #f5f5f4;
padding-top: 1.5rem; border-right: 1px solid rgba(0, 0, 0, 0.08);
height: auto; box-shadow: inset -2px 0 10px rgba(0, 0, 0, 0.02);
width: 100%;
display: flex;
justify-content: center;
border-radius: 0;
color: #444;
} }
.toolbar-item.logout-item:hover { .theme-light .toolbar-item {
color: #ff4d4d; color: #78716c;
background: none;
filter: drop-shadow(0 0 8px rgba(255, 77, 77, 0.4));
} }
.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; 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 @using NexusReader.Application.DTOs.AI
@inject KnowledgeCoordinator Coordinator @inject KnowledgeCoordinator Coordinator
@inject IReaderInteractionService InteractionService @inject IReaderInteractionService InteractionService
@inject IQuizStateService QuizService
@inject IJSRuntime JS
@if (IsVisible) @if (IsVisible)
{ {
<div class="selection-ai-panel expanded @(PositionBelow ? "below" : "")" style="@PanelStyle"> <div class="selection-ai-panel @(_positionBelow ? "below" : "")" style="@_style">
<div class="ai-bubble"> <button id="summary-btn" class="toolbar-btn primary @(IsLoadingSummary ? "loading" : "") @(IsAnyLoading ? "disabled cursor-not-allowed opacity-50" : "")"
<div class="ai-avatar"> disabled="@IsAnyLoading"
<div class="avatar-ring"></div> @onclick="RequestSummaryAsync">
<NexusIcon Name="robot" Size="48" Class="neon-pulse" /> @if (IsLoadingSummary)
<div class="avatar-label"> {
<span class="name">E-Czytnik</span> <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;">
<span class="role">Asystent AI</span> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" style="opacity: 0.25;"></circle>
</div> <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>
</div> </svg>
<div class="ai-content"> <span class="btn-text">Podsumowywanie...</span>
@if (IsLoading) }
{ else
<div class="loading-state"> {
<div class="shimmer">Skanowanie fragmentu...</div> <NexusIcon Name="book-open" Size="14" Class="btn-icon" />
</div> <span class="btn-text">Podsumuj</span>
} }
else if (Packet != null) </button>
{ <div class="toolbar-divider"></div>
<div class="summary-box"> <button id="quiz-btn" class="toolbar-btn secondary @(IsLoadingQuiz ? "loading" : "") @(IsAnyLoading ? "disabled cursor-not-allowed opacity-50" : "")"
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">@Packet.Summary</NexusTypography> disabled="@IsAnyLoading"
</div> @onclick="GenerateQuizAsync">
<div class="ai-actions"> @if (IsLoadingQuiz)
<button class="action-btn neon-border" @onclick="GenerateFullQuiz">Generuj Quiz dla całej strony</button> {
<button class="action-btn ghost" @onclick="CloseAsync">Zamknij</button> <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;">
</div> <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>
else </svg>
{ <span class="btn-text">Generowanie...</span>
<div class="summary-box"> }
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">Wykryto ciekawy fragment! Czy chcesz, abym wygenerował podsumowanie lub quiz z tego rozdziału?</NexusTypography> else
</div> {
<div class="ai-actions"> <NexusIcon Name="target" Size="14" Class="btn-icon" />
<button class="action-btn neon-border" @onclick="RequestSummary">Podsumuj zaznaczenie</button> <span class="btn-text">Quiz</span>
<button class="action-btn ghost" @onclick="CloseAsync">Pomiń</button> }
</div> </button>
}
</div>
<div class="bubble-pointer"></div>
</div>
</div> </div>
} }
@@ -56,47 +54,145 @@
[Parameter] public string FullPageContent { get; set; } = string.Empty; [Parameter] public string FullPageContent { get; set; } = string.Empty;
private bool IsVisible => !string.IsNullOrEmpty(SelectedText) && Coordinates != null; private bool IsVisible => !string.IsNullOrEmpty(SelectedText) && Coordinates != null;
private bool IsLoading = false; private bool IsLoadingSummary = false;
private KnowledgePacket? Packet; private bool IsLoadingQuiz = false;
private bool PositionBelow => Coordinates != null && Coordinates.Top < 320; 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() protected override void OnParametersSet()
{ {
Console.WriteLine($"[SelectionAiPanel] Parameters set. SelectedText: {SelectedText.Length} chars, Coordinates: {Coordinates?.Top}, PositionBelow: {PositionBelow}"); Console.WriteLine($"[SelectionAiPanel] Parameters set. SelectedText: {SelectedText.Length} chars, Coordinates: {Coordinates?.Top}");
// Reset packet when selection changes
Packet = null;
}
private string PanelStyle => Coordinates != null
? string.Create(System.Globalization.CultureInfo.InvariantCulture,
$"top: {(PositionBelow ? Coordinates.Top + 35 : Coordinates.Top - 15):F1}px !important; " +
$"left: {Math.Clamp(Coordinates.Left + Coordinates.Width / 2, 280, 1600):F1}px !important; " +
$"transform: translate(-50%, {(PositionBelow ? "0" : "-100%")}) !important;")
: "";
private async Task RequestSummary()
{
IsLoading = true;
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 (Coordinates != _lastCoordinates)
Packet = result.IsSuccess ? result.Value : null; {
IsLoading = false; _lastCoordinates = Coordinates;
_style = "visibility: hidden; opacity: 0; pointer-events: none;";
_positionBelow = false;
}
// Reset loading states when parameters change
IsLoadingSummary = false;
IsLoadingQuiz = false;
} }
private async Task GenerateFullQuiz() protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
IsLoading = true; if (IsVisible && _style.Contains("visibility: hidden"))
await Coordinator.RequestSummaryAndQuizAsync(FullPageContent); {
IsLoading = false; try
await CloseAsync(); {
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))
{
var contextPrompt = !string.IsNullOrWhiteSpace(FullPageContent)
? $"ANALYSIS CONTEXT (Full Page Content):\n{FullPageContent}\n\nUSER SELECTION TO SUMMARIZE:\n"
: "";
_ = 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 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)
{
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() private async Task CloseAsync()
{ {
Packet = null;
await InteractionService.NotifyTextSelected(string.Empty, string.Empty, 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 { .selection-ai-panel {
position: fixed; position: absolute;
z-index: 9999; z-index: 10000;
width: 550px; display: flex;
max-width: 90vw; align-items: center;
animation: fadeInScale 0.2s ease-out; background: rgba(24, 24, 28, 0.85);
pointer-events: auto; 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 { @keyframes fadeInScale {
from { opacity: 0; transform: translate(-50%, -90%) scale(0.95); } from {
to { opacity: 1; transform: translate(-50%, -100%) scale(1); } opacity: 0;
transform: scale(0.96);
}
to {
opacity: 1;
transform: scale(1);
}
} }
.ai-bubble { .selection-ai-panel.below {
position: relative; animation: fadeInScaleBelow 0.18s cubic-bezier(0.16, 1, 0.3, 1);
display: flex;
flex-direction: row;
gap: 1.5rem;
padding: 1.5rem;
background: rgba(18, 18, 18, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
color: #fff;
} }
.ai-avatar { @keyframes fadeInScaleBelow {
from {
opacity: 0;
transform: scale(0.96);
}
to {
opacity: 1;
transform: scale(1);
}
}
.toolbar-btn {
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
gap: 0.5rem; gap: 6px;
min-width: 100px; padding: 6px 12px;
} background: transparent;
border: none;
.avatar-label { border-radius: 6px;
display: flex; color: #e4e4e7; /* zinc-200 */
flex-direction: column;
align-items: center;
text-align: center;
}
.avatar-label .name {
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 600; font-weight: 500;
color: #fff;
}
.avatar-label .role {
font-size: 0.7rem;
opacity: 0.6;
}
.neon-pulse {
color: #00ff99;
filter: drop-shadow(0 0 8px #00ff99);
animation: pulse 2s infinite ease-in-out;
}
@keyframes pulse {
0% { transform: scale(1); filter: drop-shadow(0 0 8px #00ff99); }
50% { transform: scale(1.05); filter: drop-shadow(0 0 15px #00ff99); }
100% { transform: scale(1); filter: drop-shadow(0 0 8px #00ff99); }
}
.ai-content {
display: flex;
flex-direction: column;
gap: 1rem;
justify-content: center;
}
.summary-box {
font-size: 0.95rem;
line-height: 1.5;
color: #e0e0e0;
max-height: 40vh;
overflow-y: auto;
padding-right: 8px;
}
.summary-box::-webkit-scrollbar {
width: 4px;
}
.summary-box::-webkit-scrollbar-thumb {
background: rgba(0, 255, 153, 0.3);
border-radius: 2px;
}
.ai-actions {
display: flex;
gap: 1rem;
}
.action-btn {
padding: 0.5rem 1.2rem;
border-radius: 20px;
font-size: 0.85rem;
cursor: pointer; 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; font-family: inherit;
} }
.action-btn.ghost { .toolbar-btn:hover:not(.disabled) {
background: transparent; background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.2); color: #ffffff;
color: #aaa;
} }
.action-btn.neon-border { .toolbar-btn.primary {
background: rgba(0, 255, 153, 0.1); color: var(--nexus-neon, #00ff99);
border: 1px solid #00ff99;
color: #00ff99;
} }
.action-btn:hover { .toolbar-btn.primary:hover:not(.disabled) {
transform: translateY(-2px); background: rgba(0, 255, 153, 0.08);
box-shadow: 0 4px 12px rgba(0, 255, 153, 0.2); box-shadow: 0 0 12px rgba(0, 255, 153, 0.15);
} }
.bubble-pointer { .toolbar-btn.disabled {
position: absolute; opacity: 0.35;
left: 50%; cursor: not-allowed;
transform: translateX(-50%); pointer-events: none;
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
} }
.selection-ai-panel:not(.below) .bubble-pointer { .toolbar-divider {
bottom: -10px; width: 1px;
border-top: 10px solid rgba(18, 18, 18, 0.95); height: 16px;
background: rgba(255, 255, 255, 0.1);
} }
.selection-ai-panel.below .bubble-pointer { .btn-icon {
top: -10px; display: inline-flex;
border-bottom: 10px solid rgba(18, 18, 18, 0.95); align-items: center;
justify-content: center;
flex-shrink: 0;
} }
.loading-state { .spinner-inline {
padding: 1rem; 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 { @keyframes spin {
background: linear-gradient(90deg, transparent, rgba(0, 255, 153, 0.2), transparent); to {
background-size: 200% 100%; transform: rotate(360deg);
animation: shimmer 1.5s infinite; }
padding: 0.5rem;
border-radius: 4px;
} }
@keyframes shimmer { .opacity-50 {
from { background-position: 200% 0; } opacity: 0.5 !important;
to { background-position: -200% 0; }
} }
.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> </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) @if (Metadata != null)
{ {
<div class="verification-layout"> <div class="verification-layout">
@@ -196,52 +196,37 @@
margin-top: 1rem; margin-top: 1rem;
} }
.btn { ::deep .nexus-btn.btn-primary {
font-family: var(--nexus-font-sans); background: var(--nexus-neon, #00ffaa) !important;
font-weight: 600; color: #050505 !important;
padding: 0.75rem 1.5rem; border-color: transparent !important;
border-radius: 8px; box-shadow: 0 4px 12px rgba(var(--nexus-accent-rgb, 0, 255, 170), 0.2) !important;
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;
} }
.btn-primary { ::deep .nexus-btn.btn-primary:hover:not(:disabled) {
background: var(--nexus-neon, #00ffaa); background: #00e699 !important;
color: #050505; transform: translateY(-2px) !important;
box-shadow: 0 4px 12px rgba(var(--nexus-accent-rgb, 0, 255, 170), 0.2); box-shadow: 0 6px 20px rgba(var(--nexus-accent-rgb, 0, 255, 170), 0.4) !important;
} }
.btn-primary:hover { ::deep .nexus-btn.btn-primary:active:not(:disabled) {
background: #00e699; transform: translateY(0) !important;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(var(--nexus-accent-rgb, 0, 255, 170), 0.4);
} }
.btn-primary:active { ::deep .nexus-btn.btn-secondary {
transform: translateY(0); 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 { ::deep .nexus-btn.btn-secondary:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.03); background: rgba(255, 255, 255, 0.08) !important;
color: var(--nexus-text); border-color: rgba(255, 255, 255, 0.3) !important;
border: 1px solid rgba(255, 255, 255, 0.1); transform: translateY(-2px) !important;
} }
.btn-secondary:hover { ::deep .nexus-btn.btn-secondary:active:not(:disabled) {
background: rgba(255, 255, 255, 0.08); transform: translateY(0) !important;
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.btn-secondary:active {
transform: translateY(0);
} }
/* Verification State */ /* Verification State */
@@ -357,27 +342,30 @@
to { transform: scale(1.2); opacity: 0.8; } to { transform: scale(1.2); opacity: 0.8; }
} }
.btn:disabled { ::deep .nexus-btn:disabled:not(.btn-loading) {
opacity: 0.5; opacity: 0.4 !important;
cursor: not-allowed; cursor: not-allowed !important;
filter: grayscale(1); filter: grayscale(1) !important;
} }
.btn-loading { ::deep .nexus-btn.btn-loading {
position: relative; position: relative !important;
color: transparent !important; color: transparent !important;
opacity: 1 !important;
cursor: wait !important;
filter: none !important;
} }
.btn-loading::after { ::deep .nexus-btn.btn-loading::after {
content: ""; content: "" !important;
position: absolute; position: absolute !important;
width: 20px; width: 20px !important;
height: 20px; height: 20px !important;
border: 2px solid rgba(255, 255, 255, 0.2); border: 2px solid rgba(255, 255, 255, 0.2) !important;
border-top-color: var(--nexus-neon, #00ffaa); border-top-color: var(--nexus-neon, #00ffaa) !important;
border-radius: 50%; border-radius: 50% !important;
animation: spin 0.8s linear infinite; animation: spin 0.8s linear infinite !important;
filter: drop-shadow(0 0 4px var(--nexus-neon, #00ffaa)); filter: drop-shadow(0 0 4px var(--nexus-neon, #00ffaa)) !important;
} }
/* Indexing State */ /* Indexing State */
@@ -12,21 +12,21 @@
<div class="knowledge-graph-container @(GraphService.IsLoading ? "loading" : "")" id="@ContainerId"> <div class="knowledge-graph-container @(GraphService.IsLoading ? "loading" : "")" id="@ContainerId">
@if (GraphService.IsLoading || GraphService.CurrentGraphData == null) @if (GraphService.IsLoading || GraphService.CurrentGraphData == null)
{ {
<div class="loading-state"> <div class="loading-state">
<div class="preloader-robot"> <div class="preloader-robot">
<NexusIcon Name="robot" Size="64" Class="neon-pulse" /> <NexusIcon Name="robot" Size="64" Class="neon-pulse" />
<div class="scan-line"></div> <div class="scan-line"></div>
</div>
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">Mapowanie relacji rozdziału...</NexusTypography>
</div> </div>
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">Mapowanie relacji rozdziału...</NexusTypography>
</div>
} }
else else
{ {
<div class="graph-controls"> <div class="graph-controls">
<button class="zoom-btn" @onclick="ZoomIn" title="Zoom In">+</button> <button class="zoom-btn" @onclick="ZoomIn" title="Zoom In">+</button>
<button class="zoom-btn" @onclick="ZoomOut" title="Zoom Out"></button> <button class="zoom-btn" @onclick="ZoomOut" title="Zoom Out"></button>
<button class="zoom-btn reset" @onclick="ZoomReset" title="Reset">⟲</button> <button class="zoom-btn reset" @onclick="ZoomReset" title="Reset">⟲</button>
</div> </div>
} }
</div> </div>
@@ -49,7 +49,7 @@
private async Task HandleGraphUpdate() private async Task HandleGraphUpdate()
{ {
if (_module == null) return; if (_module == null) return;
if (GraphService.CurrentGraphData == null) if (GraphService.CurrentGraphData == null)
{ {
await _module.InvokeVoidAsync("clear"); await _module.InvokeVoidAsync("clear");
@@ -58,7 +58,7 @@
{ {
await _module.InvokeVoidAsync("updateData", GraphService.CurrentGraphData); await _module.InvokeVoidAsync("updateData", GraphService.CurrentGraphData);
} }
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
@@ -78,7 +78,7 @@
if (firstRender) if (firstRender)
{ {
await InitializeGraphAsync(); await InitializeGraphAsync();
if (GraphService.CurrentGraphData != null) if (GraphService.CurrentGraphData != null)
{ {
await HandleGraphUpdate(); await HandleGraphUpdate();
@@ -101,7 +101,7 @@
public async Task OnNodeClicked(string nodeId) public async Task OnNodeClicked(string nodeId)
{ {
await InteractionService.NotifyNodeSelected(nodeId); await InteractionService.NotifyNodeSelected(nodeId);
if (OnNodeSelected.HasDelegate) if (OnNodeSelected.HasDelegate)
{ {
await OnNodeSelected.InvokeAsync(nodeId); await OnNodeSelected.InvokeAsync(nodeId);
@@ -128,7 +128,7 @@
GraphService.OnGraphUpdated -= HandleGraphUpdate; GraphService.OnGraphUpdated -= HandleGraphUpdate;
GraphService.OnActiveNodeChanged -= HandleActiveNodeChange; GraphService.OnActiveNodeChanged -= HandleActiveNodeChange;
GraphService.OnLoadingChanged -= HandleLoadingChange; GraphService.OnLoadingChanged -= HandleLoadingChange;
try try
{ {
if (_module is not null) if (_module is not null)
@@ -138,7 +138,7 @@
} }
} }
catch { } catch { }
_dotNetHelper?.Dispose(); _dotNetHelper?.Dispose();
} }
} }
@@ -10,7 +10,7 @@
position: relative; position: relative;
} }
.knowledge-graph-container.loading > ::deep svg { .knowledge-graph-container.loading> ::deep svg {
display: none !important; display: none !important;
} }
@@ -93,9 +93,20 @@
} }
@keyframes robot-pulse { @keyframes robot-pulse {
0% { transform: scale(1); filter: drop-shadow(0 0 10px var(--nexus-neon)); } 0% {
50% { transform: scale(1.1); filter: drop-shadow(0 0 25px var(--nexus-neon)); } transform: scale(1);
100% { transform: scale(1); filter: drop-shadow(0 0 10px var(--nexus-neon)); } 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 { .scan-line {
@@ -111,9 +122,17 @@
} }
@keyframes scan { @keyframes scan {
0% { top: 0; } 0% {
50% { top: 100%; } top: 0;
100% { top: 0; } }
50% {
top: 100%;
}
100% {
top: 0;
}
} }
::deep .nexus-node-active { ::deep .nexus-node-active {
@@ -124,11 +143,24 @@
} }
::deep @keyframes neon-flash { ::deep @keyframes neon-flash {
0% { filter: brightness(1) drop-shadow(0 0 0px var(--nexus-neon)); } 0% {
50% { filter: brightness(3) drop-shadow(0 0 30px var(--nexus-neon)); } filter: brightness(1) drop-shadow(0 0 0px var(--nexus-neon));
100% { 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 { ::deep .neon-flash-node {
animation: neon-flash 0.8s ease-out; 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 bool _isMobile = false;
private DotNetObjectReference<ReaderCanvas>? _selfReference; private DotNetObjectReference<ReaderCanvas>? _selfReference;
private IJSObjectReference? _viewportModule; private IJSObjectReference? _viewportModule;
private IJSObjectReference? _selectionModule;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@@ -201,10 +202,13 @@
{ {
try 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) if (_selfReference != null)
{ {
await module.InvokeVoidAsync("initSelectionListener", _selfReference, _containerRef); await _selectionModule.InvokeVoidAsync("initSelectionListener", _selfReference, _containerRef);
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -440,6 +444,19 @@
InteractionService.OnTextSelected -= HandleTextSelected; InteractionService.OnTextSelected -= HandleTextSelected;
SyncService.OnProgressReceived -= HandleSyncProgressReceived; 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 try
{ {
if (_viewportModule != null) if (_viewportModule != null)
@@ -6,7 +6,7 @@
padding: 2rem 0; padding: 2rem 0;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative; position: relative;
/* Dedicated Scrollbar Styling */ /* Dedicated Scrollbar Styling */
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: rgba(0, 255, 153, 0.2) transparent; scrollbar-color: rgba(0, 255, 153, 0.2) transparent;
@@ -30,18 +30,40 @@
background-color: rgba(0, 255, 153, 0.5); background-color: rgba(0, 255, 153, 0.5);
} }
.reader-canvas.theme-dark {
background-color: #121214;
}
.reader-canvas.theme-light { .reader-canvas.theme-light {
background-color: #F9F9F9; /* Paper-white requirement */ background-color: #f4f1ea;
/* Warm light beige/gray background */
} }
.reader-flow-container { .reader-flow-container {
max-width: 800px; max-width: 680px;
margin: 0 auto; margin: 2rem auto;
min-height: calc(100vh - 180px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.5rem; gap: 1.5rem;
position: relative; 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 { .block-wrapper {
@@ -57,24 +79,49 @@
line-height: 1.65 !important; line-height: 1.65 !important;
letter-spacing: -0.01em !important; letter-spacing: -0.01em !important;
font-size: 1.15rem; 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 { .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 */ /* Technical Code Block Container */
::deep .nexus-ebook pre { ::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; color: #e0e0e0;
padding: 1.25rem; padding: 1.25rem;
border-radius: 8px; border-radius: 8px;
margin: 2rem 0; margin: 2rem 0;
overflow-x: auto; overflow-x: auto;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1); box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
border-left: 4px solid var(--nexus-neon); /* Nexus neon accent */ border-left: 4px solid var(--nexus-neon);
/* Nexus neon accent */
/* Dedicated Scrollbar for Code */ /* Dedicated Scrollbar for Code */
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: rgba(0, 255, 153, 0.3) transparent; scrollbar-color: rgba(0, 255, 153, 0.3) transparent;
@@ -101,7 +148,8 @@
/* Inline Code Highlight */ /* Inline Code Highlight */
::deep .nexus-ebook p code { ::deep .nexus-ebook p code {
background-color: rgba(0, 0, 0, 0.05); 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; padding: 0.2rem 0.4rem;
border-radius: 4px; border-radius: 4px;
font-size: 0.9em; font-size: 0.9em;
@@ -153,9 +201,20 @@
} }
@keyframes pulse-small { @keyframes pulse-small {
0% { transform: scale(1); opacity: 1; } 0% {
50% { transform: scale(1.1); opacity: 0.8; } transform: scale(1);
100% { transform: scale(1); opacity: 1; } opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
100% {
transform: scale(1);
opacity: 1;
}
} }
/* Chapter Loading Overlay and Spinners */ /* Chapter Loading Overlay and Spinners */
@@ -246,29 +305,48 @@
} }
@keyframes spin { @keyframes spin {
0% { transform: rotate(0deg); } 0% {
100% { transform: rotate(360deg); } transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} }
@keyframes scaleIn { @keyframes scaleIn {
from { transform: scale(0.9) translateY(10px); opacity: 0; } from {
to { transform: scale(1) translateY(0); opacity: 1; } transform: scale(0.9) translateY(10px);
opacity: 0;
}
to {
transform: scale(1) translateY(0);
opacity: 1;
}
} }
@keyframes fadeIn { @keyframes fadeIn {
from { opacity: 0; } from {
to { opacity: 1; } opacity: 0;
}
to {
opacity: 1;
}
} }
/* MOBILE READER UI OVERRIDES */ /* MOBILE READER UI OVERRIDES */
@media (max-width: 768px) { @media (max-width: 768px) {
.reader-canvas { .reader-canvas {
padding-top: 54px !important; 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 { .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 { .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 { .nexus-chapter-nav-btn {
@@ -372,4 +463,4 @@
.theme-light .nexus-chapter-nav-btn:hover:not(:disabled) { .theme-light .nexus-chapter-nav-btn:hover:not(:disabled) {
background: rgba(0, 0, 0, 0.06); background: rgba(0, 0, 0, 0.06);
} }
@@ -1,64 +1,99 @@
.reader-footer { .reader-footer {
position: relative; position: absolute;
height: 50px; bottom: 24px;
background: #F9F9F9; left: 50%;
border-top: 1px solid rgba(0, 0, 0, 0.08); 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; display: flex;
align-items: center; align-items: center;
padding: 0 1.5rem; padding: 0 1.5rem;
z-index: 10; z-index: 100;
flex-shrink: 0; 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 { .footer-content {
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%; width: 100%;
gap: 1.5rem; gap: 1rem;
justify-content: space-between;
} }
.navigation-controls { .navigation-controls {
display: grid; display: flex;
grid-template-columns: 32px 1fr 32px;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
width: 260px; flex: 1;
flex-shrink: 0; min-width: 0;
} }
.nav-btn { .nav-btn {
background: white; background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(0, 0, 0, 0.1); border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 6px; border-radius: 50%;
width: 32px; width: 32px;
height: 32px; height: 32px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; 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: #333; color: #a1a1aa; /* Zinc-400 default contrast */
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
} }
.nav-btn:hover:not(:disabled) { .nav-btn:hover:not(:disabled),
background: #f0f0f0; .nav-btn:focus:not(:disabled) {
transform: translateY(-1px); 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 { .nav-btn:disabled {
opacity: 0.3; opacity: 0.25;
cursor: not-allowed; 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 { .chapter-info {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: flex-start;
justify-content: center; justify-content: center;
min-width: 0; min-width: 0;
overflow: hidden; flex: 1;
color: #333; color: #e2e8f0; /* Slate-200 for clean high readability */
}
.theme-light .chapter-info {
color: #292524; /* Warm charcoal for legibility */
} }
.chapter-title { .chapter-title {
@@ -68,42 +103,66 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
text-align: center;
line-height: 1.2; line-height: 1.2;
} }
.chapter-count { .chapter-count {
opacity: 0.5; color: #a1a1aa; /* Zinc-400 for secondary info clarity */
font-size: 0.75rem; font-size: 0.7rem;
}
.theme-light .chapter-count {
color: #78716c; /* Warm stone-500 secondary info */
} }
.progress-container { .progress-container {
flex: 1; width: 80px;
height: 6px; height: 4px;
background: rgba(0, 0, 0, 0.05); background: rgba(255, 255, 255, 0.08);
border-radius: 3px; border-radius: 2px;
overflow: hidden; 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 { .progress-bar {
height: 100%; height: 100%;
background: #2ECC71; background: var(--nexus-neon, #00ff99);
border-radius: 3px; border-radius: 2px;
transition: width 0.3s ease; transition: width 0.3s ease;
} }
.theme-light .progress-bar {
background: #10b981;
}
.meta-info { .meta-info {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 0.75rem;
font-size: 0.75rem; font-size: 0.7rem;
color: #888; color: #a1a1aa;
flex-shrink: 0; flex-shrink: 0;
font-family: monospace;
}
.theme-light .meta-info {
color: #78716c;
} }
.battery { .battery {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.3rem; 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 IIdentityService IdentityService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject Microsoft.Extensions.Logging.ILogger<ReaderLayout> Logger @inject Microsoft.Extensions.Logging.ILogger<ReaderLayout> Logger
@inject IThemeService ThemeService
@inject KnowledgeCoordinator Coordinator
@implements IAsyncDisposable @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"> <div class="reader-pane">
<main> <main>
@Body @Body
@@ -62,7 +64,32 @@
<span class="panel-title">Contextual Intelligence Panel</span> <span class="panel-title">Contextual Intelligence Panel</span>
</div> </div>
<div class="panel-body"> <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-details">
<div class="node-header-section"> <div class="node-header-section">
@@ -165,7 +192,32 @@
{ {
<div class="contextual-intelligence-panel"> <div class="contextual-intelligence-panel">
<div class="panel-body"> <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-details">
<div class="node-header-section"> <div class="node-header-section">
@@ -291,6 +343,8 @@
InteractionService.OnAssistantRequested += HandleAssistantRequestedAsync; InteractionService.OnAssistantRequested += HandleAssistantRequestedAsync;
InteractionService.OnScrollPercentChanged += HandleScrollPercentChanged; InteractionService.OnScrollPercentChanged += HandleScrollPercentChanged;
GraphService.OnGraphUpdated += HandleGraphUpdatedAsync; GraphService.OnGraphUpdated += HandleGraphUpdatedAsync;
ThemeService.OnThemeChanged += HandleThemeChangedAsync;
Coordinator.OnSelectionSummaryStateChanged += HandleUpdate;
var context = PlatformService.GetDeviceContext(); var context = PlatformService.GetDeviceContext();
if (context.IsSuccess) if (context.IsSuccess)
@@ -305,6 +359,8 @@
} }
} }
private async Task HandleThemeChangedAsync() => await InvokeAsync(StateHasChanged);
private void SetActiveTab(SidebarTab tab) private void SetActiveTab(SidebarTab tab)
{ {
_activeTab = tab; _activeTab = tab;
@@ -329,6 +385,11 @@
StateHasChanged(); StateHasChanged();
} }
private async Task ClearSelectionSummary()
{
await Coordinator.ClearSelectionSummaryAsync();
}
private async Task HandleScrollPercentChanged(int percent) private async Task HandleScrollPercentChanged(int percent)
{ {
_scrollPercentage = percent; _scrollPercentage = percent;
@@ -349,12 +410,24 @@
{ {
if (_isMobile) if (_isMobile)
{ {
if (Coordinator.IsLoadingSelectionSummary || !string.IsNullOrEmpty(Coordinator.SelectionSummary))
{
_activeMobileTab = MobileReaderTab.Concepts;
_activeTab = SidebarTab.Knowledge;
}
OpenAssistant(); OpenAssistant();
} }
else else
{ {
_activeMobileTab = MobileReaderTab.Concepts; _activeMobileTab = MobileReaderTab.Concepts;
_activeTab = SidebarTab.Quiz; if (Coordinator.IsLoadingSelectionSummary || !string.IsNullOrEmpty(Coordinator.SelectionSummary))
{
_activeTab = SidebarTab.Knowledge;
}
else
{
_activeTab = SidebarTab.Quiz;
}
} }
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
@@ -445,6 +518,8 @@
InteractionService.OnAssistantRequested -= HandleAssistantRequestedAsync; InteractionService.OnAssistantRequested -= HandleAssistantRequestedAsync;
InteractionService.OnScrollPercentChanged -= HandleScrollPercentChanged; InteractionService.OnScrollPercentChanged -= HandleScrollPercentChanged;
GraphService.OnGraphUpdated -= HandleGraphUpdatedAsync; GraphService.OnGraphUpdated -= HandleGraphUpdatedAsync;
ThemeService.OnThemeChanged -= HandleThemeChangedAsync;
Coordinator.OnSelectionSummaryStateChanged -= HandleUpdate;
try try
{ {
@@ -4,18 +4,20 @@
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
overflow: hidden; overflow: hidden;
background: #121212; background: var(--nexus-bg);
} }
.reader-pane { .reader-pane {
background: #F9F9F9; grid-column: 1;
background: var(--nexus-bg);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
z-index: 5; z-index: 5;
height: 100vh; height: 100vh;
transition: background 0.2s ease, color 0.2s ease;
} }
main { main {
@@ -27,30 +29,65 @@ main {
} }
.intelligence-sidebar { .intelligence-sidebar {
grid-column: 3;
display: grid; display: grid;
grid-template-columns: 50px 1fr; grid-template-columns: 50px 1fr;
width: 100%; /* controlled by grid */ width: 100%;
/* controlled by grid */
height: 100%; height: 100%;
background: #0d0d0d; background: var(--nexus-card);
box-shadow: -10px 0 30px rgba(0, 0, 0, 0.3); 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; overflow: hidden;
z-index: 10; z-index: 10;
transition: background 0.2s ease, border-color 0.2s ease;
} }
.resizer { .resizer {
width: 4px; grid-column: 2;
width: 12px;
cursor: col-resize; cursor: col-resize;
background: rgba(255, 255, 255, 0.02); background: transparent;
transition: background 0.2s, width 0.2s;
z-index: 20; 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); background: var(--nexus-neon);
width: 6px; width: 6px;
box-shadow: 0 0 10px var(--nexus-neon); height: 80px;
box-shadow: 0 0 12px var(--nexus-neon);
} }
.app-container.is-resizing { .app-container.is-resizing {
@@ -63,6 +100,7 @@ main {
} }
.app-container.focus-mode-active .intelligence-sidebar { .app-container.focus-mode-active .intelligence-sidebar {
grid-column: 3;
grid-template-columns: 50px 0px; grid-template-columns: 50px 0px;
} }
@@ -94,7 +132,7 @@ main {
border-bottom: 1px solid rgba(255, 255, 255, 0.05); border-bottom: 1px solid rgba(255, 255, 255, 0.05);
font-family: var(--nexus-font-sans); font-family: var(--nexus-font-sans);
font-size: 0.9rem; font-size: 0.9rem;
color: #fff; color: var(--nexus-text);
flex-shrink: 0; flex-shrink: 0;
} }
@@ -149,9 +187,20 @@ main {
} }
@keyframes quiz-pulse { @keyframes quiz-pulse {
0% { filter: drop-shadow(0 0 2px var(--nexus-neon)); transform: scale(1); } 0% {
50% { filter: drop-shadow(0 0 10px var(--nexus-neon)); transform: scale(1.1); } filter: drop-shadow(0 0 2px var(--nexus-neon));
100% { filter: drop-shadow(0 0 2px var(--nexus-neon)); transform: scale(1); } 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 */ /* Contextual Intelligence Panel Layout */
@@ -226,9 +275,20 @@ main {
} }
@keyframes glow-pulse { @keyframes glow-pulse {
0% { transform: scale(0.9); opacity: 0.5; } 0% {
50% { transform: scale(1.1); opacity: 1; } transform: scale(0.9);
100% { transform: scale(0.9); opacity: 0.5; } opacity: 0.5;
}
50% {
transform: scale(1.1);
opacity: 1;
}
100% {
transform: scale(0.9);
opacity: 0.5;
}
} }
.placeholder-text { .placeholder-text {
@@ -245,8 +305,15 @@ main {
} }
@keyframes fade-in { @keyframes fade-in {
from { opacity: 0; transform: translateY(5px); } from {
to { opacity: 1; transform: translateY(0); } opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
.node-header-section { .node-header-section {
@@ -432,9 +499,20 @@ main {
} }
@keyframes quiz-pulse-glow { @keyframes quiz-pulse-glow {
0% { border-color: rgba(0, 240, 255, 0.3); box-shadow: 0 0 5px rgba(0, 240, 255, 0.1); } 0% {
50% { border-color: var(--nexus-neon, #00f0ff); box-shadow: 0 0 25px rgba(0, 240, 255, 0.3); } border-color: 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); } 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 */ /* Quiz Navigation Header */
@@ -479,7 +557,8 @@ main {
.platform-mobile .reader-pane { .platform-mobile .reader-pane {
width: 100vw !important; width: 100vw !important;
height: 100vh !important; /* full viewport height */ height: 100vh !important;
/* full viewport height */
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@@ -506,9 +585,11 @@ main {
} }
.platform-mobile .nexus-mobile-reader-tabs { .platform-mobile .nexus-mobile-reader-tabs {
display: none; /* Keep hidden by default */ display: none;
/* Keep hidden by default */
width: 100vw; width: 100vw;
height: 100vh; /* full viewport height */ height: 100vh;
/* full viewport height */
position: absolute; position: absolute;
top: 0; top: 0;
left: 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-graph .nexus-mobile-reader-tabs,
.app-container.platform-mobile.active-mobile-tab-concepts .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 { .nexus-mobile-tab-content {
@@ -542,6 +624,7 @@ main {
opacity: 0; opacity: 0;
transform: translateY(10px); transform: translateY(10px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
@@ -621,9 +704,18 @@ main {
} }
@keyframes quiz-pulse-btn-anim { @keyframes quiz-pulse-btn-anim {
0% { color: rgba(255, 255, 255, 0.5); } 0% {
50% { color: #f43f5e; text-shadow: 0 0 8px rgba(244, 63, 94, 0.6); } color: rgba(255, 255, 255, 0.5);
100% { 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 { .mobile-insight-body {
@@ -648,3 +740,279 @@ main {
} }
/* Obsolescence managed: consolidated mobile toolbar and sheet styled inside respective components */ /* 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> /// <summary>
/// Screen coordinates for text selection popup positioning. /// Screen coordinates for text selection popup positioning.
/// </summary> /// </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> /// <summary>
/// Represents a message in the KM-RAG global and mobile intelligence chat threads. /// 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 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> /// <summary>
/// Raised when the knowledge graph has been updated with new data. /// Raised when the knowledge graph has been updated with new data.
/// Subscribers must return a Task to enable proper async handling. /// Subscribers must return a Task to enable proper async handling.
/// </summary> /// </summary>
public event Func<GraphDataDto, Task>? OnGraphUpdated; 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( public KnowledgeCoordinator(
IKnowledgeService knowledgeService, IKnowledgeService knowledgeService,
IKnowledgeGraphService graphService, 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() public async Task ClearAsync()
{ {
CancelAndDisposeCts(ref _graphCts); CancelAndDisposeCts(ref _graphCts);
@@ -213,6 +267,7 @@ public sealed partial class KnowledgeCoordinator : IDisposable, IAsyncDisposable
CurrentFullPageContent = string.Empty; CurrentFullPageContent = string.Empty;
await _graphService.Clear(); await _graphService.Clear();
await _quizService.SetQuiz(null, null); await _quizService.SetQuiz(null, null);
await ClearSelectionSummaryAsync();
} }
public void Dispose() public void Dispose()
+220 -35
View File
@@ -3,37 +3,83 @@
:root { :root {
--nexus-neon: #00ff99; --nexus-neon: #00ff99;
--nexus-neon-glow: rgba(0, 255, 153, 0.3); --nexus-neon-glow: rgba(0, 255, 153, 0.3);
--nexus-bg: #121212; --nexus-bg: #121214;
--nexus-card: #1a1a1a; --nexus-card: #1a1a1e;
--nexus-text: #ffffff; --nexus-text: #ffffff;
--nexus-paper: #F9F9F9; --nexus-paper: #F9F9F9;
--nexus-font-sans: 'Inter', sans-serif; --nexus-font-sans: 'Inter', sans-serif;
--nexus-font-serif: 'Merriweather', 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;
/* 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 */ /* Global Selection Style Override */
--nexus-transition: 0.4s cubic-bezier(0.4, 0, 0.2, 1); --nexus-selection: rgba(0, 255, 153, 0.25);
/* 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);
--nexus-node-pill-bg: rgba(20, 20, 20, 0.95);
--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 */ /* Global Glassmorphism with Fallback */
.glass-panel { .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: 1px solid rgba(255, 255, 255, 0.05);
border-radius: var(--radius-xl); border-radius: var(--radius-xl);
padding: 1.5rem; padding: 1.5rem;
@@ -61,32 +107,157 @@
border: none; border: none;
text-decoration: none; text-decoration: none;
} }
.btn-nexus-primary { .btn-nexus-primary {
background: var(--nexus-neon); background: var(--nexus-neon);
color: #000000; color: #000000;
} }
.btn-nexus-secondary { .btn-nexus-secondary {
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
color: #ffffff; color: #ffffff;
} }
.btn-nexus:hover { .btn-nexus:hover {
transform: translateY(-2px); transform: translateY(-2px);
filter: brightness(1.1); filter: brightness(1.1);
} }
.btn-nexus-primary:hover { .btn-nexus-primary:hover {
box-shadow: 0 4px 15px var(--nexus-primary-glow); box-shadow: 0 4px 15px var(--nexus-primary-glow);
} }
.btn-nexus-secondary:hover { .btn-nexus-secondary:hover {
box-shadow: 0 4px 15px rgba(255, 255, 255, 0.05); box-shadow: 0 4px 15px rgba(255, 255, 255, 0.05);
} }
.theme-light { .theme-light {
--nexus-bg: var(--nexus-paper); --nexus-bg: #f4f1ea;
--nexus-card: #ffffff; --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);
}
* { * {
@@ -101,13 +272,13 @@ body {
font-family: var(--nexus-font-sans); font-family: var(--nexus-font-sans);
margin: 0; margin: 0;
padding: 0; padding: 0;
/* Handle Notches */ /* Handle Notches */
padding-top: var(--safe-area-inset-top); padding-top: var(--safe-area-inset-top);
padding-bottom: var(--safe-area-inset-bottom); padding-bottom: var(--safe-area-inset-bottom);
padding-left: var(--safe-area-inset-left); padding-left: var(--safe-area-inset-left);
padding-right: var(--safe-area-inset-right); padding-right: var(--safe-area-inset-right);
min-height: 100vh; min-height: 100vh;
overflow-x: hidden; overflow-x: hidden;
} }
@@ -133,14 +304,14 @@ body {
/* Platform Specific Tweaks */ /* Platform Specific Tweaks */
.platform-mobile .nexus-button { .platform-mobile .nexus-btn {
min-height: var(--touch-target-size); min-height: var(--touch-target-size);
min-width: var(--touch-target-size); min-width: var(--touch-target-size);
font-size: 1.1rem; font-size: 1.1rem;
padding: 12px 24px; padding: 12px 24px;
} }
.platform-desktop .nexus-button { .platform-desktop .nexus-btn {
min-height: 36px; min-height: 36px;
font-size: 0.9rem; font-size: 0.9rem;
padding: 8px 16px; padding: 8px 16px;
@@ -164,7 +335,8 @@ h1:focus {
} }
/* Preloader Styles */ /* Preloader Styles */
#app-preloader, .app-preloader { #app-preloader,
.app-preloader {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@@ -206,12 +378,25 @@ h1:focus {
} }
@keyframes spin { @keyframes spin {
0% { transform: rotate(0deg); } 0% {
100% { transform: rotate(360deg); } transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} }
@keyframes pulse { @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) // 1. Rule (red/coral)
if (type === 'rule') { if (type === 'rule') {
return { return {
color: '#ff4646', color: 'var(--nexus-node-rule, #ff4646)',
fill: 'rgba(255, 70, 70, 0.1)', fill: 'var(--nexus-node-rule-bg, rgba(255, 70, 70, 0.1))',
opacity: 0.8, opacity: 0.8,
glowKey: 'rule', glowKey: 'rule',
textColor: '#ff8b8b' textColor: 'var(--nexus-node-rule-text, #ff8b8b)'
}; };
} }
// 2. Definition (gold/amber) // 2. Definition (gold/amber)
if (type === 'definition') { if (type === 'definition') {
return { return {
color: '#ffb03a', color: 'var(--nexus-node-definition, #ffb03a)',
fill: 'rgba(255, 176, 58, 0.1)', fill: 'var(--nexus-node-definition-bg, rgba(255, 176, 58, 0.1))',
opacity: 0.8, opacity: 0.8,
glowKey: 'definition', glowKey: 'definition',
textColor: '#ffd18c' textColor: 'var(--nexus-node-definition-text, #ffd18c)'
}; };
} }
// 3. Table (purple/magenta) // 3. Table (purple/magenta)
if (type === 'table') { if (type === 'table') {
return { return {
color: '#d946ef', color: 'var(--nexus-node-table, #d946ef)',
fill: 'rgba(217, 70, 239, 0.1)', fill: 'var(--nexus-node-table-bg, rgba(217, 70, 239, 0.1))',
opacity: 0.8, opacity: 0.8,
glowKey: 'table', glowKey: 'table',
textColor: '#f5d0fe' textColor: 'var(--nexus-node-table-text, #f5d0fe)'
}; };
} }
// 4. Section (blue/indigo) // 4. Section (blue/indigo)
if (type === 'section') { if (type === 'section') {
return { return {
color: '#3b82f6', color: 'var(--nexus-node-section, #3b82f6)',
fill: 'rgba(59, 130, 246, 0.1)', fill: 'var(--nexus-node-section-bg, rgba(59, 130, 246, 0.1))',
opacity: 0.8, opacity: 0.8,
glowKey: 'section', glowKey: 'section',
textColor: '#93c5fd' textColor: 'var(--nexus-node-section-text, #93c5fd)'
}; };
} }
// 5. Bridge (cyan/comparison) // 5. Bridge (cyan/comparison)
if (group === 'bridge') { if (group === 'bridge') {
return { return {
color: '#06b6d4', color: 'var(--nexus-node-bridge, #06b6d4)',
fill: 'rgba(6, 182, 212, 0.1)', fill: 'var(--nexus-node-bridge-bg, rgba(6, 182, 212, 0.1))',
opacity: 0.7, opacity: 0.7,
glowKey: 'bridge', glowKey: 'bridge',
textColor: '#67e8f9' textColor: 'var(--nexus-node-bridge-text, #67e8f9)'
}; };
} }
// 6. Current (active/focus landmark - neon green) // 6. Current (active/focus landmark - neon green)
if (group === 'current') { if (group === 'current') {
return { return {
color: 'var(--nexus-neon)', color: 'var(--nexus-node-current, var(--nexus-neon))',
fill: 'rgba(0, 255, 153, 0.15)', fill: 'var(--nexus-node-current-bg, rgba(0, 255, 153, 0.15))',
opacity: 0.9, opacity: 0.9,
glowKey: 'current', glowKey: 'current',
textColor: '#ffffff' textColor: 'var(--nexus-node-current-text, #ffffff)'
}; };
} }
// 7. Concept / Default (subtle cool steel blue/teal) // 7. Concept / Default (subtle cool steel blue/teal)
return { return {
color: '#00d2c4', color: 'var(--nexus-node-concept, #00d2c4)',
fill: 'rgba(0, 210, 196, 0.05)', fill: 'var(--nexus-node-concept-bg, rgba(0, 210, 196, 0.05))',
opacity: 0.4, opacity: 0.4,
glowKey: 'concept', glowKey: 'concept',
textColor: '#e0e0e0' textColor: 'var(--nexus-node-concept-text, #e0e0e0)'
}; };
}; };
@@ -131,16 +131,16 @@ const getNodeGlyph = d => {
function updateNodeAppearances() { function updateNodeAppearances() {
if (!node) return; if (!node) return;
node.each(function(d) { node.each(function (d) {
const g = d3.select(this); const g = d3.select(this);
const rect = g.select(".node-pill"); const rect = g.select(".node-pill");
const text = g.select("text"); const text = g.select("text");
const isCurrent = getNodeGroup(d) === 'current'; const isCurrent = getNodeGroup(d) === 'current';
const isSelected = activeNodeId && d.id === activeNodeId; const isSelected = activeNodeId && d.id === activeNodeId;
const showFull = !isMobileMode || isSelected || isCurrent; const showFull = !isMobileMode || isSelected || isCurrent;
if (showFull) { if (showFull) {
rect.transition().duration(250) rect.transition().duration(250)
.attr("x", -getPillWidth(d) / 2) .attr("x", -getPillWidth(d) / 2)
@@ -148,7 +148,7 @@ function updateNodeAppearances() {
.attr("height", 30) .attr("height", 30)
.attr("rx", 15) .attr("rx", 15)
.attr("y", -15); .attr("y", -15);
text.text(getDisplayLabel(d)) text.text(getDisplayLabel(d))
.attr("font-size", isCurrent || isSelected ? "0.85rem" : "0.8rem") .attr("font-size", isCurrent || isSelected ? "0.85rem" : "0.8rem")
.attr("font-weight", isCurrent || isSelected ? "600" : "normal"); .attr("font-weight", isCurrent || isSelected ? "600" : "normal");
@@ -159,7 +159,7 @@ function updateNodeAppearances() {
.attr("height", 30) .attr("height", 30)
.attr("rx", 15) .attr("rx", 15)
.attr("y", -15); .attr("y", -15);
text.text(getNodeGlyph(d)) text.text(getNodeGlyph(d))
.attr("font-size", "0.9rem") .attr("font-size", "0.9rem")
.attr("font-weight", "bold"); .attr("font-weight", "bold");
@@ -170,7 +170,7 @@ function updateNodeAppearances() {
export function setMobileMode(isMobile) { export function setMobileMode(isMobile) {
isMobileMode = isMobile; isMobileMode = isMobile;
if (!simulation) return; if (!simulation) return;
if (isMobile) { if (isMobile) {
simulation.force("charge", d3.forceManyBody().strength(-60)); simulation.force("charge", d3.forceManyBody().strength(-60));
simulation.force("link").distance(180); simulation.force("link").distance(180);
@@ -187,7 +187,7 @@ export function setMobileMode(isMobile) {
simulation.force("link").distance(120); simulation.force("link").distance(120);
simulation.force("collide", d3.forceCollide().radius(d => (getPillWidth(d) / 2) + 20)); simulation.force("collide", d3.forceCollide().radius(d => (getPillWidth(d) / 2) + 20));
} }
updateNodeAppearances(); updateNodeAppearances();
simulation.alpha(0.3).restart(); simulation.alpha(0.3).restart();
} }
@@ -208,11 +208,11 @@ export function mount(containerId, data, dotNetHelper) {
.attr("viewBox", [0, 0, width, height]) .attr("viewBox", [0, 0, width, height])
.attr("width", "100%") .attr("width", "100%")
.attr("height", "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 // Radial gradients for Nebula effects
const defs = svgElement.append("defs"); const defs = svgElement.append("defs");
// Fallback radial gradient for legacy nebulaGlow // Fallback radial gradient for legacy nebulaGlow
const radialGradient = defs.append("radialGradient") const radialGradient = defs.append("radialGradient")
.attr("id", "nebulaGlow") .attr("id", "nebulaGlow")
@@ -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); radialGradient.append("stop").attr("offset", "100%").attr("stop-color", "var(--nexus-neon)").attr("stop-opacity", 0);
const colors = { const colors = {
'rule': '#ff4646', 'rule': 'var(--nexus-node-rule, #ff4646)',
'definition': '#ffb03a', 'definition': 'var(--nexus-node-definition, #ffb03a)',
'table': '#d946ef', 'table': 'var(--nexus-node-table, #d946ef)',
'section': '#3b82f6', 'section': 'var(--nexus-node-section, #3b82f6)',
'bridge': '#06b6d4', 'bridge': 'var(--nexus-node-bridge, #06b6d4)',
'current': 'var(--nexus-neon)', 'current': 'var(--nexus-node-current, var(--nexus-neon))',
'concept': '#00d2c4' 'concept': 'var(--nexus-node-concept, #00d2c4)'
}; };
Object.entries(colors).forEach(([key, color]) => { Object.entries(colors).forEach(([key, color]) => {
@@ -275,7 +275,7 @@ export function mount(containerId, data, dotNetHelper) {
zoomBehavior = d3.zoom() zoomBehavior = d3.zoom()
.scaleExtent([0.3, 4]) .scaleExtent([0.3, 4])
.on("zoom", (e) => rootGroup.attr("transform", e.transform)); .on("zoom", (e) => rootGroup.attr("transform", e.transform));
svgElement.call(zoomBehavior).on("wheel.zoom", null); svgElement.call(zoomBehavior).on("wheel.zoom", null);
// Use ResizeObserver for more reliable container size tracking // Use ResizeObserver for more reliable container size tracking
@@ -324,7 +324,7 @@ export function mount(containerId, data, dotNetHelper) {
return `translate(${d.x},${d.y})`; return `translate(${d.x},${d.y})`;
}); });
} }
if (badge && badge.style("display") !== "none") { if (badge && badge.style("display") !== "none") {
const activeData = badge.datum(); const activeData = badge.datum();
if (activeData) { if (activeData) {
@@ -377,9 +377,9 @@ export function updateData(data) {
enter => enter.append("path") enter => enter.append("path")
.attr("stroke", d => { .attr("stroke", d => {
if (d.type === 'Defines' || d.type === 'maps_to') return 'var(--nexus-accent, #00ffaa)'; 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)'; 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("fill", "none")
.attr("stroke-width", d => (d.type === 'Defines' || d.type === 'maps_to') ? 2 : 1) .attr("stroke-width", d => (d.type === 'Defines' || d.type === 'maps_to') ? 2 : 1)
@@ -413,7 +413,7 @@ export function updateData(data) {
g.append("rect") g.append("rect")
.attr("class", "node-pill") .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", d => getCategoryStyle(d).color)
.attr("stroke-width", d => getNodeGroup(d) === 'current' ? 2 : 1.2); .attr("stroke-width", d => getNodeGroup(d) === 'current' ? 2 : 1.2);
@@ -424,9 +424,9 @@ export function updateData(data) {
g.append("title") g.append("title")
.text(d => d.description ? `${d.label}\n\n${d.description}` : d.label); .text(d => d.description ? `${d.label}\n\n${d.description}` : d.label);
g.transition().duration(500).style("opacity", 1); g.transition().duration(500).style("opacity", 1);
return g; return g;
}, },
update => update.classed("neon-flash-node", false), update => update.classed("neon-flash-node", false),
@@ -466,7 +466,7 @@ function drag(simulation) {
export function setActiveNode(nodeId) { export function setActiveNode(nodeId) {
if (!svgElement || !node) return; if (!svgElement || !node) return;
activeNodeId = nodeId; activeNodeId = nodeId;
// Safety check: ensure we only target the first occurrence if IDs are duplicated // Safety check: ensure we only target the first occurrence if IDs are duplicated
const targetNode = node.filter(d => d.id === nodeId); const targetNode = node.filter(d => d.id === nodeId);
@@ -479,7 +479,7 @@ export function setActiveNode(nodeId) {
const firstMatch = targetNode.filter((d, i) => i === 0); const firstMatch = targetNode.filter((d, i) => i === 0);
const d = firstMatch.datum(); const d = firstMatch.datum();
if (!d || d.x === undefined || d.y === undefined || isNaN(d.x) || !isFinite(d.x) || isNaN(d.y) || !isFinite(d.y)) return; if (!d || d.x === undefined || d.y === undefined || isNaN(d.x) || !isFinite(d.x) || isNaN(d.y) || !isFinite(d.y)) return;
// Reset all active classes // Reset all active classes
rootGroup.selectAll(".node-pill").classed("nexus-node-active", false); rootGroup.selectAll(".node-pill").classed("nexus-node-active", false);
firstMatch.select(".node-pill").classed("nexus-node-active", true); firstMatch.select(".node-pill").classed("nexus-node-active", true);
@@ -502,7 +502,7 @@ export function setActiveNode(nodeId) {
return 20; return 20;
})); }));
} }
updateNodeAppearances(); updateNodeAppearances();
// Smooth transition to the first matching node // Smooth transition to the first matching node
@@ -514,10 +514,10 @@ export function setActiveNode(nodeId) {
export function dimNodes(activeNodeId) { export function dimNodes(activeNodeId) {
if (!node) return; if (!node) return;
node.transition().duration(500) node.transition().duration(500)
.style("opacity", d => (activeNodeId === null || d.id === activeNodeId) ? 1 : 0.4); .style("opacity", d => (activeNodeId === null || d.id === activeNodeId) ? 1 : 0.4);
if (link) { if (link) {
link.transition().duration(500) link.transition().duration(500)
.style("opacity", d => { .style("opacity", d => {
@@ -558,7 +558,7 @@ export function handleResize(containerId) {
svgElement.attr("viewBox", [0, 0, width, height]); svgElement.attr("viewBox", [0, 0, width, height]);
simulation.force("center", d3.forceCenter(width / 2, height / 2)); simulation.force("center", d3.forceCenter(width / 2, height / 2));
const prevMobileMode = isMobileMode; const prevMobileMode = isMobileMode;
isMobileMode = window.innerWidth < 768; isMobileMode = window.innerWidth < 768;
if (isMobileMode !== prevMobileMode) { if (isMobileMode !== prevMobileMode) {
@@ -1,7 +1,70 @@
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) { export function initSelectionListener(dotNetHelper, container) {
if (!container) return; if (!container) return;
console.log("[SelectionHandler] Initializing..."); console.log("[SelectionHandler] Initializing...");
// Clean up any existing listeners first
destroySelectionListener();
const handleSelection = () => { const handleSelection = () => {
const selection = window.getSelection(); const selection = window.getSelection();
@@ -16,26 +79,60 @@ export function initSelectionListener(dotNetHelper, container) {
const blockNode = node.closest('[id]'); const blockNode = node.closest('[id]');
if (blockNode) { 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;
dotNetHelper.invokeMethodAsync('HandleTextSelected', console.log("[SelectionHandler] Selection coords (first/last/combined):", topVal, bottomVal, combinedRect.left);
text,
blockNode.id, dotNetHelper.invokeMethodAsync('HandleTextSelected',
{ text,
Top: rect.top, blockNode.id,
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 { } else {
dotNetHelper.invokeMethodAsync('HandleSelectionCleared'); dotNetHelper.invokeMethodAsync('HandleSelectionCleared');
} }
}; };
// Use multiple triggers for maximum reliability const mouseUpHandler = () => setTimeout(handleSelection, 10);
document.addEventListener('selectionchange', handleSelection);
container.addEventListener('mouseup', () => 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();
}
+2 -1
View File
@@ -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); 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 DbInitializer.SeedAsync(services);
await TriggerBackgroundProcessingForUnindexedBooksAsync(services); await TriggerBackgroundProcessingForUnindexedBooksAsync(services);