Initial commit: NexusArchitect Professional Workstation Overhaul
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
@using NexusReader.UI.Shared.Services
|
||||
@inject IQuizStateService QuizState
|
||||
|
||||
<div class="ai-bubble-container">
|
||||
<div class="ai-bubble">
|
||||
<div class="ai-avatar">
|
||||
<div class="avatar-ring"></div>
|
||||
<NexusIcon Name="robot" Size="48" Class="neon-pulse" />
|
||||
<div class="avatar-label">
|
||||
<span class="name">E-Czytnik</span>
|
||||
<span class="role">Asystent AI</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ai-content">
|
||||
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">@Dialogue</NexusTypography>
|
||||
|
||||
<div class="ai-actions">
|
||||
<button class="action-btn ghost" @onclick='() => HandleActionClick("more")'>Pokaż więcej informacji</button>
|
||||
<button class="action-btn neon-border" @onclick='() => HandleActionClick("quiz")'>Rozwiąż quiz</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="bubble-pointer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@code {
|
||||
[Parameter] public string ContextBlockId { get; set; } = string.Empty;
|
||||
[Parameter] public string Dialogue { get; set; } = string.Empty;
|
||||
[Parameter] public List<string> Actions { get; set; } = new();
|
||||
[Parameter] public EventCallback<string> OnActionTriggered { get; set; }
|
||||
|
||||
private bool _isQuizMode = false;
|
||||
|
||||
private async Task HandleActionClick(string action)
|
||||
{
|
||||
if (action.Contains("quiz", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_isQuizMode = true;
|
||||
QuizState.RequestQuiz(ContextBlockId);
|
||||
}
|
||||
|
||||
if (OnActionTriggered.HasDelegate)
|
||||
{
|
||||
await OnActionTriggered.InvokeAsync(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
.ai-bubble-container {
|
||||
margin: 2rem 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ai-bubble {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
background: rgba(18, 18, 18, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
max-width: 600px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ai-avatar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.avatar-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.avatar-label .name {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.avatar-label .role {
|
||||
font-size: 0.7rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.neon-pulse {
|
||||
color: var(--nexus-neon);
|
||||
filter: drop-shadow(0 0 8px var(--nexus-neon));
|
||||
animation: pulse 2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); filter: drop-shadow(0 0 8px var(--nexus-neon)); }
|
||||
50% { transform: scale(1.05); filter: drop-shadow(0 0 15px var(--nexus-neon)); }
|
||||
100% { transform: scale(1); filter: drop-shadow(0 0 8px var(--nexus-neon)); }
|
||||
}
|
||||
|
||||
.ai-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ai-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-family: var(--nexus-font-sans);
|
||||
}
|
||||
|
||||
.action-btn.ghost {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.action-btn.neon-border {
|
||||
background: rgba(0, 255, 153, 0.1);
|
||||
border: 1px solid var(--nexus-neon);
|
||||
color: var(--nexus-neon);
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 255, 153, 0.2);
|
||||
}
|
||||
|
||||
.bubble-pointer {
|
||||
position: absolute;
|
||||
right: -10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid transparent;
|
||||
border-left: 10px solid rgba(18, 18, 18, 0.95);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
@using NexusReader.UI.Shared.Services
|
||||
@inject IFocusModeService FocusMode
|
||||
|
||||
<aside class="intelligence-toolbar">
|
||||
<div class="toolbar-top">
|
||||
<button class="toolbar-item" title="Back">
|
||||
<NexusIcon Name="play" Size="20" Class="rotate-180" />
|
||||
</button>
|
||||
<button class="toolbar-item active" title="Chat">
|
||||
<NexusIcon Name="message-square" Size="20" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-middle">
|
||||
<button class="toolbar-item" title="Settings">
|
||||
<NexusIcon Name="settings" Size="20" />
|
||||
</button>
|
||||
<button class="toolbar-item" title="Bookmarks">
|
||||
<NexusIcon Name="bookmark" Size="20" />
|
||||
</button>
|
||||
<button class="toolbar-item" title="Search">
|
||||
<NexusIcon Name="search" Size="20" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-bottom">
|
||||
<button class="toolbar-item @(FocusMode.IsFocusModeActive ? "active focus-active" : "")"
|
||||
@onclick="FocusMode.ToggleAsync" title="Focus Mode (F)">
|
||||
<NexusIcon Name="target" Size="20" />
|
||||
</button>
|
||||
<button class="toolbar-item" title="Global Settings">
|
||||
<NexusIcon Name="settings" Size="20" />
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
FocusMode.OnFocusModeChanged += StateHasChanged;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
FocusMode.OnFocusModeChanged -= StateHasChanged;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
.intelligence-toolbar {
|
||||
width: 50px;
|
||||
height: 100%;
|
||||
background: #1a1a1a;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 0;
|
||||
align-items: center;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.toolbar-top, .toolbar-middle, .toolbar-bottom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.toolbar-item {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.toolbar-item:hover {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.toolbar-item.active {
|
||||
color: var(--nexus-neon);
|
||||
}
|
||||
|
||||
.toolbar-item.focus-active {
|
||||
filter: drop-shadow(0 0 5px var(--nexus-neon));
|
||||
}
|
||||
|
||||
.rotate-180 {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
@using MediatR
|
||||
@using NexusReader.Application.Queries.Quiz
|
||||
@using NexusReader.Application.Commands.Quiz
|
||||
@using NexusReader.Application.Abstractions.Services
|
||||
@inject IMediator Mediator
|
||||
@inject IPlatformService PlatformService
|
||||
|
||||
<div class="knowledge-check">
|
||||
<div class="quiz-header">
|
||||
<span class="header-title">Sprawdzian Wiedzy</span>
|
||||
<button class="expand-btn">⌵</button>
|
||||
</div>
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<div class="loading-state">Pobieranie pytań...</div>
|
||||
}
|
||||
else if (_quiz != null)
|
||||
{
|
||||
<div class="quiz-body">
|
||||
@foreach (var question in _quiz.Questions)
|
||||
{
|
||||
<div class="question-container">
|
||||
<p class="question-text">@question.Question</p>
|
||||
|
||||
<div class="options-list">
|
||||
@for (int i = 0; i < question.Options.Count; i++)
|
||||
{
|
||||
var index = i;
|
||||
var letter = (char)('A' + i);
|
||||
<button class="option-item @GetOptionClass(question, index)"
|
||||
@onclick="() => SelectOptionAsync(question, index)"
|
||||
disabled="@_states.ContainsKey(question)">
|
||||
<span class="option-letter">@letter)</span>
|
||||
<span class="option-text">@question.Options[index]</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="quiz-footer">
|
||||
<button class="submit-btn" disabled="@(!AllQuestionsAnswered())">Wyślij</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
@code {
|
||||
[Parameter] public string ContextBlockId { get; set; } = string.Empty;
|
||||
|
||||
private bool _isLoading = true;
|
||||
private QuizDto? _quiz;
|
||||
|
||||
private Dictionary<QuizQuestionDto, (int SelectedIndex, bool IsCorrect)> _states = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_isLoading = true;
|
||||
var query = new GetQuizQuestionsQuery(ContextBlockId);
|
||||
var result = await Mediator.Send(query);
|
||||
|
||||
if (result.IsSuccess)
|
||||
_quiz = result.Value;
|
||||
|
||||
_isLoading = false;
|
||||
}
|
||||
|
||||
private async Task SelectOptionAsync(QuizQuestionDto question, int index)
|
||||
{
|
||||
if (_states.ContainsKey(question)) return;
|
||||
|
||||
// Haptic feedback
|
||||
await PlatformService.VibrateAsync(40);
|
||||
|
||||
var cmd = new SubmitAnswerCommand(index, question.CorrectIndex);
|
||||
var res = await Mediator.Send(cmd);
|
||||
|
||||
_states[question] = (index, res.IsSuccess);
|
||||
|
||||
if (res.IsSuccess)
|
||||
await PlatformService.VibrateSuccessAsync();
|
||||
else
|
||||
await PlatformService.VibrateErrorAsync();
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private bool AllQuestionsAnswered()
|
||||
{
|
||||
return _quiz != null && _states.Count == _quiz.Questions.Count;
|
||||
}
|
||||
|
||||
|
||||
private string GetBlockClass(QuizQuestionDto question)
|
||||
{
|
||||
if (!_states.TryGetValue(question, out var state)) return "";
|
||||
return state.IsCorrect ? "state-correct" : "state-incorrect";
|
||||
}
|
||||
|
||||
private string GetOptionClass(QuizQuestionDto question, int index)
|
||||
{
|
||||
if (!_states.TryGetValue(question, out var state)) return "";
|
||||
|
||||
if (state.SelectedIndex == index)
|
||||
return state.IsCorrect ? "option-correct" : "option-incorrect";
|
||||
|
||||
if (state.IsCorrect == false && question.CorrectIndex == index)
|
||||
return "option-revealed-correct";
|
||||
|
||||
return "option-faded";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
.knowledge-check {
|
||||
padding: 1.5rem;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12px;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.quiz-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
font-family: var(--nexus-font-sans);
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #666;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.question-text {
|
||||
font-size: 0.95rem;
|
||||
color: #ccc;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.options-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 0.8rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.option-item:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.option-item.selected {
|
||||
border-color: var(--nexus-neon);
|
||||
background: rgba(0, 255, 153, 0.05);
|
||||
}
|
||||
|
||||
.option-letter {
|
||||
font-weight: 600;
|
||||
color: var(--nexus-neon);
|
||||
min-width: 25px;
|
||||
}
|
||||
|
||||
.option-text {
|
||||
font-size: 0.9rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.quiz-footer {
|
||||
margin-top: 1.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
padding: 0.6rem 2.5rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 20px;
|
||||
color: #888;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.submit-btn:not(:disabled) {
|
||||
background: var(--nexus-neon);
|
||||
color: #000;
|
||||
border-color: var(--nexus-neon);
|
||||
}
|
||||
|
||||
.option-correct {
|
||||
border-color: #00ff99 !important;
|
||||
background: rgba(0, 255, 153, 0.1) !important;
|
||||
}
|
||||
|
||||
.option-incorrect {
|
||||
border-color: #ff4444 !important;
|
||||
background: rgba(255, 68, 68, 0.1) !important;
|
||||
}
|
||||
Reference in New Issue
Block a user