392 lines
15 KiB
Swift
392 lines
15 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
|
|
typealias PromptIcon = String
|
|
|
|
extension PromptIcon {
|
|
static let allCases: [PromptIcon] = [
|
|
// Document & Text
|
|
"doc.text.fill",
|
|
"textbox",
|
|
"checkmark.seal.fill",
|
|
|
|
// Communication
|
|
"bubble.left.and.bubble.right.fill",
|
|
"message.fill",
|
|
"envelope.fill",
|
|
|
|
// Professional
|
|
"person.2.fill",
|
|
"person.wave.2.fill",
|
|
"briefcase.fill",
|
|
|
|
// Technical
|
|
"curlybraces",
|
|
"terminal.fill",
|
|
"gearshape.fill",
|
|
|
|
// Content
|
|
"doc.text.image.fill",
|
|
"note",
|
|
"book.fill",
|
|
"bookmark.fill",
|
|
"pencil.circle.fill",
|
|
|
|
// Media & Creative
|
|
"video.fill",
|
|
"mic.fill",
|
|
"music.note",
|
|
"photo.fill",
|
|
"paintbrush.fill",
|
|
|
|
// Productivity & Time
|
|
"clock.fill",
|
|
"calendar",
|
|
"list.bullet",
|
|
"checkmark.circle.fill",
|
|
"timer",
|
|
"hourglass",
|
|
"star.fill",
|
|
"flag.fill",
|
|
"tag.fill",
|
|
"folder.fill",
|
|
"paperclip",
|
|
"tray.fill",
|
|
"chart.bar.fill",
|
|
"flame.fill",
|
|
"target",
|
|
"list.clipboard.fill",
|
|
"brain.head.profile",
|
|
"lightbulb.fill",
|
|
"megaphone.fill",
|
|
"heart.fill",
|
|
"map.fill",
|
|
"house.fill",
|
|
"camera.fill",
|
|
"figure.walk",
|
|
"dumbbell.fill",
|
|
"cart.fill",
|
|
"creditcard.fill",
|
|
"graduationcap.fill",
|
|
"airplane",
|
|
"leaf.fill",
|
|
"hand.raised.fill",
|
|
"hand.thumbsup.fill"
|
|
]
|
|
}
|
|
|
|
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 = "doc.text.fill",
|
|
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)
|
|
.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) {
|
|
let alert = NSAlert()
|
|
alert.messageText = "Delete Prompt?"
|
|
alert.informativeText = "Are you sure you want to delete '\(self.title)' prompt? This action cannot be undone."
|
|
alert.alertStyle = .warning
|
|
alert.addButton(withTitle: "Delete")
|
|
alert.addButton(withTitle: "Cancel")
|
|
|
|
let response = alert.runModal()
|
|
if response == .alertFirstButtonReturn {
|
|
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)
|
|
}
|
|
}
|