// // PromptLibraryStore.swift // Zyquo Cloud // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // // Observable store for the prompt template library and personas. // Built-in items come from PromptLibraryData / PersonaLibraryData and are // immutable; user-created items are persisted as JSON via PersistenceService. // import Foundation @MainActor final class PromptLibraryStore: ObservableObject { // MARK: - Published state /// User-created templates (never contains built-ins). @Published var userTemplates: [PromptTemplate] /// User-created personas (never contains built-ins). @Published var userPersonas: [Persona] // MARK: - Constants private static let templatesFileName = "user-templates.json" private static let personasFileName = "user-personas.json" private let persistence: PersistenceService // MARK: - Init init(persistence: PersistenceService = .shared) { self.persistence = persistence self.userTemplates = persistence.load([PromptTemplate].self, from: Self.templatesFileName) ?? [] self.userPersonas = persistence.load([Persona].self, from: Self.personasFileName) ?? [] } // MARK: - Combined catalogs (built-in + user) /// All templates: the built-in library followed by user templates. var allTemplates: [PromptTemplate] { PromptLibraryData.templates + userTemplates } /// All personas: the built-in set followed by user personas. var allPersonas: [Persona] { PersonaLibraryData.personas + userPersonas } /// Template categories in display order (built-in order first, then any /// user-only categories alphabetically). var templateCategories: [String] { var seen = Set() var ordered: [String] = [] for template in PromptLibraryData.templates where seen.insert(template.category).inserted { ordered.append(template.category) } let userOnly = Set(userTemplates.map(\.category)).subtracting(seen).sorted() return ordered + userOnly } /// Templates belonging to a category, built-ins first. func templates(in category: String) -> [PromptTemplate] { allTemplates.filter { $0.category == category } } // MARK: - Template CRUD (user items only) /// Adds a user template. Built-in flags are stripped defensively. func add(_ template: PromptTemplate) { var template = template template.isBuiltIn = false userTemplates.append(template) saveTemplates() } /// Updates a user template in place. Built-in templates are immutable /// and silently ignored. func update(_ template: PromptTemplate) { guard let index = userTemplates.firstIndex(where: { $0.id == template.id }) else { return } var template = template template.isBuiltIn = false userTemplates[index] = template saveTemplates() } /// Deletes a user template. Built-in templates cannot be deleted. func delete(_ template: PromptTemplate) { guard userTemplates.contains(where: { $0.id == template.id }) else { return } userTemplates.removeAll { $0.id == template.id } saveTemplates() } // MARK: - Persona CRUD (user items only) /// Adds a user persona. func add(_ persona: Persona) { userPersonas.append(persona) savePersonas() } /// Updates a user persona in place. Built-in personas are immutable /// and silently ignored. func update(_ persona: Persona) { guard let index = userPersonas.firstIndex(where: { $0.id == persona.id }) else { return } userPersonas[index] = persona savePersonas() } /// Deletes a user persona. Built-in personas cannot be deleted. func delete(_ persona: Persona) { guard userPersonas.contains(where: { $0.id == persona.id }) else { return } userPersonas.removeAll { $0.id == persona.id } savePersonas() } // MARK: - Lookup func persona(withID id: UUID) -> Persona? { allPersonas.first { $0.id == id } } func template(withID id: UUID) -> PromptTemplate? { allTemplates.first { $0.id == id } } // MARK: - Template application /// Fills a template with the user's input, replacing every `{{input}}` /// placeholder. Templates without a placeholder get the input appended. static func apply(_ template: PromptTemplate, input: String) -> String { guard template.body.contains("{{input}}") else { return input.isEmpty ? template.body : template.body + "\n\n" + input } return template.body.replacingOccurrences(of: "{{input}}", with: input) } // MARK: - Persistence private func saveTemplates() { persistence.save(userTemplates, to: Self.templatesFileName) } private func savePersonas() { persistence.save(userPersonas, to: Self.personasFileName) } }