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 · 98 lines swift
Raw Blame History
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