SPB Git

spb/zyquo-cloud Public MIT

Native macOS AI chat client for 12 cloud providers — your keys, every cloud model, one beautiful chat.

Swift 97.4% Shell 1.7% Makefile 1%
5.6 KB · 153 lines swift
Raw Blame History
1//2//  CommandPaletteView.swift3//  Zyquo Cloud4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//8//  ⌘K palette: one search box over models, prompt templates, and personas.9//  Models switch the current conversation; templates insert into the draft;10//  personas apply their system prompt + preferences.11//1213import SwiftUI1415struct CommandPaletteView: View {16    let currentModel: AIModel?17    var onSelectModel: (AIModel) -> Void18    var onInsertTemplate: (PromptTemplate) -> Void19    var onApplyPersona: (Persona) -> Void2021    @EnvironmentObject private var catalog: ModelCatalog22    @StateObject private var library = PromptLibraryStore()23    @Environment(\.dismiss) private var dismiss24    @State private var query = ""25    @FocusState private var focused: Bool2627    var body: some View {28        VStack(spacing: 0) {29            HStack(spacing: ZyquoSpacing.xs) {30                Image(systemName: "command")31                    .font(.system(size: 13))32                    .foregroundStyle(ZyquoColor.textTertiary)33                TextField("Switch model, insert template, apply persona…", text: $query)34                    .textFieldStyle(.plain)35                    .font(ZyquoFont.body(size: 15))36                    .focused($focused)37            }38            .padding(ZyquoSpacing.md)39            ZyquoHairline()40            ScrollView {41                LazyVStack(alignment: .leading, spacing: 1) {42                    if !matchingModels.isEmpty {43                        section("Models")44                        ForEach(matchingModels.prefix(8)) { model in45                            paletteRow(46                                symbol: model.provider.symbolName,47                                title: model.displayName,48                                subtitle: "\(model.provider.displayName) · \(model.contextBadge)"49                            ) {50                                onSelectModel(model)51                                dismiss()52                            }53                        }54                    }55                    if !matchingPersonas.isEmpty {56                        section("Personas")57                        ForEach(matchingPersonas.prefix(6)) { persona in58                            paletteRow(59                                symbol: persona.symbolName,60                                title: persona.name,61                                subtitle: String(persona.systemPrompt.prefix(70))62                            ) {63                                onApplyPersona(persona)64                                dismiss()65                            }66                        }67                    }68                    if !matchingTemplates.isEmpty {69                        section("Prompt Library")70                        ForEach(matchingTemplates.prefix(10)) { template in71                            paletteRow(72                                symbol: "text.badge.plus",73                                title: template.title,74                                subtitle: template.category75                            ) {76                                onInsertTemplate(template)77                                dismiss()78                            }79                        }80                    }81                }82                .padding(ZyquoSpacing.xxs)83            }84            .frame(height: 380)85        }86        .frame(width: 560)87        .background(ZyquoColor.surface)88        .onAppear { focused = true }89        .onExitCommand { dismiss() }90    }9192    private var matchingModels: [AIModel] {93        query.isEmpty94            ? catalog.all.filter(\.isRecommended)95            : catalog.all.filter {96                $0.displayName.localizedCaseInsensitiveContains(query)97                    || $0.id.localizedCaseInsensitiveContains(query)98            }99    }100101    private var matchingTemplates: [PromptTemplate] {102        query.isEmpty103            ? Array(library.allTemplates.prefix(10))104            : library.allTemplates.filter {105                $0.title.localizedCaseInsensitiveContains(query)106                    || $0.category.localizedCaseInsensitiveContains(query)107            }108    }109110    private var matchingPersonas: [Persona] {111        query.isEmpty112            ? library.allPersonas113            : library.allPersonas.filter { $0.name.localizedCaseInsensitiveContains(query) }114    }115116    private func section(_ title: String) -> some View {117        Text(title)118            .font(ZyquoFont.caption)119            .foregroundStyle(ZyquoColor.textTertiary)120            .padding(.horizontal, ZyquoSpacing.sm)121            .padding(.top, ZyquoSpacing.xs)122            .padding(.bottom, 2)123    }124125    private func paletteRow(126        symbol: String, title: String, subtitle: String, action: @escaping () -> Void127    ) -> some View {128        Button(action: action) {129            HStack(spacing: ZyquoSpacing.sm) {130                Image(systemName: symbol)131                    .font(.system(size: 13))132                    .foregroundStyle(ZyquoColor.accent)133                    .frame(width: 18)134                VStack(alignment: .leading, spacing: 0) {135                    Text(title)136                        .font(ZyquoFont.body(size: 13))137                        .foregroundStyle(ZyquoColor.textPrimary)138                    Text(subtitle)139                        .font(ZyquoFont.caption)140                        .foregroundStyle(ZyquoColor.textTertiary)141                        .lineLimit(1)142                }143                Spacer()144            }145            .padding(.horizontal, ZyquoSpacing.sm)146            .padding(.vertical, 5)147            .contentShape(Rectangle())148        }149        .buttonStyle(.plain)150        .zyquoHoverHighlight()151    }152}153