spb/poche Public
Agent personnel 100 % on-device — SwiftUI + Apple Foundation Models + EventKit + SwiftData. Aucune API, aucun serveur.
Swift 100%
1//2// TurnBubbleView.swift3// Poche4//5// Author: Simon-Pierre Boucher6// Contact: contact@spboucher.ai7//89import SwiftUI1011struct TurnBubbleView: View {12 let turn: ChatTurn1314 var body: some View {15 switch turn.role {16 case .system:17 systemNotice18 case .user:19 HStack {20 Spacer(minLength: 56)21 Text(turn.text)22 .foregroundStyle(.white)23 .padding(.horizontal, Theme.spacingMedium + 4)24 .padding(.vertical, Theme.spacingMedium - 2)25 .background(Theme.accentGradient)26 .clipShape(RoundedRectangle(cornerRadius: Theme.bubbleCornerRadius))27 .shadow(color: Theme.accentA.opacity(0.28), radius: 7, y: 3)28 }29 case .assistant:30 HStack {31 assistantBody32 .padding(.horizontal, Theme.spacingMedium + 4)33 .padding(.vertical, Theme.spacingMedium - 2)34 .background(Theme.surface)35 .clipShape(RoundedRectangle(cornerRadius: Theme.bubbleCornerRadius))36 .shadow(color: .black.opacity(0.06), radius: 5, y: 2)37 Spacer(minLength: 56)38 }39 }40 }4142 private var systemNotice: some View {43 HStack(spacing: Theme.spacingSmall) {44 Image(systemName: turn.text.hasPrefix("✓") ? "checkmark.circle.fill" : "info.circle")45 .foregroundStyle(turn.text.hasPrefix("✓") ? AnyShapeStyle(.green) : AnyShapeStyle(.secondary))46 Text(turn.text.hasPrefix("✓") ? String(turn.text.dropFirst(2)) : turn.text)47 .foregroundStyle(.secondary)48 }49 .font(.footnote.weight(.medium))50 .padding(.horizontal, Theme.spacingMedium + 2)51 .padding(.vertical, Theme.spacingSmall + 2)52 .background(.thinMaterial, in: Capsule())53 .frame(maxWidth: .infinity, alignment: .center)54 }5556 @ViewBuilder57 private var assistantBody: some View {58 switch turn.status {59 case .streaming where turn.text.isEmpty:60 TypingIndicator()61 .padding(.vertical, 4)62 case .refused, .failed:63 // Neutral tone: the conversation stays intact, rephrasing stays possible.64 Text(turn.text)65 .italic()66 .foregroundStyle(.secondary)67 default:68 Text(turn.text)69 .foregroundStyle(.primary)70 }71 }72}7374/// Three softly pulsing dots while the first token is on its way.75struct TypingIndicator: View {76 @State private var animating = false7778 var body: some View {79 HStack(spacing: 5) {80 ForEach(0..<3, id: \.self) { index in81 Circle()82 .fill(Theme.accentGradient)83 .frame(width: 7, height: 7)84 .scaleEffect(animating ? 1 : 0.55)85 .opacity(animating ? 1 : 0.4)86 .animation(87 .easeInOut(duration: 0.5)88 .repeatForever(autoreverses: true)89 .delay(Double(index) * 0.16),90 value: animating91 )92 }93 }94 .onAppear { animating = true }95 .accessibilityLabel("Poche écrit")96 }97}98