Files
Will Miao 92820be4ad feat(first-run): gate the initial library scan behind a welcome page
Fresh installs no longer start a full library scan (with per-file SHA256
and sidecar writes) automatically at startup. Instead a welcome page
lists the configured model roots, estimates the scan cost from a quick
hash-free walk, and lets the user pick the sidecar storage layout and
UI language before confirming. Deferring shows an empty library; the
existing refresh button starts the scan later. Upgrading installs are
detected via a non-empty persisted cache snapshot (row-count check, so
the empty file created by scanner construction no longer masquerades
as an existing library) and keep the old behavior.

Bundled fixes surfaced by the new flow:

- broadcast an init-progress error when the startup scan fails instead
  of silently landing on an empty library; the initialization page now
  shows the error with a retry button
- skip the onboarding language modal/tutorial while the welcome or
  initialization page is up (its spotlight targets don't exist there,
  so the tour was silently auto-completed and never shown)
- promotional banners wait until onboarding is completed or skipped;
  alert banners (startup messages, cache health) are unaffected
- honor the hidden attribute in first-run/initialization CSS so the
  no-roots hint and error boxes don't render as empty frames
2026-10-25 09:02:57 +08:00

75 lines
3.3 KiB
HTML

<!-- First Run Welcome Component -->
<div class="first-run-container" id="firstRunContainer">
<div class="first-run-content">
<div class="first-run-language">
<label for="firstRunLanguage"><i class="fas fa-globe"></i> {{ t('firstRun.language.label') }}</label>
<select id="firstRunLanguage">
<option value="en">English</option>
<option value="zh-CN">简体中文</option>
<option value="zh-TW">繁體中文</option>
<option value="ja">日本語</option>
<option value="ko">한국어</option>
<option value="es">Español</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="ru">Русский</option>
</select>
</div>
<div class="first-run-header">
<h2 id="firstRunTitle">{{ t('firstRun.title') }}</h2>
<p class="first-run-subtitle" id="firstRunSubtitle">{{ t('firstRun.description') }}</p>
</div>
<div class="first-run-libraries" id="firstRunLibraries">
<!-- Library summary rows are rendered by first_run.js -->
</div>
<div class="first-run-estimate" id="firstRunEstimate" hidden>
<i class="fas fa-clock"></i>
<span id="firstRunEstimateText"></span>
</div>
<div class="first-run-no-roots" id="firstRunNoRoots" hidden>
<i class="fas fa-exclamation-triangle"></i>
<p id="firstRunNoRootsText"></p>
</div>
<div class="first-run-sidecar">
<h3>{{ t('firstRun.sidecar.title') }}</h3>
<label class="first-run-sidecar-option">
<input type="radio" name="sidecarStorageMode" value="alongside" checked>
<span class="first-run-sidecar-option-text">
<strong>{{ t('firstRun.sidecar.alongside.label') }}</strong>
<small>{{ t('firstRun.sidecar.alongside.description') }}</small>
</span>
</label>
<label class="first-run-sidecar-option">
<input type="radio" name="sidecarStorageMode" value="centralized">
<span class="first-run-sidecar-option-text">
<strong>{{ t('firstRun.sidecar.centralized.label') }}</strong>
<small>{{ t('firstRun.sidecar.centralized.description') }}</small>
</span>
</label>
<div class="first-run-sidecar-path" id="firstRunSidecarPathRow" hidden>
<input type="text" id="firstRunSidecarPath"
placeholder="{{ t('firstRun.sidecar.centralized.pathPlaceholder') }}">
</div>
</div>
<div class="first-run-actions">
<button type="button" class="primary-btn" id="firstRunStart">
<i class="fas fa-play"></i> {{ t('firstRun.actions.start') }}
</button>
<button type="button" class="secondary-btn" id="firstRunDefer">
{{ t('firstRun.actions.defer') }}
</button>
</div>
<div class="first-run-error" id="firstRunError" hidden>
<i class="fas fa-exclamation-circle"></i>
<span id="firstRunErrorText"></span>
</div>
</div>
</div>