// // ChatView.swift // Poche // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // import SwiftUI /// Single screen: thread, pending confirmations, composer. /// No tabs, no settings menu on first launch (CLAUDE.md ยง6). struct ChatView: View { @Environment(AppDependencies.self) private var deps var body: some View { @Bindable var chat = deps.chat ZStack { AppBackground() VStack(spacing: 0) { header if chat.turns.isEmpty { WelcomeView { suggestion in Task { await chat.send(suggestion) } } } else { thread(chat: chat) } ForEach(deps.confirm.pending) { action in ConfirmationCardView( action: action, onConfirm: { Task { await deps.confirm.confirm(action) } }, onModify: { deps.confirm.dismiss(action) } ) .padding(.horizontal, Theme.spacingMedium) .padding(.bottom, Theme.spacingSmall) .transition(.move(edge: .bottom).combined(with: .opacity)) } ComposerView( text: $chat.draft, isBusy: chat.isBusy, onSend: { Task { await chat.send() } } ) } .animation(.spring(duration: 0.35), value: deps.confirm.pending.count) } .fontDesign(.rounded) .task { #if DEBUG // Deterministic driving hooks for simulator verification only. let env = ProcessInfo.processInfo.environment if env["POCHE_DEMO_THREAD"] != nil, chat.turns.isEmpty { chat.injectDemoTurns() } if env["POCHE_DEMO_CARD"] != nil, deps.confirm.pending.isEmpty { deps.confirm.propose(PendingAction(payload: .reminder( ReminderDraft(title: "Appeler le dentiste", due: .now.addingTimeInterval(86_400), list: nil) ))) } if let text = env["POCHE_AUTOSEND"], chat.turns.isEmpty { await chat.send(text) } #endif } } private var header: some View { VStack(spacing: 0) { HStack(spacing: Theme.spacingSmall + 2) { BrandMark(size: 30) Text("Poche") .font(.title3.weight(.bold)) Spacer() } .padding(.horizontal, Theme.spacingMedium + 4) .padding(.vertical, Theme.spacingSmall + 2) ContextGaugeView(ratio: deps.agent.usageRatio) .padding(.horizontal, Theme.spacingMedium) } } private func thread(chat: ChatViewModel) -> some View { ScrollView { LazyVStack(spacing: Theme.spacingMedium) { ForEach(chat.turns) { turn in TurnBubbleView(turn: turn) .transition(.move(edge: .bottom).combined(with: .opacity)) } } .padding(.horizontal, Theme.spacingMedium) .padding(.vertical, Theme.spacingLarge) .animation(.spring(duration: 0.35), value: chat.turns.count) } .defaultScrollAnchor(.bottom) .scrollDismissesKeyboard(.interactively) } }