| 1 | import SwiftUI |
| 2 | |
| 3 | // 🤖 The native quick form ("Fill in a form instead"): every onboarding |
| 4 | // question at once, submitted as a fast-fill through |
| 5 | // ElevenConversationModel.applyQuickForm — the same verbs and step records the |
| 6 | // conversation would produce, after which the flow re-enters and walks itself |
| 7 | // to the finish. Native flows skip the sign-in (access is the Eleven account) |
| 8 | // and the photo step, so the primary button is Save, not Sign in: welcome asks |
| 9 | // only the name; setup adds the space name, email, invites, and the extras. |
| 10 | // Shared logic here; the hosts (ElevenChatScreen / ElevenConversationPane) |
| 11 | // present it as each platform's sheet. |
| 12 | public struct ElevenQuickFormView: View { |
| 13 | @ObservedObject var model: ElevenConversationModel |
| 14 | @Environment(\.dismiss) private var dismiss |
| 15 | @State private var name: String |
| 16 | @State private var spaceName: String |
| 17 | @State private var email = "" |
| 18 | @State private var membersInvite = true |
| 19 | @State private var ai = true |
| 20 | @State private var bots = true |
| 21 | @State private var apps = true |
| 22 | @State private var error: String? |
| 23 | @State private var saving = false |
| 24 | |
| 25 | public init(model: ElevenConversationModel) { |
| 26 | self.model = model |
| 27 | _name = State(initialValue: model.instance.screenName) |
| 28 | _spaceName = State(initialValue: model.quickFormSpaceName) |
| 29 | } |
| 30 | |
| 31 | private var title: String { |
| 32 | model.quickFormIsSetup ? "Set up your space" : "Introduce yourself" |
| 33 | } |
| 34 | |
| 35 | public var body: some View { |
| 36 | #if os(macOS) |
| 37 | // Mac sheet chrome: title, the grouped form, then the button row — |
| 38 | // Cancel left, the prominent default action right. |
| 39 | VStack(spacing: 0) { |
| 40 | Text(title) |
| 41 | .font(.headline) |
| 42 | .padding(.top, 14) |
| 43 | .padding(.bottom, 4) |
| 44 | formBody |
| 45 | .formStyle(.grouped) |
| 46 | Divider() |
| 47 | HStack { |
| 48 | Button("Cancel") { dismiss() } |
| 49 | .keyboardShortcut(.cancelAction) |
| 50 | Spacer() |
| 51 | Button("Save", action: submit) |
| 52 | .keyboardShortcut(.defaultAction) |
| 53 | .buttonStyle(.borderedProminent) |
| 54 | .disabled(saving) |
| 55 | } |
| 56 | .padding(12) |
| 57 | } |
| 58 | .frame(minWidth: 400, minHeight: model.quickFormIsSetup ? 540 : 220) |
| 59 | #else |
| 60 | NavigationStack { |
| 61 | formBody |
| 62 | .navigationTitle(title) |
| 63 | .navigationBarTitleDisplayMode(.inline) |
| 64 | .toolbar { |
| 65 | ToolbarItem(placement: .cancellationAction) { |
| 66 | Button("Cancel") { dismiss() } |
| 67 | } |
| 68 | ToolbarItem(placement: .confirmationAction) { |
| 69 | Button("Save", action: submit) |
| 70 | .disabled(saving) |
| 71 | } |
| 72 | } |
| 73 | } |
| 74 | #endif |
| 75 | } |
| 76 | |
| 77 | private var formBody: some View { |
| 78 | Form { |
| 79 | Section { |
| 80 | TextField("Your name", text: $name) |
| 81 | .accessibilityLabel("Your name") |
| 82 | } |
| 83 | if model.quickFormIsSetup { |
| 84 | Section("Your space") { |
| 85 | TextField("Space name", text: $spaceName) |
| 86 | .accessibilityLabel("Space name") |
| 87 | TextField("Contact email (optional)", text: $email) |
| 88 | .accessibilityLabel("Contact email (optional)") |
| 89 | #if os(iOS) |
| 90 | .keyboardType(.emailAddress) |
| 91 | .textInputAutocapitalization(.never) |
| 92 | .autocorrectionDisabled() |
| 93 | #endif |
| 94 | } |
| 95 | Section { |
| 96 | Toggle("Members can invite people", isOn: $membersInvite) |
| 97 | } footer: { |
| 98 | Text("Otherwise, new faces only arrive through you.") |
| 99 | } |
| 100 | Section("Extras") { |
| 101 | VStack(alignment: .leading, spacing: 3) { |
| 102 | Toggle("AI", isOn: $ai) |
| 103 | // The one-line reassurance, right under the first (and |
| 104 | // scariest-sounding) switch. |
| 105 | Text("Everything essential works with any of these off — change your mind anytime in Admin.") |
| 106 | .font(.footnote) |
| 107 | .foregroundStyle(.secondary) |
| 108 | } |
| 109 | Toggle("Bots", isOn: $bots) |
| 110 | Toggle("Apps", isOn: $apps) |
| 111 | } |
| 112 | } |
| 113 | if let error { |
| 114 | // Inline, occupying layout — never a floating overlay; the |
| 115 | // words carry the state (color is decoration, not the signal). |
| 116 | Section { |
| 117 | Text(error) |
| 118 | .font(.callout) |
| 119 | .foregroundStyle(.red) |
| 120 | .accessibilityLabel("Error: \(error)") |
| 121 | } |
| 122 | } |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | private func submit() { |
| 127 | // The same required checks the conversational steps enforce. |
| 128 | if cleanNameAnswer(name).isEmpty { |
| 129 | error = "Add your name to continue." |
| 130 | return |
| 131 | } |
| 132 | if model.quickFormIsSetup, spaceName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { |
| 133 | error = "Give your space a name to continue." |
| 134 | return |
| 135 | } |
| 136 | saving = true |
| 137 | var v = QuickFormValues() |
| 138 | v.name = name |
| 139 | v.spaceName = spaceName |
| 140 | v.email = email |
| 141 | v.membersInvite = membersInvite |
| 142 | v.ai = ai |
| 143 | v.bots = bots |
| 144 | v.apps = apps |
| 145 | Task { @MainActor in |
| 146 | if let msg = await model.applyQuickForm(v) { |
| 147 | error = msg |
| 148 | saving = false |
| 149 | } else { |
| 150 | dismiss() |
| 151 | } |
| 152 | } |
| 153 | } |
| 154 | } |
| 155 | |