SPB Git

spb/zyquo-atlas Public License

The AI-native macOS web browser — every surface, intelligent.

Swift 75.2% JavaScript 22% Shell 2% Makefile 0.9%
3.6 KB · 90 lines swift
Raw Blame History
1//2//  ReaderView.swift3//  Zyquo Atlas4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//8//  Reader mode: a clean, themeable article view built from the tab's extracted9//  PageContext (Readability), with an optional AI summary at the top. Reader10//  typography (measure, size, spacing) follows the theme; honest fallback when11//  the page isn't an article.12//1314import SwiftUI1516struct ReaderView: View {17    @ObservedObject var tab: Tab18    @EnvironmentObject private var env: AppEnvironment19    @EnvironmentObject private var themeEngine: ThemeEngine20    private var theme: AtlasTheme { themeEngine.theme }2122    @State private var context: PageContext?23    @State private var loadError: String?24    @StateObject private var ai = AIService()2526    var body: some View {27        ScrollView {28            VStack(alignment: .leading, spacing: ZyquoSpacing.md) {29                if let ctx = context {30                    Text(ctx.title).font(.system(size: 30 * themeEngine.layout.uiScale, weight: .bold))31                        .foregroundStyle(theme.textPrimary)32                    if let byline = ctx.byline {33                        Text(byline).font(ZyquoFont.caption).foregroundStyle(theme.textSecondary)34                    }35                    summaryCard(ctx)36                    Divider().overlay(theme.border)37                    Text(ctx.markdown)38                        .font(.system(size: 17 * themeEngine.layout.uiScale))39                        .lineSpacing(8)40                        .foregroundStyle(theme.textPrimary)41                        .textSelection(.enabled)42                } else if let loadError {43                    Label(loadError, systemImage: "doc.plaintext")44                        .font(ZyquoFont.body()).foregroundStyle(theme.textSecondary)45                } else {46                    ProgressView().frame(maxWidth: .infinity).padding(ZyquoSpacing.xxl)47                }48            }49            .frame(maxWidth: 720, alignment: .leading)50            .frame(maxWidth: .infinity)51            .padding(.horizontal, ZyquoSpacing.xxl)52            .padding(.vertical, ZyquoSpacing.xl)53        }54        .background(theme.backgroundColor)55        .task(id: tab.id) { await load() }56    }5758    private func summaryCard(_ ctx: PageContext) -> some View {59        VStack(alignment: .leading, spacing: ZyquoSpacing.xs) {60            HStack {61                Label("AI summary", systemImage: "sparkles").font(ZyquoFont.caption)62                    .foregroundStyle(theme.accentIndigo)63                Spacer()64                if !ai.isStreaming && ai.output.isEmpty {65                    Button("Summarize", action: { summarize(ctx) })66                        .font(ZyquoFont.caption).foregroundStyle(theme.accent)67                }68            }69            if !ai.output.isEmpty {70                Text(ai.output).font(ZyquoFont.body()).foregroundStyle(theme.textPrimary).textSelection(.enabled)71            } else if ai.isStreaming {72                Text("Summarizing…").font(ZyquoFont.caption).foregroundStyle(theme.textSecondary)73            }74        }75        .padding(ZyquoSpacing.md)76        .background(RoundedRectangle(cornerRadius: ZyquoRadius.medium).fill(theme.accentSubtle))77    }7879    private func load() async {80        do { context = try await tab.extractPageContext() }81        catch { loadError = "Reader couldn't extract this page. It may be an app page or blocked." }82    }8384    private func summarize(_ ctx: PageContext) {85        guard let model = env.model(for: .standard),86              let key = try? env.vault.apiKey(for: model.provider) else { return }87        ai.run(.summarize, on: ctx, model: model, apiKey: key)88    }89}90