Polish Novya UI with brand colors, first-load skeletons, and session transition overlay.

Keep chat pinned to the latest message while AI thinks or streams, and align buttons/inputs with the logo teal/blue focus treatment.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ar.badiei
2026-09-09 01:31:24 +03:30
co-authored by Cursor
parent 4ac0c16665
commit 8016b8f45e
56 changed files with 1874 additions and 529 deletions
+27 -4
View File
@@ -1,6 +1,11 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { api, hasSessionToken } from '@/services/backend';
import { getLatestByPhase } from '@/services/assessmentService';
import { getProgramProgress } from '@/services/programService';
import { PROGRAM_SESSION_COUNT } from '@/data/program/sessions';
/** Sessions 1..(N-1) done ⇒ learner is on final session; post-test may open manually. */
const POST_TEST_UNLOCK_MIN_COMPLETED_SESSIONS = PROGRAM_SESSION_COUNT - 1;
export type LearningStep =
| 'registered'
@@ -65,6 +70,8 @@ export function useLearningJourney(userId?: string) {
isCompleted: boolean;
} | null>(null);
const [loading, setLoading] = useState(true);
/** Skeleton / blocking load only on first fetch — silent on reloadProgress. */
const hasLoadedOnceRef = useRef(false);
const mapBackendJourney = useCallback((journey: {
currentStep: LearningStep;
@@ -118,12 +125,16 @@ export function useLearningJourney(userId?: string) {
return;
}
setLoading(true);
const isInitialLoad = !hasLoadedOnceRef.current;
if (isInitialLoad) {
setLoading(true);
}
if (useLocalOnly) {
const local = await mergeAssessmentCompletion(readLocalProgress(userId));
setProgress(local);
setBackendPermissions(null);
hasLoadedOnceRef.current = true;
setLoading(false);
return;
}
@@ -154,10 +165,15 @@ export function useLearningJourney(userId?: string) {
setProgress(merged);
setBackendPermissions(null);
} finally {
hasLoadedOnceRef.current = true;
setLoading(false);
}
}, [mapBackendJourney, mergeAssessmentCompletion, userId, useLocalOnly]);
useEffect(() => {
hasLoadedOnceRef.current = false;
}, [userId]);
useEffect(() => {
loadProgress();
}, [loadProgress]);
@@ -239,17 +255,24 @@ export function useLearningJourney(userId?: string) {
}, [progress, saveProgress, userId]);
const derivedPermissions = useMemo(() => {
const completedSessions = userId
? getProgramProgress(userId).completedSessions.length
: 0;
const reachedFinalSession =
Boolean(progress?.training_completed_at) ||
completedSessions >= POST_TEST_UNLOCK_MIN_COMPLETED_SESSIONS;
return {
canTakePreTest: !progress?.pre_test_completed_at,
canOpenTraining: Boolean(progress?.pre_test_completed_at),
canTakePostTest: Boolean(
progress?.pre_test_completed_at &&
progress?.training_completed_at &&
reachedFinalSession &&
!progress?.post_test_completed_at
),
isCompleted: Boolean(progress?.post_test_completed_at)
};
}, [progress]);
}, [progress, userId]);
const permissions = backendPermissions ?? derivedPermissions;