Ariver
2026-06-30 ac31349b32d89b2798cd20224fcdede91d6bfe05
privatevoice.src/frontend/src/components/onboarding/OnboardingView.svelte
@@ -10,12 +10,18 @@
  let progress = $state(0)
  let failed = $state(false)
  let downloadActive = $state(false)
  let cancelling = $state(false)
  let currentModelID = $state('')
  onMount(() => {
    Events.On('model:download-progress', (ev: any) => {
      const data = ev?.data
      if (data?.percent != null) {
        if (data?.modelID) {
          currentModelID = data.modelID
        }
        downloadActive = true
        cancelling = false
        failed = false
        statusText = t('onboarding.syncModel')
        progress = data.percent
@@ -29,6 +35,7 @@
      const data = ev?.data
      if (data?.status === 'ready') {
        downloadActive = false
        cancelling = false
        failed = false
        statusText = t('onboarding.complete')
        progress = 100
@@ -41,6 +48,7 @@
        }
      } else if (data?.status === 'error') {
        downloadActive = false
        cancelling = false
        statusText = t('onboarding.failed')
        detailText = data?.error || ''
        failed = true
@@ -52,7 +60,10 @@
  async function triggerDownload() {
    try {
      const current: any = await Call.ByName('voicesnap/services.EngineService.GetCurrentModelStatus')
      currentModelID = typeof current?.modelID === 'string' ? current.modelID : ''
      downloadActive = true
      cancelling = false
      failed = false
      statusText = t('onboarding.syncModel')
      detailText = ''
@@ -62,10 +73,28 @@
      detailText = t('onboarding.extracting')
    } catch (err: any) {
      downloadActive = false
      statusText = t('onboarding.failed')
      detailText = err?.message || String(err)
      cancelling = false
      if (isCancelError(err)) {
        statusText = t('onboarding.cancelled')
        detailText = t('onboarding.cancelledDetail')
      } else {
        statusText = t('onboarding.failed')
        detailText = err?.message || String(err)
      }
      failed = true
    }
  }
  function isCancelError(err: any): boolean {
    const text = String(err?.message || err || '').toLowerCase()
    return text.includes('context canceled') || text.includes('cancelled') || text.includes('canceled')
  }
  async function cancelDownload() {
    if (!downloadActive || cancelling) return
    cancelling = true
    statusText = t('onboarding.cancelling')
    await Call.ByName('voicesnap/services.EngineService.CancelModelDownload', currentModelID)
  }
</script>
@@ -86,6 +115,18 @@
    <!-- Progress bar -->
    <div class="progress-track">
      <div class="progress-fill" style="width: {progress}%" class:failed></div>
    </div>
    <div class="actions">
      {#if downloadActive}
        <button class="action cancel" onclick={cancelDownload} disabled={cancelling}>
          {cancelling ? t('onboarding.cancelling') : t('onboarding.cancel')}
        </button>
      {:else if failed}
        <button class="action primary" onclick={triggerDownload}>
          {t('onboarding.retry')}
        </button>
      {/if}
    </div>
  </div>
</div>
@@ -162,4 +203,34 @@
  .progress-fill.failed {
    background: var(--color-red);
  }
  .actions {
    margin-top: var(--spacing-lg);
    min-height: 32px;
  }
  .action {
    min-width: 96px;
    height: 32px;
    padding: 0 14px;
    border: none;
    border-radius: var(--radius-sm);
    color: white;
    font-size: var(--font-size-sm);
    font-weight: 500;
    cursor: pointer;
  }
  .action.primary {
    background: var(--color-blue);
  }
  .action.cancel {
    background: var(--color-red);
  }
  .action:disabled {
    cursor: not-allowed;
    opacity: 0.72;
  }
</style>