Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions Cotabby/Models/OnboardingTemplate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import Foundation
/// static value type should not capture. Keeping the data here and the rules in `Support/` keeps the
/// resolution pure and unit-testable.

/// One of the onboarding starting points the user chooses from. Quick, Everyday, and Powerful are
/// curated tiers; Custom is the neutral "set it up yourself" option that applies lean defaults so a
/// user who does not want a curated tier can still get going and configure the rest in Settings.
/// One of the onboarding starting points. Quick, Everyday, and Powerful are the curated tiers shown
/// as selectable cards (`curatedTiers`). Custom is the neutral "set it up yourself" option that
/// applies lean defaults; it is no longer its own card. The template step's "Set up later" button
/// applies it under the hood so a user who does not want a curated tier can still move forward and
/// configure the rest in Settings.
enum OnboardingTemplate: String, CaseIterable, Identifiable, Equatable, Sendable {
case quick
case everyday
Expand All @@ -22,6 +24,11 @@ enum OnboardingTemplate: String, CaseIterable, Identifiable, Equatable, Sendable

var id: String { rawValue }

/// The curated tiers shown as selectable cards in onboarding, in display order. Excludes
/// `.custom`, which is applied implicitly by the "Set up later" button rather than picked from a
/// card. Kept distinct from `allCases` so the pure recommender still reasons over every tier.
static let curatedTiers: [OnboardingTemplate] = [.quick, .everyday, .powerful]

var title: String {
switch self {
case .quick:
Expand Down
2 changes: 1 addition & 1 deletion Cotabby/UI/WelcomeTemplateStepView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ struct WelcomeTemplateStepView: View {
engineSelector

VStack(spacing: 10) {
ForEach(OnboardingTemplate.allCases) { template in
ForEach(OnboardingTemplate.curatedTiers) { template in
let availability = OnboardingTemplateRecommender.availability(
for: template,
hardware: hardware,
Expand Down
50 changes: 33 additions & 17 deletions Cotabby/UI/WelcomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ struct WelcomeView: View {
/// Reports the current step's raw index up to the coordinator so it can persist a resume point.
/// The wizard is re-shown from this step if the user is pulled out before finishing (see #314).
let onStepChange: (Int) -> Void
/// True when this user has completed a prior onboarding version. Custom keeps the user's existing
/// settings instead of overwriting them with template defaults, since they have already tuned
/// Cotabby and chose Custom precisely to preserve that.
/// True when this user has completed a prior onboarding version. The Custom path keeps the user's
/// existing settings instead of overwriting them with template defaults, since they have already
/// tuned Cotabby; advancing via "Set up later" preserves that.
let isReturningUser: Bool

@State private var step: WelcomeStep
Expand Down Expand Up @@ -216,9 +216,17 @@ extension WelcomeView {
WelcomeNavigation(
canGoBack: true,
canContinue: canContinueFromTemplate,
// With no curated tier chosen, the primary button becomes "Set up later" and applies
// the neutral Custom path under the hood, so the user is never blocked on a card.
continueTitle: selectedTemplate == nil ? "Set up later" : "Continue",
disabledHint: templateStepDisabledHint,
onBack: { step = .permissions },
onContinue: { step = .aboutYou }
onContinue: {
if selectedTemplate == nil {
applyTemplate(.custom)
}
step = .aboutYou
}
)
case .aboutYou:
WelcomeNavigation(
Expand Down Expand Up @@ -663,9 +671,13 @@ extension WelcomeView {
return modelDownloadManager.state(for: model)
}

/// Whether the template step's primary button is enabled. With no tier chosen it is always
/// enabled: the button reads "Set up later" and applies the neutral Custom path. With a tier
/// chosen, Apple Intelligence is immediately ready, while Open Source waits until that tier's
/// model download has at least started (it finishes in the background).
fileprivate var canContinueFromTemplate: Bool {
guard let template = selectedTemplate else {
return false
return true
}
let plan = resolvedPlan(for: template)
switch plan.engine {
Expand All @@ -680,17 +692,18 @@ extension WelcomeView {
}
}

/// Tooltip for the disabled primary button. Only reachable once a tier is chosen but its Open
/// Source download hasn't started yet — with no tier chosen the button is "Set up later" and
/// always enabled, so there is no longer a "pick something" hint.
fileprivate var templateStepDisabledHint: String {
selectedTemplate == nil
? "Choose a starting point to continue."
: "Hang on while your model starts downloading."
"Hang on while your model starts downloading."
}

fileprivate func resolvedPlan(for template: OnboardingTemplate) -> ResolvedTemplatePlan {
let base = OnboardingTemplateRecommender.resolvePlan(for: template, engine: selectedEngine)
// Returning users picking Custom on the OSS engine see their currently selected local model
// in the footer instead of the static template default, so the card matches the settings
// we will actually preserve in applyTemplate.
// Returning users on the Custom path with the OSS engine keep their currently selected local
// model instead of the static template default, so the done-step status and model activation
// reflect the settings applyTemplate actually preserves for them.
guard
template == .custom,
isReturningUser,
Expand Down Expand Up @@ -726,12 +739,13 @@ extension WelcomeView {
}
}

/// Applies a template's settings and starts its model download (if any). Selecting a card is the
/// user's explicit consent to download, so a multi-gigabyte fetch only ever starts from here.
/// Applies a template's settings and starts its model download (if any). Choosing a tier card —
/// or taking the "Set up later" path, which applies `.custom` — is the user's explicit consent to
/// download, so a multi-gigabyte fetch only ever starts from here.
fileprivate func applyTemplate(_ template: OnboardingTemplate) {
selectedTemplate = template

// Returning users picking Custom keep every setting they previously tuned. Skipping the
// Returning users on the Custom path keep every setting they previously tuned. Skipping the
// writes here (and the model download below) preserves their engine, word count, behavior
// toggles, and avoids re-triggering a multi-gigabyte fetch they already completed.
if template == .custom && isReturningUser {
Expand Down Expand Up @@ -827,11 +841,13 @@ struct WelcomeButton: View {
}
}

/// Continue navigation bar for middle wizard steps.
/// "Continue" can be disabled with a tooltip hint explaining what's needed.
/// Continue navigation bar for middle wizard steps. The primary button label defaults to "Continue"
/// but can be overridden (the template step shows "Set up later" when no tier is chosen). The button
/// can be disabled with a tooltip hint explaining what's needed.
struct WelcomeNavigation: View {
var canGoBack: Bool = false
var canContinue: Bool = true
var continueTitle: String = "Continue"
var disabledHint: String?
var onBack: (() -> Void)?
let onContinue: () -> Void
Expand All @@ -847,7 +863,7 @@ struct WelcomeNavigation: View {

Spacer(minLength: 0)

Button("Continue") {
Button(continueTitle) {
onContinue()
}
.buttonStyle(.borderedProminent)
Expand Down