Ariver
2026-06-30 ac31349b32d89b2798cd20224fcdede91d6bfe05
privatevoice.src/frontend/src/components/onboarding/OnboardingView.svelte
@@ -9,11 +9,21 @@
  let detailText = $state('')
  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
        const downloadedMB = Math.floor((data.downloaded || 0) / 1024 / 1024)
        const totalMB = Math.floor((data.total || 0) / 1024 / 1024)
@@ -24,13 +34,21 @@
    Events.On('engine:status', (ev: any) => {
      const data = ev?.data
      if (data?.status === 'ready') {
        downloadActive = false
        cancelling = false
        failed = false
        statusText = t('onboarding.complete')
        progress = 100
      } else if (data?.status === 'need_model') {
        statusText = t('onboarding.failed')
        detailText = data?.error || ''
        failed = true
        const error = typeof data?.error === 'string' ? data.error.trim() : ''
        if (error && !downloadActive) {
          statusText = t('onboarding.failed')
          detailText = error
          failed = true
        }
      } else if (data?.status === 'error') {
        downloadActive = false
        cancelling = false
        statusText = t('onboarding.failed')
        detailText = data?.error || ''
        failed = true
@@ -42,20 +60,41 @@
  async function triggerDownload() {
    try {
      const config: any = await Call.ByName('voicesnap/services.ConfigService.GetConfig')
      if (config) {
        await Call.ByName('voicesnap/services.EngineService.DownloadModel',
          config.ModelDownloadUrl,
          config.FallbackModelDownloadUrl
        )
        statusText = t('onboarding.optimizing')
        detailText = t('onboarding.extracting')
      }
      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 = ''
      await Call.ByName('voicesnap/services.EngineService.DownloadCurrentModel')
      downloadActive = false
      statusText = t('onboarding.optimizing')
      detailText = t('onboarding.extracting')
    } catch (err: any) {
      statusText = t('onboarding.failed')
      detailText = err?.message || String(err)
      downloadActive = false
      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>
@@ -76,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>
@@ -152,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>