SPB Git

spb/poche Public

Agent personnel 100 % on-device — SwiftUI + Apple Foundation Models + EventKit + SwiftData. Aucune API, aucun serveur.

Swift 100%
4.3 KB · 117 lines swift
Raw Blame History
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