SPB Git

spb/poche Public

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

Swift 100%
3.3 KB · 93 lines swift
Raw Blame History
1//2//  ComposerView.swift3//  Poche4//5//  Author:  Simon-Pierre Boucher6//  Contact: contact@spboucher.ai7//89import SwiftUI1011struct ComposerView: View {12    @Binding var text: String13    let isBusy: Bool14    let onSend: () -> Void1516    @FocusState private var isFocused: Bool17    @State private var dictation = DictationController()1819    private var canSend: Bool {20        !isBusy && !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty21    }2223    var body: some View {24        HStack(alignment: .bottom, spacing: Theme.spacingSmall + 2) {25            if dictation.isAvailable {26                micButton27            }2829            TextField("Écris à Poche…", text: $text, axis: .vertical)30                .lineLimit(1...5)31                .textFieldStyle(.plain)32                .focused($isFocused)33                .padding(.horizontal, Theme.spacingMedium + 4)34                .padding(.vertical, Theme.spacingMedium - 1)35                .background(Theme.surface)36                .clipShape(RoundedRectangle(cornerRadius: Theme.bubbleCornerRadius))37                .overlay {38                    RoundedRectangle(cornerRadius: Theme.bubbleCornerRadius)39                        .strokeBorder(40                            isFocused ? Theme.accentA.opacity(0.45) : Color.primary.opacity(0.07),41                            lineWidth: 142                        )43                }44                .onSubmit { if canSend { onSend() } }4546            Button(action: onSend) {47                Image(systemName: "arrow.up")48                    .font(.system(size: 17, weight: .bold))49                    .foregroundStyle(.white)50                    .frame(width: 40, height: 40)51                    .background {52                        if canSend {53                            Circle().fill(Theme.accentGradient)54                        } else {55                            Circle().fill(Color.secondary.opacity(0.35))56                        }57                    }58                    .shadow(59                        color: canSend ? Theme.accentA.opacity(0.35) : .clear,60                        radius: 6, y: 361                    )62            }63            .disabled(!canSend)64            .animation(.easeInOut(duration: 0.15), value: canSend)65            .accessibilityLabel("Envoyer")66        }67        .padding(.horizontal, Theme.spacingMedium)68        .padding(.top, Theme.spacingSmall)69        .padding(.bottom, Theme.spacingSmall + 2)70        .onChange(of: dictation.transcript) { _, transcript in71            if !transcript.isEmpty { text = transcript }72        }73    }7475    private var micButton: some View {76        Button {77            Task { await dictation.toggle() }78        } label: {79            Image(systemName: dictation.state == .recording ? "waveform.circle.fill" : "mic")80                .font(.system(size: dictation.state == .recording ? 30 : 19, weight: .medium))81                .foregroundStyle(82                    dictation.state == .recording83                        ? AnyShapeStyle(Theme.accentGradient)84                        : AnyShapeStyle(Color.secondary)85                )86                .frame(width: 40, height: 40)87                .symbolEffect(.pulse, isActive: dictation.state == .recording)88        }89        .disabled(isBusy)90        .accessibilityLabel(dictation.state == .recording ? "Arrêter la dictée" : "Dicter")91    }92}93