// // TemplateBrowserView.swift // Zyquo Agent // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // The template browser sheet (empty state "Browse templates", ⌘K, menu): // built-in library + user templates grouped by category, searchable, with // user-template CRUD. Using a template with {{variables}} opens the fill-in // sheet; the rendered prompt lands in a new task's input bar (pendingDraft). // import SwiftUI struct TemplateBrowserView: View { /// Called with the rendered prompt and the template's suggested mode. var onUse: (String, SafetyMode) -> Void @EnvironmentObject private var templates: TemplateStore @Environment(\.dismiss) private var dismiss @State private var query = "" @State private var fillingTemplate: TaskTemplate? @State private var editingTemplate: TaskTemplate? @State private var creatingTemplate = false var body: some View { VStack(spacing: 0) { header ZyquoHairline() ScrollView { LazyVStack(alignment: .leading, spacing: 1) { ForEach(TemplateCategory.allCases) { category in let matching = filtered(in: category) if !matching.isEmpty { sectionHeader(category) ForEach(matching) { template in templateRow(template) } } } } .padding(ZyquoSpacing.xs) } ZyquoHairline() footer } .frame(width: 560, height: 480) .background(ZyquoColor.surface) .sheet(item: $fillingTemplate) { template in TemplateFillSheet(template: template) { prompt in use(prompt: prompt, template: template) } } .sheet(item: $editingTemplate) { template in TemplateEditorSheet(template: template, isNew: false) } .sheet(isPresented: $creatingTemplate) { TemplateEditorSheet( template: TaskTemplate(title: "", category: .filesAndFolders, prompt: ""), isNew: true ) } } private var header: some View { HStack(spacing: ZyquoSpacing.xs) { Image(systemName: "square.grid.2x2") .font(.system(size: 14)) .foregroundStyle(ZyquoColor.accent) Text("Task Templates") .font(ZyquoFont.bodyEmphasis(size: 14)) .foregroundStyle(ZyquoColor.textPrimary) Spacer() HStack(spacing: ZyquoSpacing.xxs) { Image(systemName: "magnifyingglass") .font(.system(size: 11)) .foregroundStyle(ZyquoColor.textTertiary) TextField("Search templates", text: $query) .textFieldStyle(.plain) .font(ZyquoFont.body(size: 12.5)) .frame(width: 160) } .padding(.horizontal, ZyquoSpacing.xs) .padding(.vertical, 4) .background( RoundedRectangle(cornerRadius: ZyquoRadius.small, style: .continuous) .fill(ZyquoColor.surfaceSecondary) ) } .padding(ZyquoSpacing.sm) } private var footer: some View { HStack { Button { creatingTemplate = true } label: { Label("New Template", systemImage: "plus") } .controlSize(.small) Spacer() Button("Close") { dismiss() } .controlSize(.small) .keyboardShortcut(.cancelAction) } .padding(ZyquoSpacing.sm) } private func sectionHeader(_ category: TemplateCategory) -> some View { HStack(spacing: ZyquoSpacing.xxs) { Image(systemName: category.symbolName) .font(.system(size: 10)) Text(category.displayName) .font(ZyquoFont.caption) } .foregroundStyle(ZyquoColor.textTertiary) .padding(.horizontal, ZyquoSpacing.xs) .padding(.top, ZyquoSpacing.sm) .padding(.bottom, 2) } private func templateRow(_ template: TaskTemplate) -> some View { Button { if template.variables.isEmpty { use(prompt: template.prompt, template: template) } else { fillingTemplate = template } } label: { HStack(spacing: ZyquoSpacing.xs) { Image(systemName: template.displaySymbol) .font(.system(size: 12)) .foregroundStyle(ZyquoColor.accent) .frame(width: 18) VStack(alignment: .leading, spacing: 1) { HStack(spacing: ZyquoSpacing.xxs) { Text(template.title) .font(ZyquoFont.body(size: 13)) .foregroundStyle(ZyquoColor.textPrimary) if !template.isBuiltIn { ZyquoBadge(text: "yours", color: ZyquoColor.success) } if !template.variables.isEmpty { ZyquoBadge(text: "\(template.variables.count) variable\(template.variables.count == 1 ? "" : "s")") } } Text(template.prompt) .font(ZyquoFont.caption) .foregroundStyle(ZyquoColor.textTertiary) .lineLimit(1) } Spacer(minLength: 0) ZyquoBadge(text: template.suggestedSafetyMode.displayName, color: ZyquoColor.textSecondary) if !template.isBuiltIn { Button { editingTemplate = template } label: { Image(systemName: "pencil") .font(.system(size: 10)) .foregroundStyle(ZyquoColor.textSecondary) } .buttonStyle(.plain) .help("Edit template") } } .padding(.horizontal, ZyquoSpacing.xs) .padding(.vertical, 5) .contentShape(Rectangle()) } .buttonStyle(.plain) .zyquoHoverHighlight() } private func filtered(in category: TemplateCategory) -> [TaskTemplate] { let all = templates.templates(in: category) let trimmed = query.trimmingCharacters(in: .whitespaces) guard !trimmed.isEmpty else { return all } return all.filter { $0.title.localizedCaseInsensitiveContains(trimmed) || $0.prompt.localizedCaseInsensitiveContains(trimmed) } } private func use(prompt: String, template: TaskTemplate) { dismiss() onUse(prompt, template.suggestedSafetyMode) } } // MARK: - Variable fill-in struct TemplateFillSheet: View { let template: TaskTemplate var onSubmit: (String) -> Void @Environment(\.dismiss) private var dismiss @State private var values: [String: String] = [:] var body: some View { VStack(alignment: .leading, spacing: ZyquoSpacing.sm) { HStack(spacing: ZyquoSpacing.xs) { Image(systemName: template.displaySymbol) .font(.system(size: 14)) .foregroundStyle(ZyquoColor.accent) Text(template.title) .font(ZyquoFont.title) .foregroundStyle(ZyquoColor.textPrimary) } Form { ForEach(template.variables, id: \.self) { name in TextField(name.capitalized, text: Binding( get: { values[name] ?? "" }, set: { values[name] = $0 } )) } } Text(preview) .font(ZyquoFont.body(size: 12)) .foregroundStyle(ZyquoColor.textSecondary) .lineLimit(5) .padding(ZyquoSpacing.xs) .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: ZyquoRadius.small, style: .continuous) .fill(ZyquoColor.surfaceSecondary) ) HStack { Spacer() Button("Cancel") { dismiss() } Button("Use Template") { dismiss() onSubmit(template.renderedPrompt(values: values)) } .buttonStyle(.borderedProminent) .keyboardShortcut(.defaultAction) .disabled(!allFilled) } } .padding(ZyquoSpacing.xl) .frame(width: 440) .background(ZyquoColor.surface) } private var allFilled: Bool { template.variables.allSatisfy { !(values[$0] ?? "").trimmingCharacters(in: .whitespaces).isEmpty } } private var preview: String { template.renderedPrompt(values: values) } } // MARK: - User-template editor struct TemplateEditorSheet: View { @State var template: TaskTemplate let isNew: Bool @EnvironmentObject private var templates: TemplateStore @Environment(\.dismiss) private var dismiss var body: some View { VStack(alignment: .leading, spacing: ZyquoSpacing.sm) { Text(isNew ? "New Template" : "Edit Template") .font(ZyquoFont.title) .foregroundStyle(ZyquoColor.textPrimary) Form { TextField("Title", text: $template.title) Picker("Category", selection: $template.category) { ForEach(TemplateCategory.allCases) { category in Text(category.displayName).tag(category) } } Picker("Suggested safety mode", selection: $template.suggestedSafetyMode) { ForEach(SafetyMode.allCases) { mode in Text(mode.displayName).tag(mode) } } VStack(alignment: .leading, spacing: ZyquoSpacing.xxs) { Text("Prompt — use {{variable}} for fill-in placeholders") .font(ZyquoFont.caption) .foregroundStyle(ZyquoColor.textSecondary) TextEditor(text: $template.prompt) .font(ZyquoFont.body(size: 12.5)) .frame(height: 120) .padding(ZyquoSpacing.xxs) .background( RoundedRectangle(cornerRadius: ZyquoRadius.small, style: .continuous) .strokeBorder(ZyquoColor.border, lineWidth: ZyquoMetrics.hairline) ) } } HStack { if !isNew { Button("Delete", role: .destructive) { templates.delete(template.id) dismiss() } } Spacer() Button("Cancel") { dismiss() } Button("Save") { if isNew { templates.add(template) } else { templates.update(template) } dismiss() } .buttonStyle(.borderedProminent) .keyboardShortcut(.defaultAction) .disabled(template.title.trimmingCharacters(in: .whitespaces).isEmpty || template.prompt.trimmingCharacters(in: .whitespaces).isEmpty) } } .padding(ZyquoSpacing.xl) .frame(width: 480) .background(ZyquoColor.surface) } }