// // CommandPaletteView.swift // Zyquo Cloud // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // ⌘K palette: one search box over models, prompt templates, and personas. // Models switch the current conversation; templates insert into the draft; // personas apply their system prompt + preferences. // import SwiftUI struct CommandPaletteView: View { let currentModel: AIModel? var onSelectModel: (AIModel) -> Void var onInsertTemplate: (PromptTemplate) -> Void var onApplyPersona: (Persona) -> Void @EnvironmentObject private var catalog: ModelCatalog @StateObject private var library = PromptLibraryStore() @Environment(\.dismiss) private var dismiss @State private var query = "" @FocusState private var focused: Bool var body: some View { VStack(spacing: 0) { HStack(spacing: ZyquoSpacing.xs) { Image(systemName: "command") .font(.system(size: 13)) .foregroundStyle(ZyquoColor.textTertiary) TextField("Switch model, insert template, apply persona…", text: $query) .textFieldStyle(.plain) .font(ZyquoFont.body(size: 15)) .focused($focused) } .padding(ZyquoSpacing.md) ZyquoHairline() ScrollView { LazyVStack(alignment: .leading, spacing: 1) { if !matchingModels.isEmpty { section("Models") ForEach(matchingModels.prefix(8)) { model in paletteRow( symbol: model.provider.symbolName, title: model.displayName, subtitle: "\(model.provider.displayName) · \(model.contextBadge)" ) { onSelectModel(model) dismiss() } } } if !matchingPersonas.isEmpty { section("Personas") ForEach(matchingPersonas.prefix(6)) { persona in paletteRow( symbol: persona.symbolName, title: persona.name, subtitle: String(persona.systemPrompt.prefix(70)) ) { onApplyPersona(persona) dismiss() } } } if !matchingTemplates.isEmpty { section("Prompt Library") ForEach(matchingTemplates.prefix(10)) { template in paletteRow( symbol: "text.badge.plus", title: template.title, subtitle: template.category ) { onInsertTemplate(template) dismiss() } } } } .padding(ZyquoSpacing.xxs) } .frame(height: 380) } .frame(width: 560) .background(ZyquoColor.surface) .onAppear { focused = true } .onExitCommand { dismiss() } } private var matchingModels: [AIModel] { query.isEmpty ? catalog.all.filter(\.isRecommended) : catalog.all.filter { $0.displayName.localizedCaseInsensitiveContains(query) || $0.id.localizedCaseInsensitiveContains(query) } } private var matchingTemplates: [PromptTemplate] { query.isEmpty ? Array(library.allTemplates.prefix(10)) : library.allTemplates.filter { $0.title.localizedCaseInsensitiveContains(query) || $0.category.localizedCaseInsensitiveContains(query) } } private var matchingPersonas: [Persona] { query.isEmpty ? library.allPersonas : library.allPersonas.filter { $0.name.localizedCaseInsensitiveContains(query) } } private func section(_ title: String) -> some View { Text(title) .font(ZyquoFont.caption) .foregroundStyle(ZyquoColor.textTertiary) .padding(.horizontal, ZyquoSpacing.sm) .padding(.top, ZyquoSpacing.xs) .padding(.bottom, 2) } private func paletteRow( symbol: String, title: String, subtitle: String, action: @escaping () -> Void ) -> some View { Button(action: action) { HStack(spacing: ZyquoSpacing.sm) { Image(systemName: symbol) .font(.system(size: 13)) .foregroundStyle(ZyquoColor.accent) .frame(width: 18) VStack(alignment: .leading, spacing: 0) { Text(title) .font(ZyquoFont.body(size: 13)) .foregroundStyle(ZyquoColor.textPrimary) Text(subtitle) .font(ZyquoFont.caption) .foregroundStyle(ZyquoColor.textTertiary) .lineLimit(1) } Spacer() } .padding(.horizontal, ZyquoSpacing.sm) .padding(.vertical, 5) .contentShape(Rectangle()) } .buttonStyle(.plain) .zyquoHoverHighlight() } }