diff --git a/frontend/src/lib/components/LogViewer.svelte b/frontend/src/lib/components/LogViewer.svelte index 2098e47a70..dbb8015459 100644 --- a/frontend/src/lib/components/LogViewer.svelte +++ b/frontend/src/lib/components/LogViewer.svelte @@ -367,7 +367,9 @@ > {#if isLoading}
- + + {#if tag}
{tagLabel ?? 'tag'}: {tag}
diff --git a/frontend/src/lib/components/ScriptEditor.svelte b/frontend/src/lib/components/ScriptEditor.svelte index 936d49f3ee..13f3de2a7a 100644 --- a/frontend/src/lib/components/ScriptEditor.svelte +++ b/frontend/src/lib/components/ScriptEditor.svelte @@ -2270,14 +2270,10 @@
{:else} {#key previewLayout} - + + {#if previewLayout === 'bottom' && !(debugMode && isDebuggableScript)}
diff --git a/frontend/src/lib/components/home/TutorialBanner.svelte b/frontend/src/lib/components/home/TutorialBanner.svelte index 9661d6738a..14d8b750c1 100644 --- a/frontend/src/lib/components/home/TutorialBanner.svelte +++ b/frontend/src/lib/components/home/TutorialBanner.svelte @@ -15,8 +15,20 @@ import { hasRoleAccess } from '$lib/tutorials/roleUtils' import { onMount } from 'svelte' - let isDismissed = $state(false) - let hasCompletedAny = $state(false) + type BannerState = 'hidden' | 'start' | 'new' + + // Deciding what to show needs an API round-trip, so the banner paints the state the last visit + // resolved to and reconciles once the sync answers. Guessing wrong once in a while beats + // reflowing the home page on every load; nothing cached means hidden, the direction that does + // not push the page down. + const TUTORIAL_BANNER_STATE_KEY = 'tutorial_banner_state' + + const cachedState = + getLocalSetting(TUTORIAL_BANNER_DISMISSED_KEY) === 'true' + ? 'hidden' + : getLocalSetting(TUTORIAL_BANNER_STATE_KEY) + let isDismissed = $state(cachedState !== 'start' && cachedState !== 'new') + let hasCompletedAny = $state(cachedState === 'new') /** * Get all tutorial indexes that are accessible to the current user based on their role. @@ -42,60 +54,81 @@ return indexes }) + function resolveState(state: BannerState) { + isDismissed = state === 'hidden' + hasCompletedAny = state === 'new' + // Last: persisting is best-effort, and a storage failure must not leave the banner stuck on + // whatever the cache said + storeLocalSetting(TUTORIAL_BANNER_STATE_KEY, state) + } + + // The banner is interactive while the initial sync is still in flight, so a dismiss or a skip + // can land mid-await. Once that happens the user's choice wins and the sync must not resurrect + // the banner. + let userHidBanner = false + + function hideBannerForUser() { + userHidBanner = true + resolveState('hidden') + } + onMount(async () => { + // Manually dismissed via the X button (soft dismiss, per-device). Checked before the network + // call so a dismissed banner can never flash back in. + if (getLocalSetting(TUTORIAL_BANNER_DISMISSED_KEY) === 'true') { + resolveState('hidden') + return + } + try { // Sync tutorial progress from backend first await syncTutorialsTodos() - - // Check if banner has been manually dismissed via X button (soft dismiss, per-device) - const manuallyDismissed = getLocalSetting(TUTORIAL_BANNER_DISMISSED_KEY) === 'true' - if (manuallyDismissed) { - isDismissed = true - return - } - - // Check if user deliberately skipped all tutorials (permanent dismiss, from backend) - if ($skippedAll) { - isDismissed = true - return - } - - // Safe to check tutorialsToDo here since we awaited syncTutorialsTodos() above - // Filter tutorialsToDo to only include tutorials accessible to the user's role - const remainingAccessibleTutorials = $tutorialsToDo.filter((index) => - accessibleTutorialIndexes.has(index) - ) - - // Calculate if user has completed at least one tutorial (for banner wording) - // This determines whether to show "New tutorial available!" or "Learn with interactive tutorials" - hasCompletedAny = remainingAccessibleTutorials.length < accessibleTutorialIndexes.size - - // Hide banner if all accessible tutorials are completed (but can reappear with new tutorials) - if (remainingAccessibleTutorials.length === 0) { - isDismissed = true - return - } - - // Show banner - user has accessible tutorials to complete - isDismissed = false } catch (error) { console.error('Failed to sync tutorial progress:', error) - // Fallback to manual dismissal check only if API call fails - isDismissed = getLocalSetting(TUTORIAL_BANNER_DISMISSED_KEY) === 'true' + // Keep whatever the last successful sync resolved to rather than guessing again + return } + + if (userHidBanner) { + return + } + + // Check if user deliberately skipped all tutorials (permanent dismiss, from backend) + if ($skippedAll) { + resolveState('hidden') + return + } + + // Safe to check tutorialsToDo here since we awaited syncTutorialsTodos() above + // Filter tutorialsToDo to only include tutorials accessible to the user's role + const remainingAccessibleTutorials = $tutorialsToDo.filter((index) => + accessibleTutorialIndexes.has(index) + ) + + // Hide banner if all accessible tutorials are completed (but can reappear with new tutorials) + if (remainingAccessibleTutorials.length === 0) { + resolveState('hidden') + return + } + + // Having completed at least one accessible tutorial switches the wording to + // "New tutorial available!" instead of "Learn with interactive tutorials" + resolveState( + remainingAccessibleTutorials.length < accessibleTutorialIndexes.size ? 'new' : 'start' + ) }) async function handleSkipAllTutorials() { // Skip all tutorials and set skipped_all flag in backend (permanent) await skipAllTodos() await syncTutorialsTodos() - // No need to set localStorage - backend skipped_all flag is the source of truth - isDismissed = true + // No need to set the dismissed flag - backend skipped_all flag is the source of truth + hideBannerForUser() } function dismissBanner() { storeLocalSetting(TUTORIAL_BANNER_DISMISSED_KEY, 'true') - isDismissed = true + hideBannerForUser() const actions: ToastAction[] = [ { diff --git a/frontend/src/lib/components/scriptEditor/LogPanel.svelte b/frontend/src/lib/components/scriptEditor/LogPanel.svelte index 008a987999..f253beadc7 100644 --- a/frontend/src/lib/components/scriptEditor/LogPanel.svelte +++ b/frontend/src/lib/components/scriptEditor/LogPanel.svelte @@ -223,9 +223,11 @@
{:else}
- + + {#if previewIsLoading} - + {:else} Test to see the result here {/if}