import Foundation import SwiftUI enum PromptIcon: String, Codable, CaseIterable { // Document & Text case documentFill = "doc.text.fill" case textbox = "textbox" case sealedFill = "checkmark.seal.fill" // Communication case chatFill = "bubble.left.and.bubble.right.fill" case messageFill = "message.fill" case emailFill = "envelope.fill" // Professional case meetingFill = "person.2.fill" case presentationFill = "person.wave.2.fill" case briefcaseFill = "briefcase.fill" // Technical case codeFill = "curlybraces" case terminalFill = "terminal.fill" case gearFill = "gearshape.fill" // Content case blogFill = "doc.text.image.fill" case notesFill = "note" case bookFill = "book.fill" case bookmarkFill = "bookmark.fill" case pencilFill = "pencil.circle.fill" // Media & Creative case videoFill = "video.fill" case micFill = "mic.fill" case musicFill = "music.note" case photoFill = "photo.fill" case brushFill = "paintbrush.fill" var title: String { switch self { // Document & Text case .documentFill: return "Document" case .textbox: return "Textbox" case .sealedFill: return "Sealed" // Communication case .chatFill: return "Chat" case .messageFill: return "Message" case .emailFill: return "Email" // Professional case .meetingFill: return "Meeting" case .presentationFill: return "Presentation" case .briefcaseFill: return "Briefcase" // Technical case .codeFill: return "Code" case .terminalFill: return "Terminal" case .gearFill: return "Settings" // Content case .blogFill: return "Blog" case .notesFill: return "Notes" case .bookFill: return "Book" case .bookmarkFill: return "Bookmark" case .pencilFill: return "Edit" // Media & Creative case .videoFill: return "Video" case .micFill: return "Audio" case .musicFill: return "Music" case .photoFill: return "Photo" case .brushFill: return "Design" } } } struct CustomPrompt: Identifiable, Codable, Equatable { let id: UUID let title: String let promptText: String var isActive: Bool let icon: PromptIcon let description: String? let isPredefined: Bool let triggerWords: [String] let useSystemInstructions: Bool init( id: UUID = UUID(), title: String, promptText: String, isActive: Bool = false, icon: PromptIcon = .documentFill, description: String? = nil, isPredefined: Bool = false, triggerWords: [String] = [], useSystemInstructions: Bool = true ) { self.id = id self.title = title self.promptText = promptText self.isActive = isActive self.icon = icon self.description = description self.isPredefined = isPredefined self.triggerWords = triggerWords self.useSystemInstructions = useSystemInstructions } enum CodingKeys: String, CodingKey { case id, title, promptText, isActive, icon, description, isPredefined, triggerWords, useSystemInstructions } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(UUID.self, forKey: .id) title = try container.decode(String.self, forKey: .title) promptText = try container.decode(String.self, forKey: .promptText) isActive = try container.decode(Bool.self, forKey: .isActive) icon = try container.decode(PromptIcon.self, forKey: .icon) description = try container.decodeIfPresent(String.self, forKey: .description) isPredefined = try container.decode(Bool.self, forKey: .isPredefined) triggerWords = try container.decode([String].self, forKey: .triggerWords) useSystemInstructions = try container.decodeIfPresent(Bool.self, forKey: .useSystemInstructions) ?? true } var finalPromptText: String { if useSystemInstructions { return String(format: AIPrompts.customPromptTemplate, self.promptText) } else { return self.promptText } } } // MARK: - UI Extensions extension CustomPrompt { func promptIcon(isSelected: Bool, onTap: @escaping () -> Void, onEdit: ((CustomPrompt) -> Void)? = nil, onDelete: ((CustomPrompt) -> Void)? = nil) -> some View { VStack(spacing: 8) { ZStack { // Dynamic background with blur effect RoundedRectangle(cornerRadius: 14) .fill( LinearGradient( gradient: isSelected ? Gradient(colors: [ Color.accentColor.opacity(0.9), Color.accentColor.opacity(0.7) ]) : Gradient(colors: [ Color(NSColor.controlBackgroundColor).opacity(0.95), Color(NSColor.controlBackgroundColor).opacity(0.85) ]), startPoint: .topLeading, endPoint: .bottomTrailing ) ) .overlay( RoundedRectangle(cornerRadius: 14) .stroke( LinearGradient( gradient: Gradient(colors: [ isSelected ? Color.white.opacity(0.3) : Color.white.opacity(0.15), isSelected ? Color.white.opacity(0.1) : Color.white.opacity(0.05) ]), startPoint: .topLeading, endPoint: .bottomTrailing ), lineWidth: 1 ) ) .shadow( color: isSelected ? Color.accentColor.opacity(0.4) : Color.black.opacity(0.1), radius: isSelected ? 10 : 6, x: 0, y: 3 ) // Decorative background elements Circle() .fill( RadialGradient( gradient: Gradient(colors: [ isSelected ? Color.white.opacity(0.15) : Color.white.opacity(0.08), Color.clear ]), center: .center, startRadius: 1, endRadius: 25 ) ) .frame(width: 50, height: 50) .offset(x: -15, y: -15) .blur(radius: 2) // Icon with enhanced effects Image(systemName: icon.rawValue) .font(.system(size: 20, weight: .medium)) .foregroundStyle( LinearGradient( colors: isSelected ? [Color.white, Color.white.opacity(0.9)] : [Color.primary.opacity(0.9), Color.primary.opacity(0.7)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .shadow( color: isSelected ? Color.white.opacity(0.5) : Color.clear, radius: 4 ) .shadow( color: isSelected ? Color.accentColor.opacity(0.5) : Color.clear, radius: 3 ) } .frame(width: 48, height: 48) // Enhanced title styling VStack(spacing: 2) { Text(title) .font(.system(size: 11, weight: .medium)) .foregroundColor(isSelected ? .primary : .secondary) .lineLimit(1) .frame(maxWidth: 70) // Trigger word section with consistent height ZStack(alignment: .center) { if !triggerWords.isEmpty { HStack(spacing: 2) { Image(systemName: "mic.fill") .font(.system(size: 7)) .foregroundColor(isSelected ? .accentColor.opacity(0.9) : .secondary.opacity(0.7)) if triggerWords.count == 1 { Text("\"\(triggerWords[0])...\"") .font(.system(size: 8, weight: .regular)) .foregroundColor(isSelected ? .primary.opacity(0.8) : .secondary.opacity(0.7)) .lineLimit(1) } else { Text("\"\(triggerWords[0])...\" +\(triggerWords.count - 1)") .font(.system(size: 8, weight: .regular)) .foregroundColor(isSelected ? .primary.opacity(0.8) : .secondary.opacity(0.7)) .lineLimit(1) } } .frame(maxWidth: 70) } } .frame(height: 16) } } .padding(.horizontal, 4) .padding(.vertical, 6) .contentShape(Rectangle()) .scaleEffect(isSelected ? 1.05 : 1.0) .onTapGesture(count: 2) { // Double tap to edit if let onEdit = onEdit { onEdit(self) } } .onTapGesture(count: 1) { // Single tap to select onTap() } .contextMenu { if onEdit != nil || onDelete != nil { if let onEdit = onEdit { Button { onEdit(self) } label: { Label("Edit", systemImage: "pencil") } } if let onDelete = onDelete, !isPredefined { Button(role: .destructive) { onDelete(self) } label: { Label("Delete", systemImage: "trash") } } } } } // Static method to create an "Add New" button with the same styling as the prompt icons static func addNewButton(action: @escaping () -> Void) -> some View { VStack(spacing: 8) { ZStack { // Dynamic background with blur effect - same styling as promptIcon RoundedRectangle(cornerRadius: 14) .fill( LinearGradient( gradient: Gradient(colors: [ Color(NSColor.controlBackgroundColor).opacity(0.95), Color(NSColor.controlBackgroundColor).opacity(0.85) ]), startPoint: .topLeading, endPoint: .bottomTrailing ) ) .overlay( RoundedRectangle(cornerRadius: 14) .stroke( LinearGradient( gradient: Gradient(colors: [ Color.white.opacity(0.15), Color.white.opacity(0.05) ]), startPoint: .topLeading, endPoint: .bottomTrailing ), lineWidth: 1 ) ) .shadow( color: Color.black.opacity(0.1), radius: 6, x: 0, y: 3 ) // Decorative background elements (same as in promptIcon) Circle() .fill( RadialGradient( gradient: Gradient(colors: [ Color.white.opacity(0.08), Color.clear ]), center: .center, startRadius: 1, endRadius: 25 ) ) .frame(width: 50, height: 50) .offset(x: -15, y: -15) .blur(radius: 2) // Plus icon with same styling as the normal icons Image(systemName: "plus.circle.fill") .font(.system(size: 20, weight: .medium)) .foregroundStyle( LinearGradient( colors: [Color.accentColor.opacity(0.9), Color.accentColor.opacity(0.7)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) } .frame(width: 48, height: 48) // Text label with matching styling VStack(spacing: 2) { Text("Add New") .font(.system(size: 11, weight: .medium)) .foregroundColor(.secondary) .lineLimit(1) .frame(maxWidth: 70) // Empty space matching the trigger word area height Spacer() .frame(height: 16) } } .padding(.horizontal, 4) .padding(.vertical, 6) .contentShape(Rectangle()) .onTapGesture(perform: action) } }