spb/zyquo-atlas Public License
The AI-native macOS web browser — every surface, intelligent.
Swift 75.2%
JavaScript 22%
Shell 2%
Makefile 0.9%
1//2// HistoryView.swift3// Zyquo Atlas4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// Full-text history (⌘Y): grouped by day, searchable, with delete controls and9// "ask AI about my history" — the model receives only titles/hosts (never page10// content), on explicit user action, via the shared provider layer.11//1213import SwiftUI1415struct HistoryView: View {16 let onOpen: (String) -> Void17 @EnvironmentObject private var env: AppEnvironment18 @EnvironmentObject private var themeEngine: ThemeEngine19 @Environment(\.dismiss) private var dismiss20 private var theme: AtlasTheme { themeEngine.theme }2122 @State private var query = ""23 @State private var askText = ""24 @StateObject private var ai = AIService()2526 var body: some View {27 VStack(spacing: 0) {28 header29 Divider().overlay(theme.border)30 askBar31 Divider().overlay(theme.border)32 list33 }34 .frame(width: 660, height: 620)35 .background(theme.backgroundColor)36 }3738 private var header: some View {39 HStack {40 Label("History", systemImage: "clock").font(ZyquoFont.title).foregroundStyle(theme.textPrimary)41 Spacer()42 searchField43 Menu {44 Button("Clear last hour") { env.history.deleteRange(since: Date().addingTimeInterval(-3600)) }45 Button("Clear today") { env.history.deleteRange(since: Calendar.current.startOfDay(for: Date())) }46 Button("Clear all", role: .destructive) { env.history.clearAll() }47 } label: { Image(systemName: "trash") }48 .menuStyle(.borderlessButton).frame(width: 44).foregroundStyle(theme.textSecondary)49 Button("Done") { dismiss() }.foregroundStyle(theme.accent)50 }51 .padding(ZyquoSpacing.md)52 .background(theme.surface)53 }5455 private var searchField: some View {56 HStack(spacing: ZyquoSpacing.xs) {57 Image(systemName: "magnifyingglass").font(.system(size: 11)).foregroundStyle(theme.textTertiary)58 TextField("Search history", text: $query).textFieldStyle(.plain).font(ZyquoFont.control)59 .foregroundStyle(theme.textPrimary).frame(width: 180)60 }61 .padding(.horizontal, ZyquoSpacing.sm).frame(height: 28)62 .background(RoundedRectangle(cornerRadius: ZyquoRadius.small).fill(theme.surfaceSecondary))63 }6465 private var askBar: some View {66 VStack(alignment: .leading, spacing: ZyquoSpacing.xs) {67 HStack(spacing: ZyquoSpacing.xs) {68 Image(systemName: "sparkles").foregroundStyle(theme.accentIndigo)69 TextField("Ask AI about your history (e.g. “that article about maps I read last week”)", text: $askText)70 .textFieldStyle(.plain).font(ZyquoFont.body())71 .foregroundStyle(theme.textPrimary)72 .onSubmit(runAsk)73 if ai.isStreaming { Button("Stop") { ai.cancel() }.font(ZyquoFont.caption).foregroundStyle(theme.danger) }74 else { Button("Ask", action: runAsk).font(ZyquoFont.caption).foregroundStyle(theme.accent) }75 }76 if !ai.output.isEmpty || ai.errorText != nil {77 ScrollView {78 Text(ai.errorText ?? ai.output)79 .font(ZyquoFont.body()).foregroundStyle(ai.errorText != nil ? theme.danger : theme.textPrimary)80 .frame(maxWidth: .infinity, alignment: .leading).textSelection(.enabled)81 }82 .frame(maxHeight: 120)83 }84 }85 .padding(ZyquoSpacing.md)86 }8788 private var list: some View {89 ScrollView {90 LazyVStack(alignment: .leading, spacing: 0, pinnedViews: [.sectionHeaders]) {91 ForEach(env.history.grouped(query), id: \.day) { group in92 Section {93 ForEach(group.items) { entry in94 row(entry)95 Divider().overlay(theme.border)96 }97 } header: {98 Text(dayLabel(group.day)).font(ZyquoFont.caption).foregroundStyle(theme.textTertiary)99 .frame(maxWidth: .infinity, alignment: .leading)100 .padding(.horizontal, ZyquoSpacing.md).padding(.vertical, ZyquoSpacing.xs)101 .background(theme.backgroundColor)102 }103 }104 if env.history.entries.isEmpty {105 Text("No history yet").font(ZyquoFont.caption).foregroundStyle(theme.textTertiary)106 .frame(maxWidth: .infinity).padding(ZyquoSpacing.xxl)107 }108 }109 }110 }111112 private func row(_ entry: HistoryEntry) -> some View {113 HStack(spacing: ZyquoSpacing.sm) {114 Image(systemName: "globe").font(.system(size: 11)).foregroundStyle(theme.textTertiary)115 VStack(alignment: .leading, spacing: 1) {116 Text(entry.title).font(ZyquoFont.body(size: 12.5)).lineLimit(1).foregroundStyle(theme.textPrimary)117 Text(entry.host).font(ZyquoFont.caption).lineLimit(1).foregroundStyle(theme.textTertiary)118 }119 Spacer()120 Text(HistoryService.relative(entry.lastVisit)).font(ZyquoFont.caption).foregroundStyle(theme.textTertiary)121 Button { env.history.delete(entry.id) } label: { Image(systemName: "xmark") }122 .buttonStyle(.plain).foregroundStyle(theme.textTertiary)123 }124 .padding(.horizontal, ZyquoSpacing.md).padding(.vertical, ZyquoSpacing.xs)125 .contentShape(Rectangle())126 .onTapGesture { onOpen(entry.url); dismiss() }127 }128129 private func runAsk() {130 let q = askText.trimmingCharacters(in: .whitespacesAndNewlines)131 guard !q.isEmpty, let model = env.model(for: .standard) else { return }132 guard let key = try? env.vault.apiKey(for: model.provider) else {133 askText = ""; return134 }135 let context = env.history.aiContext()136 let prompt = """137 Here is a list of the user's recent browsing history (title — site — when). \138 Answer the user's question by pointing to the most relevant items. If none \139 match, say so. Do not invent entries.140141 QUESTION: \(q)142143 HISTORY:144 \(context)145 """146 ai.runRawPrompt(system: "You help the user find pages in their browsing history.",147 userText: prompt, model: model, apiKey: key)148 }149150 private func dayLabel(_ day: Date) -> String {151 let cal = Calendar.current152 if cal.isDateInToday(day) { return "Today" }153 if cal.isDateInYesterday(day) { return "Yesterday" }154 let f = DateFormatter(); f.dateStyle = .medium155 return f.string(from: day)156 }157}158