spb/poche Public
Agent personnel 100 % on-device — SwiftUI + Apple Foundation Models + EventKit + SwiftData. Aucune API, aucun serveur.
Swift 100%
1//2// ChatView.swift3// Poche4//5// Author: Simon-Pierre Boucher6// Contact: contact@spboucher.ai7//89import SwiftUI1011/// Single screen: thread, pending confirmations, composer.12/// No tabs, no settings menu on first launch (CLAUDE.md §6).13struct ChatView: View {14 @Environment(AppDependencies.self) private var deps1516 var body: some View {17 @Bindable var chat = deps.chat1819 ZStack {20 AppBackground()2122 VStack(spacing: 0) {23 header2425 if chat.turns.isEmpty {26 WelcomeView { suggestion in27 Task { await chat.send(suggestion) }28 }29 } else {30 thread(chat: chat)31 }3233 ForEach(deps.confirm.pending) { action in34 ConfirmationCardView(35 action: action,36 onConfirm: { Task { await deps.confirm.confirm(action) } },37 onModify: { deps.confirm.dismiss(action) }38 )39 .padding(.horizontal, Theme.spacingMedium)40 .padding(.bottom, Theme.spacingSmall)41 .transition(.move(edge: .bottom).combined(with: .opacity))42 }4344 ComposerView(45 text: $chat.draft,46 isBusy: chat.isBusy,47 onSend: { Task { await chat.send() } }48 )49 }50 .animation(.spring(duration: 0.35), value: deps.confirm.pending.count)51 }52 .fontDesign(.rounded)53 .task {54 #if DEBUG55 // Deterministic driving hooks for simulator verification only.56 let env = ProcessInfo.processInfo.environment57 if env["POCHE_DEMO_THREAD"] != nil, chat.turns.isEmpty {58 chat.injectDemoTurns()59 }60 if env["POCHE_DEMO_CARD"] != nil, deps.confirm.pending.isEmpty {61 deps.confirm.propose(PendingAction(payload: .reminder(62 ReminderDraft(title: "Appeler le dentiste", due: .now.addingTimeInterval(86_400), list: nil)63 )))64 }65 if let text = env["POCHE_AUTOSEND"], chat.turns.isEmpty {66 await chat.send(text)67 }68 #endif69 }70 }7172 private var header: some View {73 VStack(spacing: 0) {74 HStack(spacing: Theme.spacingSmall + 2) {75 BrandMark(size: 30)76 Text("Poche")77 .font(.title3.weight(.bold))78 Spacer()79 }80 .padding(.horizontal, Theme.spacingMedium + 4)81 .padding(.vertical, Theme.spacingSmall + 2)8283 ContextGaugeView(ratio: deps.agent.usageRatio)84 .padding(.horizontal, Theme.spacingMedium)85 }86 }8788 private func thread(chat: ChatViewModel) -> some View {89 ScrollView {90 LazyVStack(spacing: Theme.spacingMedium) {91 ForEach(chat.turns) { turn in92 TurnBubbleView(turn: turn)93 .transition(.move(edge: .bottom).combined(with: .opacity))94 }95 }96 .padding(.horizontal, Theme.spacingMedium)97 .padding(.vertical, Theme.spacingLarge)98 .animation(.spring(duration: 0.35), value: chat.turns.count)99 }100 .defaultScrollAnchor(.bottom)101 .scrollDismissesKeyboard(.interactively)102 }103}104