// // EmptyStateView.swift // Zyquo Agent // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // The "What should I do on your Mac?" hero: brand glyph, four example task // cards (clicking fills the input), the safety-mode selector, and the model // chip — powerful and trustworthy, never blank. // import SwiftUI struct AgentEmptyStateView: View { let model: AIModel? let safetyMode: SafetyMode /// Name of the task's active persona (nil = none / no task yet). var personaName: String? var onSelectModel: (AIModel) -> Void var onSelectSafetyMode: (SafetyMode) -> Void /// Called with the chosen persona (nil = none). var onSelectPersona: ((Persona?) -> Void)? var onBrowseTemplates: (() -> Void)? var onSuggestion: (String) -> Void @EnvironmentObject private var personas: PersonaStore private static let suggestions: [(symbol: String, title: String, prompt: String)] = [ ("folder.badge.gearshape", "Organize my Downloads folder", "Look at my Downloads folder, group the files by type into subfolders (Images, Documents, Archives, Installers…), and show me a summary of what you moved."), ("textformat.abc.dottedunderline", "Batch-rename these files", "Rename all the files in the workspace to a consistent kebab-case pattern with a numeric suffix, and list the before → after mapping."), ("chevron.left.forwardslash.chevron.right", "Set up a Python project and run the tests", "Create a small Python project in the workspace with a src/ layout, one example module with two functions, pytest tests for them, then run the tests and report the results."), ("note.text", "Export my Notes to Markdown", "Use AppleScript to read my Apple Notes and export each note in the default folder as a Markdown file in the workspace, named after its title."), ] var body: some View { VStack(spacing: ZyquoSpacing.lg) { Spacer() AgentZGlyph(size: 88) VStack(spacing: ZyquoSpacing.xxs) { Text("What should I do on your Mac?") .font(ZyquoFont.title) .foregroundStyle(ZyquoColor.textPrimary) Text("Zyquo Agent plans, runs commands, and works until it's done — with your approval on anything risky.") .font(ZyquoFont.body()) .foregroundStyle(ZyquoColor.textSecondary) .multilineTextAlignment(.center) } HStack(spacing: ZyquoSpacing.sm) { ModelChipView(model: model, onSelect: onSelectModel) SafetyModePicker(mode: safetyMode, onSelect: onSelectSafetyMode) if let onSelectPersona, !personas.personas.isEmpty { personaMenu(onSelectPersona) } } if let onBrowseTemplates { Button { onBrowseTemplates() } label: { HStack(spacing: ZyquoSpacing.xxs) { Image(systemName: "square.grid.2x2") .font(.system(size: 11)) Text("Browse templates") .font(ZyquoFont.body(size: 12.5)) } .foregroundStyle(ZyquoColor.accent) } .buttonStyle(.plain) .help("25+ ready-made agent tasks (⌘K)") } LazyVGrid( columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: ZyquoSpacing.sm ) { ForEach(Self.suggestions, id: \.title) { suggestion in Button { onSuggestion(suggestion.prompt) } label: { HStack(spacing: ZyquoSpacing.xs) { Image(systemName: suggestion.symbol) .font(.system(size: 14)) .foregroundStyle(ZyquoColor.accent) .frame(width: 20) Text(suggestion.title) .font(ZyquoFont.body(size: 13)) .foregroundStyle(ZyquoColor.textPrimary) .multilineTextAlignment(.leading) Spacer(minLength: 0) } .padding(ZyquoSpacing.sm) .background( RoundedRectangle(cornerRadius: ZyquoRadius.medium, style: .continuous) .fill(ZyquoColor.surface) .overlay( RoundedRectangle(cornerRadius: ZyquoRadius.medium, style: .continuous) .strokeBorder(ZyquoColor.border, lineWidth: ZyquoMetrics.hairline) ) ) .contentShape(Rectangle()) } .buttonStyle(PressableButtonStyle()) } } .frame(maxWidth: ZyquoMetrics.emptyStateGridWidth) Spacer() Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(ZyquoColor.background) } /// Persona chip + menu (None + every stored persona). private func personaMenu(_ onSelect: @escaping (Persona?) -> Void) -> some View { Menu { Button("None") { onSelect(nil) } Divider() ForEach(personas.personas) { persona in Button { onSelect(persona) } label: { Label(persona.name, systemImage: persona.symbolName) } } } label: { HStack(spacing: ZyquoSpacing.xxs) { Image(systemName: "person.crop.circle") .font(.system(size: 11, weight: .medium)) .foregroundStyle(ZyquoColor.accent) Text(personaName ?? "Persona") .font(ZyquoFont.bodyEmphasis(size: 12.5)) .foregroundStyle(personaName == nil ? ZyquoColor.textSecondary : ZyquoColor.textPrimary) .lineLimit(1) } .padding(.horizontal, ZyquoSpacing.xs) .padding(.vertical, 4) .background( RoundedRectangle(cornerRadius: ZyquoRadius.small, style: .continuous) .fill(ZyquoColor.surfaceSecondary) ) } .menuStyle(.borderlessButton) .fixedSize() .help("Persona — adds a system-prompt section and can pin a model and safety default") } } /// The Manual / Guarded / Autonomous segmented control (header + empty state). struct SafetyModePicker: View { let mode: SafetyMode var onSelect: (SafetyMode) -> Void var body: some View { Picker("Safety mode", selection: Binding( get: { mode }, set: { onSelect($0) } )) { ForEach(SafetyMode.allCases) { candidate in Text(candidate.displayName).tag(candidate) } } .pickerStyle(.segmented) .labelsHidden() .controlSize(.small) .fixedSize() .help(helpText) } private var helpText: String { switch mode { case .manual: return "Manual — approve every action" case .guarded: return "Guarded — safe actions run automatically, anything mutating asks" case .autonomous: return "Autonomous — runs freely within budget; destructive actions still ask" } } }