From e3a33f53b792ce3323f53fd676d59ed5f42521e4 Mon Sep 17 00:00:00 2001 From: Alex Nork Date: Thu, 12 Feb 2026 21:04:03 -0500 Subject: [PATCH] Add driver/passenger capabilities briefing step for first-meeting onboarding Introduce CapabilitiesBriefingView (step 3) with typewriter-animated framing text using a driver/passenger metaphor, and CapabilitiesModalView showing what the assistant can do, won't do, and how control works. Wire step 3 in FirstMeetingFlowView. Part of #1340. Closes #1345. Co-Authored-By: Claude Opus 4.6 --- .../CapabilitiesBriefingView.swift | 102 +++++++++++++++ .../FirstMeeting/CapabilitiesModalView.swift | 116 ++++++++++++++++++ .../FirstMeeting/FirstMeetingFlowView.swift | 4 +- 3 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/CapabilitiesBriefingView.swift create mode 100644 clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/CapabilitiesModalView.swift diff --git a/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/CapabilitiesBriefingView.swift b/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/CapabilitiesBriefingView.swift new file mode 100644 index 00000000000..970e483b38f --- /dev/null +++ b/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/CapabilitiesBriefingView.swift @@ -0,0 +1,102 @@ +import SwiftUI + +@MainActor +struct CapabilitiesBriefingView: View { + @Bindable var state: OnboardingState + var onComplete: () -> Void + + @State private var firstParagraphDone = false + @State private var showSecondParagraph = false + @State private var showButtons = false + @State private var showCapabilitiesModal = false + + private let firstText = "Okay, I think I\u{2019}ve got a good sense of where to start. Before we dive in \u{2014} quick road trip safety briefing." + private let secondText = "For now, think of it like you\u{2019}re driving and I\u{2019}m the passenger. I can navigate, handle stuff on my phone, keep us on track \u{2014} but you\u{2019}re steering." + + var body: some View { + HStack(alignment: .center, spacing: VSpacing.xxxl) { + // Creature on the left, small like in interview step + CreatureView(visible: true, animated: false) + .scaleEffect(0.5) + .frame(width: 200, height: 200) + + OnboardingPanel { + VStack(alignment: .leading, spacing: VSpacing.xl) { + // Framing text + VStack(alignment: .leading, spacing: VSpacing.lg) { + TypewriterText( + fullText: firstText, + speed: 0.03, + font: VFont.body, + onComplete: { + firstParagraphDone = true + withAnimation(.easeOut(duration: 0.4)) { + showSecondParagraph = true + } + } + ) + + if showSecondParagraph { + TypewriterText( + fullText: secondText, + speed: 0.03, + font: VFont.body, + onComplete: { + withAnimation(.easeOut(duration: 0.5)) { + showButtons = true + } + } + ) + .transition(.opacity.combined(with: .offset(y: 6))) + } + } + + // Action buttons + if showButtons { + VStack(spacing: VSpacing.md) { + OnboardingButton( + title: "Got it, let\u{2019}s go", + style: .primary, + fadeIn: true, + fadeDelay: 0.1 + ) { + state.capabilitiesBriefingShown = true + onComplete() + } + + OnboardingButton( + title: "See what I can do", + style: .ghost, + fadeIn: true, + fadeDelay: 0.3 + ) { + showCapabilitiesModal = true + } + } + .transition(.opacity.combined(with: .offset(y: 8))) + } + } + } + .frame(maxWidth: 420) + } + .sheet(isPresented: $showCapabilitiesModal) { + CapabilitiesModalView() + } + } +} + +#Preview { + ZStack { + MeadowBackground() + CapabilitiesBriefingView( + state: { + let s = OnboardingState() + s.currentStep = 3 + s.assistantName = "Velly" + return s + }(), + onComplete: {} + ) + } + .frame(width: 1366, height: 849) +} diff --git a/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/CapabilitiesModalView.swift b/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/CapabilitiesModalView.swift new file mode 100644 index 00000000000..6a658450a91 --- /dev/null +++ b/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/CapabilitiesModalView.swift @@ -0,0 +1,116 @@ +import SwiftUI + +@MainActor +struct CapabilitiesModalView: View { + @Environment(\.dismiss) private var dismiss + + var body: some View { + VStack(spacing: 0) { + ScrollView { + VStack(alignment: .leading, spacing: VSpacing.xxl) { + sectionView( + icon: "sparkles", + iconColor: Emerald._500, + title: "What I can do", + items: [ + "Browse the web and search for information", + "Read, write, and organize files", + "Manage tasks and reminders", + "Help with email drafts and replies", + "Take actions in apps on your behalf", + ] + ) + + sectionView( + icon: "shield.lefthalf.filled", + iconColor: Rose._500, + title: "What I won\u{2019}t do", + items: [ + "Act without asking when something\u{2019}s irreversible", + "Access your accounts without permission \u{2014} I\u{2019}ll always ask first", + "Store sensitive information like passwords", + "Make decisions that should be yours", + ] + ) + + sectionView( + icon: "car.fill", + iconColor: Violet._500, + title: "How control works", + items: [ + "You\u{2019}re always in the driver\u{2019}s seat", + "I\u{2019}ll ask before doing anything big", + "You can take over anytime", + "Say \u{201C}stop\u{201D} or press Escape to halt any action", + ] + ) + } + .padding(.horizontal, VSpacing.xxl) + .padding(.top, VSpacing.xxl) + .padding(.bottom, VSpacing.lg) + } + + Divider() + .background(VColor.surfaceBorder.opacity(0.4)) + + VButton(label: "Got it", style: .primary, isFullWidth: true) { + dismiss() + } + .padding(.horizontal, VSpacing.xxl) + .padding(.vertical, VSpacing.lg) + } + .frame(width: 400, minHeight: 480) + .background( + RoundedRectangle(cornerRadius: VRadius.xl) + .fill(.ultraThinMaterial) + .overlay( + RoundedRectangle(cornerRadius: VRadius.xl) + .fill(Meadow.panelBackground) + ) + .overlay( + RoundedRectangle(cornerRadius: VRadius.xl) + .stroke(Meadow.panelBorder, lineWidth: 1) + ) + ) + } + + // MARK: - Section Builder + + private func sectionView( + icon: String, + iconColor: Color, + title: String, + items: [String] + ) -> some View { + VStack(alignment: .leading, spacing: VSpacing.md) { + HStack(spacing: VSpacing.sm) { + Image(systemName: icon) + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(iconColor) + Text(title) + .font(VFont.headline) + .foregroundColor(VColor.textPrimary) + } + + VStack(alignment: .leading, spacing: VSpacing.sm) { + ForEach(items, id: \.self) { item in + HStack(alignment: .top, spacing: VSpacing.sm) { + Text("\u{2022}") + .font(VFont.body) + .foregroundColor(VColor.textMuted) + Text(item) + .font(VFont.body) + .foregroundColor(VColor.textSecondary) + .fixedSize(horizontal: false, vertical: true) + } + } + } + .padding(.leading, VSpacing.xs) + } + } +} + +#Preview { + CapabilitiesModalView() + .frame(width: 420, height: 560) +} diff --git a/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/FirstMeetingFlowView.swift b/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/FirstMeetingFlowView.swift index c5c6b98c1cb..47336a8c267 100644 --- a/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/FirstMeetingFlowView.swift +++ b/clients/macos/vellum-assistant/Features/Onboarding/FirstMeeting/FirstMeetingFlowView.swift @@ -47,9 +47,7 @@ struct FirstMeetingFlowView: View { .font(VFont.headline) .foregroundColor(VColor.textPrimary) case 3: - Text("Capabilities briefing — coming soon") - .font(VFont.headline) - .foregroundColor(VColor.textPrimary) + CapabilitiesBriefingView(state: state, onComplete: { state.advance() }) case 4: Text("Observation mode — coming soon") .font(VFont.headline)