import SwiftUI import LChatCore // 🤖 iOS host for Eleven's native onboarding conversation (setup / welcome): // owns the model's lifecycle and wires the finish chips' thread hop into the // navigation path. Models are cached per thread so leaving and returning // resumes the same run (the flow itself also resumes from recorded state). struct ElevenChatScreen: View { let instance: SavedInstance let thread: ChatThread @ObservedObject var store: ThreadStore @Binding var path: [Route] @State private var model: ElevenConversationModel? @State private var showDiscover = false @State private var showQuickForm = false @State private var showSignIn = false /// The key/access step's "Sign in" chip presents the ordinary doors — /// its own model, so a sign-in taken here behaves exactly like one taken /// on the first-run screen (same adopt seam, same last-used marker). @StateObject private var signIn = HomeSignInModel() @ScaledMetric(relativeTo: .body) private var chatFontSize: CGFloat = 17 @AppStorage("nativeTheme") private var themeName = "classic" var body: some View { Group { if let model { ElevenChatView(model: model, theme: .named(themeName)) .environment(\.chatFontSize, chatFontSize) } else { ProgressView() } } .navigationTitle(thread.title) .navigationBarTitleDisplayMode(.inline) .sheet(isPresented: $showDiscover) { // The community directory (shared view; web: showDiscover) — the // welcome finish's "Browse open groups" chip lands here. NavigationStack { DiscoverGroupsView(store: store) { id in showDiscover = false path.append(.thread(id)) } } } .sheet(isPresented: $showQuickForm) { // "Fill in a form instead" — the whole conversation's questions at // once; submitting fast-fills the flow's records and it resumes. if let model { ElevenQuickFormView(model: model) } } .sheet(isPresented: $showSignIn) { // iOS 18 gates the ceremony (PRF); below it the chip is never // offered at all — signInToEleven stays nil, so the flow records // where the key actually lives instead of pitching. if #available(iOS 18.0, *) { NavigationStack { SignInFirstRun(model: signIn) } } } // The doors don't dismiss themselves (first-run swaps the whole // screen instead); close on a landed sign-in, and the conversation's // settle picks the session up from there. .onChange(of: signIn.phase) { _, phase in if phase.signedIn { showSignIn = false } } .onAppear { guard model == nil else { return } let m = ElevenConversationCache.shared.model(for: instance, thread: thread, myUserId: store.myUserId) m?.openQuickForm = { showQuickForm = true } m?.openThread = { id in path.append(.thread(id)) } m?.openAdmin = { Task { @MainActor in if let id = await store.openOrCreateAppThread(app: "admin", title: "Admin") { path.append(.thread(id)) } } } m?.openDiscover = { showDiscover = true } if #available(iOS 18.0, *) { m?.signInToEleven = { showSignIn = true } } // Outgoing payloads stamp the SavedInstance's name — persist a // flow rename, the way ServerSettingsSheet persists a manual one. m?.onNameSet = { name in var instances = InstancesStore.load() if let i = instances.firstIndex(where: { $0.id == instance.id }) { instances[i].screenName = name InstancesStore.save(instances) } } m?.start(roster: store.roster, threads: store.threads) model = m } } }