spb/poche Public
Agent personnel 100 % on-device — SwiftUI + Apple Foundation Models + EventKit + SwiftData. Aucune API, aucun serveur.
Swift 100%
1//2// ConfirmationCardView.swift3// Poche4//5// Author: Simon-Pierre Boucher6// Contact: contact@spboucher.ai7//89import SwiftUI1011/// The confirmation card: what will be done, with resolved values in clear12/// text, and two choices. This friction is what makes a 3B agent usable —13/// never minimize it away (CLAUDE.md §6).14struct ConfirmationCardView: View {15 let action: PendingAction16 let onConfirm: () -> Void17 let onModify: () -> Void1819 var body: some View {20 VStack(alignment: .leading, spacing: Theme.spacingMedium) {21 HStack(spacing: Theme.spacingMedium) {22 RoundedRectangle(cornerRadius: 10)23 .fill(Theme.accentGradient)24 .frame(width: 36, height: 36)25 .overlay {26 Image(systemName: iconName)27 .font(.system(size: 16, weight: .semibold))28 .foregroundStyle(.white)29 }3031 VStack(alignment: .leading, spacing: 1) {32 Text(action.title)33 .font(.headline)34 Text("En attente de ta confirmation")35 .font(.caption)36 .foregroundStyle(.secondary)37 }38 }3940 VStack(alignment: .leading, spacing: Theme.spacingSmall + 2) {41 ForEach(action.fields, id: \.label) { field in42 HStack(alignment: .firstTextBaseline, spacing: Theme.spacingMedium) {43 Text(field.label)44 .font(.subheadline)45 .foregroundStyle(.secondary)46 .frame(width: 90, alignment: .leading)47 Text(field.value)48 .font(.subheadline.weight(.medium))49 }50 }51 }52 .padding(Theme.spacingMedium)53 .frame(maxWidth: .infinity, alignment: .leading)54 .background(Theme.background.opacity(0.6))55 .clipShape(RoundedRectangle(cornerRadius: Theme.cardCornerRadius - 8))5657 if case .failed(let message) = action.status {58 Label(message, systemImage: "exclamationmark.triangle.fill")59 .font(.footnote)60 .foregroundStyle(.red)61 }6263 HStack(spacing: Theme.spacingMedium) {64 Button(action: onConfirm) {65 Group {66 if action.status == .executing {67 ProgressView()68 .controlSize(.small)69 .tint(.white)70 } else {71 Text("Confirmer")72 .font(.subheadline.weight(.semibold))73 }74 }75 .foregroundStyle(.white)76 .frame(maxWidth: .infinity)77 .padding(.vertical, Theme.spacingMedium - 2)78 .background(Theme.accentGradient)79 .clipShape(Capsule())80 }81 .buttonStyle(.plain)82 .disabled(action.status == .executing)8384 Button(action: onModify) {85 Text("Modifier")86 .font(.subheadline.weight(.semibold))87 .foregroundStyle(.primary)88 .frame(maxWidth: .infinity)89 .padding(.vertical, Theme.spacingMedium - 2)90 .background(Theme.background)91 .clipShape(Capsule())92 }93 .buttonStyle(.plain)94 .disabled(action.status == .executing)95 }96 }97 .padding(Theme.spacingMedium + 2)98 .background(Theme.surface)99 .clipShape(RoundedRectangle(cornerRadius: Theme.cardCornerRadius))100 .overlay {101 RoundedRectangle(cornerRadius: Theme.cardCornerRadius)102 .strokeBorder(Theme.accentA.opacity(0.25))103 }104 .shadow(color: .black.opacity(0.10), radius: 14, y: 6)105 }106107 private var iconName: String {108 switch action.payload {109 case .reminder: "bell.fill"110 case .event: "calendar"111 case .note: "note.text"112 case .newTask: "checklist"113 case .taskUpdate: "pencil"114 }115 }116}117