spb/poche Public
Agent personnel 100 % on-device — SwiftUI + Apple Foundation Models + EventKit + SwiftData. Aucune API, aucun serveur.
Swift 100%
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