SPB Git

spb/prisme Public MIT

Navigateur iOS intelligent — chaque page comprise localement avant d'être affichée. SwiftUI · WebKit · Foundation Models, 100% on-device.

Swift 96.7% JavaScript 3.3%
3.4 KB · 93 lines swift
Raw Blame History
1//2//  InsightStrip.swift3//  Prisme4//5//  Author: Simon-Pierre Boucher <contact@spboucher.ai>6//78import SwiftUI910/// The understanding strip: after a page settles, Prisme shows what it11/// read — instantly and deterministically (reading time, structure), then12/// enriched with the page kind and essence when the local model delivers.13/// This is the thesis made visible ("chaque page est comprise avant d'être14/// affichée") without the user hunting for a button. One tap opens the15/// reader; the × dismisses it for this page. Never a popup, never modal.16struct InsightStrip: View {17    let insight: PageInsight18    let digest: PageDigest?19    let accent: Color20    let onRead: () -> Void21    let onDismiss: () -> Void2223    /// Single tint for generated content, everywhere in the app (§7).24    private static let generated = Color(red: 0.55, green: 0.36, blue: 0.96)2526    var body: some View {27        HStack(spacing: Spacing.m) {28            Button(action: onRead) {29                HStack(spacing: Spacing.m) {30                    Image(systemName: "doc.plaintext")31                        .font(.subheadline.weight(.semibold))32                        .foregroundStyle(accent)3334                    VStack(alignment: .leading, spacing: 2) {35                        Text(headline)36                            .font(.footnote.weight(.semibold))37                            .foregroundStyle(.primary)38                        if let gist = digest?.gist, !gist.isEmpty {39                            HStack(alignment: .top, spacing: Spacing.xs) {40                                Image(systemName: "sparkles")41                                    .font(.caption2)42                                    .padding(.top, 2)43                                Text(gist)44                                    .font(.caption)45                                    .italic()46                                    .lineLimit(2)47                            }48                            .foregroundStyle(Self.generated)49                        }50                    }5152                    Spacer(minLength: Spacing.s)5354                    Text("Lire")55                        .font(.footnote.weight(.semibold))56                        .foregroundStyle(.white)57                        .padding(.horizontal, Spacing.m)58                        .padding(.vertical, Spacing.xs + 2)59                        .background(accent, in: Capsule())60                }61                .contentShape(Rectangle())62            }63            .buttonStyle(.plain)64            .accessibilityIdentifier("insight.read")6566            Button(action: onDismiss) {67                Image(systemName: "xmark")68                    .font(.caption.bold())69                    .foregroundStyle(.secondary)70                    .frame(width: 28, height: 28)71                    .contentShape(Circle())72            }73            .buttonStyle(.plain)74            .accessibilityIdentifier("insight.dismiss")75        }76        .padding(.horizontal, Spacing.l)77        .padding(.vertical, Spacing.m)78        .glassEffect(.regular, in: RoundedRectangle(cornerRadius: Radius.l))79    }8081    private var headline: String {82        var parts: [String] = []83        if let kind = digest?.kind {84            parts.append(kind.label)85        }86        parts.append("~\(insight.readingMinutes) min de lecture")87        if digest == nil, insight.sectionCount > 1 {88            parts.append("\(insight.sectionCount) sections")89        }90        return parts.joined(separator: " · ")91    }92}93