vOOice/VoiceInk/Models/CustomPrompt.swift

337 lines
13 KiB
Swift

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 triggerWord: String?
init(
id: UUID = UUID(),
title: String,
promptText: String,
isActive: Bool = false,
icon: PromptIcon = .documentFill,
description: String? = nil,
isPredefined: Bool = false,
triggerWord: String? = nil
) {
self.id = id
self.title = title
self.promptText = promptText
self.isActive = isActive
self.icon = icon
self.description = description
self.isPredefined = isPredefined
self.triggerWord = triggerWord
}
}
// 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 let triggerWord = triggerWord, !triggerWord.isEmpty {
HStack(spacing: 2) {
Image(systemName: "mic.fill")
.font(.system(size: 7))
.foregroundColor(isSelected ? .accentColor.opacity(0.9) : .secondary.opacity(0.7))
Text("\"\(triggerWord)...\"")
.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(perform: 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)
}
}