diff --git a/VoiceInk/Models/CustomPrompt.swift b/VoiceInk/Models/CustomPrompt.swift index 7ce6d03..4c30e7f 100644 --- a/VoiceInk/Models/CustomPrompt.swift +++ b/VoiceInk/Models/CustomPrompt.swift @@ -108,7 +108,7 @@ struct CustomPrompt: Identifiable, Codable, Equatable { // MARK: - UI Extensions extension CustomPrompt { - func promptIcon(isSelected: Bool, onTap: @escaping () -> Void, onEdit: ((CustomPrompt) -> Void)? = nil, onDelete: ((CustomPrompt) -> Void)? = nil, assistantTriggerWord: String? = nil) -> some View { + 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 @@ -206,21 +206,7 @@ extension CustomPrompt { // Trigger word section with consistent height ZStack(alignment: .center) { - if id == PredefinedPrompts.assistantPromptId, let assistantTriggerWord = assistantTriggerWord, !assistantTriggerWord.isEmpty { - // Show the global assistant trigger word for the Assistant Mode - HStack(spacing: 2) { - Image(systemName: "mic.fill") - .font(.system(size: 7)) - .foregroundColor(isSelected ? .accentColor.opacity(0.9) : .secondary.opacity(0.7)) - - Text("\"\(assistantTriggerWord)...\"") - .font(.system(size: 8, weight: .regular)) - .foregroundColor(isSelected ? .primary.opacity(0.8) : .secondary.opacity(0.7)) - .lineLimit(1) - } - .frame(maxWidth: 70) - } else if let triggerWord = triggerWord, !triggerWord.isEmpty { - // Show custom trigger words for Enhancement Modes + if let triggerWord = triggerWord, !triggerWord.isEmpty { HStack(spacing: 2) { Image(systemName: "mic.fill") .font(.system(size: 7)) @@ -234,7 +220,7 @@ extension CustomPrompt { .frame(maxWidth: 70) } } - .frame(height: 16) // Fixed height for all modes, with or without trigger words + .frame(height: 16) } } .padding(.horizontal, 4) @@ -243,7 +229,7 @@ extension CustomPrompt { .scaleEffect(isSelected ? 1.05 : 1.0) .onTapGesture(perform: onTap) .contextMenu { - if !isPredefined && (onEdit != nil || onDelete != nil) { + if onEdit != nil || onDelete != nil { if let onEdit = onEdit { Button { onEdit(self) @@ -252,7 +238,7 @@ extension CustomPrompt { } } - if let onDelete = onDelete { + if let onDelete = onDelete, !isPredefined { Button(role: .destructive) { onDelete(self) } label: { diff --git a/VoiceInk/Models/PredefinedPrompts.swift b/VoiceInk/Models/PredefinedPrompts.swift index cd9fed2..ff0cac2 100644 --- a/VoiceInk/Models/PredefinedPrompts.swift +++ b/VoiceInk/Models/PredefinedPrompts.swift @@ -5,7 +5,7 @@ enum PredefinedPrompts { private static let predefinedPromptsKey = "PredefinedPrompts" // Static UUIDs for predefined prompts - private static let defaultPromptId = UUID(uuidString: "00000000-0000-0000-0000-000000000001")! + static let defaultPromptId = UUID(uuidString: "00000000-0000-0000-0000-000000000001")! static let assistantPromptId = UUID(uuidString: "00000000-0000-0000-0000-000000000002")! static var all: [CustomPrompt] { diff --git a/VoiceInk/PowerMode/PowerModeConfigView.swift b/VoiceInk/PowerMode/PowerModeConfigView.swift index 9365ed4..3db5cc0 100644 --- a/VoiceInk/PowerMode/PowerModeConfigView.swift +++ b/VoiceInk/PowerMode/PowerModeConfigView.swift @@ -564,20 +564,20 @@ struct ConfigurationView: View { .foregroundColor(.primary) PromptSelectionGrid( + prompts: enhancementService.allPrompts, selectedPromptId: selectedPromptId, - onPromptTap: { prompt in + onPromptSelected: { prompt in selectedPromptId = prompt.id }, - onPromptEdit: { prompt in + onEditPrompt: { prompt in selectedPromptForEdit = prompt }, - onPromptDelete: { prompt in + onDeletePrompt: { prompt in enhancementService.deletePrompt(prompt) }, - onAddNew: { + onAddNewPrompt: { isEditingPrompt = true - }, - assistantTriggerWord: enhancementService.assistantTriggerWord + } ) } diff --git a/VoiceInk/Services/AIEnhancementService.swift b/VoiceInk/Services/AIEnhancementService.swift index 71f9c9a..88ba0d0 100644 --- a/VoiceInk/Services/AIEnhancementService.swift +++ b/VoiceInk/Services/AIEnhancementService.swift @@ -42,15 +42,9 @@ class AIEnhancementService: ObservableObject { } } - @Published var assistantTriggerWord: String { - didSet { - UserDefaults.standard.set(assistantTriggerWord, forKey: "assistantTriggerWord") - } - } - @Published var customPrompts: [CustomPrompt] { didSet { - if let encoded = try? JSONEncoder().encode(customPrompts.filter { !$0.isPredefined }) { + if let encoded = try? JSONEncoder().encode(customPrompts) { UserDefaults.standard.set(encoded, forKey: "customPrompts") } } @@ -67,7 +61,7 @@ class AIEnhancementService: ObservableObject { } var allPrompts: [CustomPrompt] { - PredefinedPrompts.createDefaultPrompts() + customPrompts.filter { !$0.isPredefined } + return customPrompts } private let aiService: AIService @@ -87,7 +81,6 @@ class AIEnhancementService: ObservableObject { self.isEnhancementEnabled = UserDefaults.standard.bool(forKey: "isAIEnhancementEnabled") self.useClipboardContext = UserDefaults.standard.bool(forKey: "useClipboardContext") self.useScreenCaptureContext = UserDefaults.standard.bool(forKey: "useScreenCaptureContext") - self.assistantTriggerWord = UserDefaults.standard.string(forKey: "assistantTriggerWord") ?? "hey" if let savedPromptsData = UserDefaults.standard.data(forKey: "customPrompts"), let decodedPrompts = try? JSONDecoder().decode([CustomPrompt].self, from: savedPromptsData) { @@ -110,6 +103,8 @@ class AIEnhancementService: ObservableObject { name: .aiProviderKeyChanged, object: nil ) + + initializePredefinedPrompts() } deinit { @@ -166,20 +161,17 @@ class AIEnhancementService: ObservableObject { "" } - switch mode { - case .transcriptionEnhancement: - if let activePrompt = activePrompt, - activePrompt.id == PredefinedPrompts.assistantPromptId { - return AIPrompts.assistantMode + contextSection - } - - var systemMessage = String(format: AIPrompts.customPromptTemplate, activePrompt!.promptText) - systemMessage += contextSection - return systemMessage - - case .aiAssistant: + guard let activePrompt = activePrompt else { return AIPrompts.assistantMode + contextSection } + + if activePrompt.id == PredefinedPrompts.assistantPromptId { + return activePrompt.promptText + contextSection + } + + var systemMessage = String(format: AIPrompts.customPromptTemplate, activePrompt.promptText) + systemMessage += contextSection + return systemMessage } private func makeRequest(text: String, mode: EnhancementPrompt, retryCount: Int = 0) async throws -> String { @@ -418,12 +410,7 @@ class AIEnhancementService: ObservableObject { func enhance(_ text: String) async throws -> String { logger.notice("🚀 Starting AI enhancement for text (\(text.count) characters)") - let enhancementPrompt: EnhancementPrompt = { - if let activePrompt = activePrompt, activePrompt.id == PredefinedPrompts.assistantPromptId { - return .aiAssistant - } - return .transcriptionEnhancement - }() + let enhancementPrompt: EnhancementPrompt = .transcriptionEnhancement var retryCount = 0 while retryCount < maxRetries { @@ -477,16 +464,12 @@ class AIEnhancementService: ObservableObject { } func updatePrompt(_ prompt: CustomPrompt) { - if prompt.isPredefined { return } - if let index = customPrompts.firstIndex(where: { $0.id == prompt.id }) { customPrompts[index] = prompt } } func deletePrompt(_ prompt: CustomPrompt) { - if prompt.isPredefined { return } - customPrompts.removeAll { $0.id == prompt.id } if selectedPromptId == prompt.id { selectedPromptId = allPrompts.first?.id @@ -511,6 +494,31 @@ class AIEnhancementService: ObservableObject { private func getRetryDelay(for retryCount: Int) -> TimeInterval { return retryCount == 1 ? 1.0 : 2.0 } + + private func initializePredefinedPrompts() { + let predefinedTemplates = PredefinedPrompts.createDefaultPrompts() + + for template in predefinedTemplates { + if let existingIndex = customPrompts.firstIndex(where: { $0.id == template.id }) { + // Update existing predefined prompt: only update prompt text, preserve trigger word + var updatedPrompt = customPrompts[existingIndex] + updatedPrompt = CustomPrompt( + id: updatedPrompt.id, + title: template.title, + promptText: template.promptText, // Update from template + isActive: updatedPrompt.isActive, + icon: template.icon, + description: template.description, + isPredefined: true, + triggerWord: updatedPrompt.triggerWord // Preserve user's trigger word + ) + customPrompts[existingIndex] = updatedPrompt + } else { + // Add new predefined prompt (no default trigger word) + customPrompts.append(template) + } + } + } } enum EnhancementError: Error { diff --git a/VoiceInk/Services/PromptDetectionService.swift b/VoiceInk/Services/PromptDetectionService.swift index 8b17b87..d2ad3f2 100644 --- a/VoiceInk/Services/PromptDetectionService.swift +++ b/VoiceInk/Services/PromptDetectionService.swift @@ -20,21 +20,10 @@ class PromptDetectionService { let originalEnhancementState = enhancementService.isEnhancementEnabled let originalPromptId = enhancementService.selectedPromptId - if let result = checkAssistantTrigger(text: text, triggerWord: enhancementService.assistantTriggerWord) { - return PromptDetectionResult( - shouldEnableAI: true, - selectedPromptId: PredefinedPrompts.assistantPromptId, - processedText: result, - detectedTriggerWord: enhancementService.assistantTriggerWord, - originalEnhancementState: originalEnhancementState, - originalPromptId: originalPromptId - ) - } - for prompt in enhancementService.allPrompts { if let triggerWord = prompt.triggerWord?.trimmingCharacters(in: .whitespacesAndNewlines), !triggerWord.isEmpty, - let result = checkCustomTrigger(text: text, triggerWord: triggerWord) { + let result = removeTriggerWord(from: text, triggerWord: triggerWord) { return PromptDetectionResult( shouldEnableAI: true, @@ -87,14 +76,6 @@ class PromptDetectionService { } } - private func checkAssistantTrigger(text: String, triggerWord: String) -> String? { - return removeTriggerWord(from: text, triggerWord: triggerWord) - } - - private func checkCustomTrigger(text: String, triggerWord: String) -> String? { - return removeTriggerWord(from: text, triggerWord: triggerWord) - } - private func removeTriggerWord(from text: String, triggerWord: String) -> String? { let trimmedText = text.trimmingCharacters(in: .whitespacesAndNewlines) let lowerText = trimmedText.lowercased() diff --git a/VoiceInk/Views/Components/PromptSelectionGrid.swift b/VoiceInk/Views/Components/PromptSelectionGrid.swift index 20c2179..5d52af8 100644 --- a/VoiceInk/Views/Components/PromptSelectionGrid.swift +++ b/VoiceInk/Views/Components/PromptSelectionGrid.swift @@ -4,32 +4,63 @@ import SwiftUI struct PromptSelectionGrid: View { @EnvironmentObject private var enhancementService: AIEnhancementService + let prompts: [CustomPrompt] let selectedPromptId: UUID? - let onPromptTap: (CustomPrompt) -> Void - let onPromptEdit: (CustomPrompt) -> Void - let onPromptDelete: (CustomPrompt) -> Void - let onAddNew: () -> Void - let assistantTriggerWord: String? + let onPromptSelected: (CustomPrompt) -> Void + let onEditPrompt: ((CustomPrompt) -> Void)? + let onDeletePrompt: ((CustomPrompt) -> Void)? + let onAddNewPrompt: (() -> Void)? init( + prompts: [CustomPrompt], selectedPromptId: UUID?, - onPromptTap: @escaping (CustomPrompt) -> Void, - onPromptEdit: @escaping (CustomPrompt) -> Void = { _ in }, - onPromptDelete: @escaping (CustomPrompt) -> Void = { _ in }, - onAddNew: @escaping () -> Void, - assistantTriggerWord: String? = nil + onPromptSelected: @escaping (CustomPrompt) -> Void, + onEditPrompt: ((CustomPrompt) -> Void)? = nil, + onDeletePrompt: ((CustomPrompt) -> Void)? = nil, + onAddNewPrompt: (() -> Void)? = nil ) { + self.prompts = prompts self.selectedPromptId = selectedPromptId - self.onPromptTap = onPromptTap - self.onPromptEdit = onPromptEdit - self.onPromptDelete = onPromptDelete - self.onAddNew = onAddNew - self.assistantTriggerWord = assistantTriggerWord + self.onPromptSelected = onPromptSelected + self.onEditPrompt = onEditPrompt + self.onDeletePrompt = onDeletePrompt + self.onAddNewPrompt = onAddNewPrompt + } + + private var sortedPrompts: [CustomPrompt] { + prompts.sorted { prompt1, prompt2 in + // Predefined prompts come first + if prompt1.isPredefined && !prompt2.isPredefined { + return true + } + if !prompt1.isPredefined && prompt2.isPredefined { + return false + } + + // Among predefined prompts: Default first, then Assistant + if prompt1.isPredefined && prompt2.isPredefined { + if prompt1.id == PredefinedPrompts.defaultPromptId { + return true + } + if prompt2.id == PredefinedPrompts.defaultPromptId { + return false + } + if prompt1.id == PredefinedPrompts.assistantPromptId { + return true + } + if prompt2.id == PredefinedPrompts.assistantPromptId { + return false + } + } + + // Custom prompts: sort alphabetically by title + return prompt1.title.localizedCaseInsensitiveCompare(prompt2.title) == .orderedAscending + } } var body: some View { VStack(alignment: .leading, spacing: 12) { - if enhancementService.allPrompts.isEmpty { + if sortedPrompts.isEmpty { Text("No prompts available") .foregroundColor(.secondary) .font(.caption) @@ -38,29 +69,42 @@ struct PromptSelectionGrid: View { GridItem(.adaptive(minimum: 80, maximum: 100), spacing: 36) ] - LazyVGrid(columns: columns, spacing: 24) { - ForEach(enhancementService.allPrompts) { prompt in + LazyVGrid(columns: columns, spacing: 16) { + ForEach(sortedPrompts) { prompt in prompt.promptIcon( isSelected: selectedPromptId == prompt.id, onTap: { withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { - onPromptTap(prompt) + onPromptSelected(prompt) } }, - onEdit: onPromptEdit, - onDelete: onPromptDelete, - assistantTriggerWord: assistantTriggerWord + onEdit: onEditPrompt, + onDelete: onDeletePrompt ) } - // Plus icon using the same styling as prompt icons - CustomPrompt.addNewButton { - onAddNew() + if let onAddNewPrompt = onAddNewPrompt { + CustomPrompt.addNewButton { + onAddNewPrompt() + } + .help("Add new prompt") } - .help("Add new prompt") } .padding(.vertical, 12) .padding(.horizontal, 16) + + // Helpful tip for users + HStack { + Image(systemName: "info.circle") + .font(.caption) + .foregroundColor(.secondary) + + Text("Right-click on prompts to edit or delete") + .font(.caption) + .foregroundColor(.secondary) + } + .padding(.top, 8) + .padding(.horizontal, 16) } } } diff --git a/VoiceInk/Views/EnhancementSettingsView.swift b/VoiceInk/Views/EnhancementSettingsView.swift index f7b5184..2906378 100644 --- a/VoiceInk/Views/EnhancementSettingsView.swift +++ b/VoiceInk/Views/EnhancementSettingsView.swift @@ -5,8 +5,6 @@ struct EnhancementSettingsView: View { @State private var isEditingPrompt = false @State private var isSettingsExpanded = true @State private var selectedPromptForEdit: CustomPrompt? - @State private var isEditingTriggerWord = false - @State private var tempTriggerWord = "" var body: some View { ScrollView { @@ -92,88 +90,22 @@ struct EnhancementSettingsView: View { // Prompts Section VStack(alignment: .leading, spacing: 12) { PromptSelectionGrid( + prompts: enhancementService.allPrompts, selectedPromptId: enhancementService.selectedPromptId, - onPromptTap: { prompt in + onPromptSelected: { prompt in enhancementService.setActivePrompt(prompt) }, - onPromptEdit: { prompt in + onEditPrompt: { prompt in selectedPromptForEdit = prompt }, - onPromptDelete: { prompt in + onDeletePrompt: { prompt in enhancementService.deletePrompt(prompt) }, - onAddNew: { + onAddNewPrompt: { isEditingPrompt = true - }, - assistantTriggerWord: enhancementService.assistantTriggerWord + } ) } - - Divider() - - // Assistant Mode Section - VStack(alignment: .leading, spacing: 12) { - HStack { - Text("Assistant Prompt") - .font(.subheadline) - Image(systemName: "sparkles") - .foregroundColor(.accentColor) - } - - Text("Configure how to trigger the AI assistant mode") - .font(.caption) - .foregroundColor(.secondary) - - VStack(alignment: .leading, spacing: 12) { - HStack { - Text("Current Trigger:") - .font(.subheadline) - Text("\"\(enhancementService.assistantTriggerWord)\"") - .font(.system(.subheadline, design: .monospaced)) - .foregroundColor(.accentColor) - } - - if isEditingTriggerWord { - VStack(alignment: .leading, spacing: 8) { - HStack { - TextField("New trigger word", text: $tempTriggerWord) - .textFieldStyle(.roundedBorder) - .frame(maxWidth: 200) - - Button("Save") { - enhancementService.assistantTriggerWord = tempTriggerWord - isEditingTriggerWord = false - } - .buttonStyle(.borderedProminent) - .disabled(tempTriggerWord.isEmpty) - - Button("Cancel") { - isEditingTriggerWord = false - tempTriggerWord = enhancementService.assistantTriggerWord - } - .buttonStyle(.bordered) - } - - Text("Default: \"hey\"") - .font(.caption) - .foregroundColor(.secondary) - } - } else { - Button("Change Trigger Word") { - tempTriggerWord = enhancementService.assistantTriggerWord - isEditingTriggerWord = true - } - .buttonStyle(.bordered) - } - } - - Text("Start with \"\(enhancementService.assistantTriggerWord), \" to use AI assistant mode") - .font(.caption) - .foregroundColor(.secondary) - Text("Instead of enhancing the text, VoiceInk will respond like a conversational AI assistant") - .font(.caption) - .foregroundColor(.secondary) - } } .padding() .background(Color(.windowBackgroundColor).opacity(0.4)) diff --git a/VoiceInk/Views/PromptEditorView.swift b/VoiceInk/Views/PromptEditorView.swift index ffe12a1..b3531d0 100644 --- a/VoiceInk/Views/PromptEditorView.swift +++ b/VoiceInk/Views/PromptEditorView.swift @@ -27,6 +27,13 @@ struct PromptEditorView: View { @State private var triggerWord: String @State private var showingPredefinedPrompts = false + private var isEditingPredefinedPrompt: Bool { + if case .edit(let prompt) = mode { + return prompt.isPredefined + } + return false + } + init(mode: Mode) { self.mode = mode switch mode { @@ -49,7 +56,7 @@ struct PromptEditorView: View { VStack(spacing: 0) { // Header with modern styling HStack { - Text(mode == .add ? "New Prompt" : "Edit Prompt") + Text(isEditingPredefinedPrompt ? "Edit Trigger Word" : (mode == .add ? "New Prompt" : "Edit Prompt")) .font(.title2) .fontWeight(.bold) Spacer() @@ -68,7 +75,7 @@ struct PromptEditorView: View { .fontWeight(.medium) } .buttonStyle(.borderedProminent) - .disabled(title.isEmpty || promptText.isEmpty) + .disabled(isEditingPredefinedPrompt ? false : (title.isEmpty || promptText.isEmpty)) .keyboardShortcut(.return, modifiers: .command) } } @@ -80,141 +87,157 @@ struct PromptEditorView: View { ScrollView { VStack(spacing: 24) { - // Title and Icon Section with improved layout - HStack(spacing: 20) { - // Title Field - VStack(alignment: .leading, spacing: 8) { - Text("Title") - .font(.headline) - .foregroundColor(.secondary) - TextField("Enter a short, descriptive title", text: $title) - .textFieldStyle(.roundedBorder) - .font(.body) - } - .frame(maxWidth: .infinity) - - // Icon Selector with preview - VStack(alignment: .leading, spacing: 8) { - Text("Icon") - .font(.headline) - .foregroundColor(.secondary) - - Menu { - IconMenuContent(selectedIcon: $selectedIcon) - } label: { - HStack { - Image(systemName: selectedIcon.rawValue) - .font(.system(size: 16)) - .foregroundColor(.accentColor) - .frame(width: 24) - - Text(selectedIcon.title) - .foregroundColor(.primary) - - Spacer() - - Image(systemName: "chevron.up.chevron.down") - .font(.system(size: 12)) - .foregroundColor(.secondary) - } - .padding(8) - .background(Color(NSColor.controlBackgroundColor)) - .cornerRadius(8) - } - .frame(width: 180) - } - } - .padding(.horizontal) - .padding(.top, 8) - - // Description Field - VStack(alignment: .leading, spacing: 8) { - Text("Description") - .font(.headline) - .foregroundColor(.secondary) - - Text("Add a brief description of what this prompt does") - .font(.subheadline) - .foregroundColor(.secondary) - - TextField("Enter a description", text: $description) - .textFieldStyle(.roundedBorder) - .font(.body) - } - .padding(.horizontal) - - // Trigger Word Field - VStack(alignment: .leading, spacing: 8) { - Text("Trigger Word") - .font(.headline) - .foregroundColor(.secondary) - - Text("Add a custom word to activate this prompt by voice (optional)") - .font(.subheadline) - .foregroundColor(.secondary) - - TextField("Enter a trigger word", text: $triggerWord) - .textFieldStyle(.roundedBorder) - .font(.body) - } - .padding(.horizontal) - - // Prompt Text Section with improved styling - VStack(alignment: .leading, spacing: 8) { - Text("Prompt Instructions") - .font(.headline) - .foregroundColor(.secondary) - - Text("Define how AI should enhance your transcriptions") - .font(.subheadline) - .foregroundColor(.secondary) - - TextEditor(text: $promptText) - .font(.system(.body, design: .monospaced)) - .frame(minHeight: 200) - .padding(12) - .background( - RoundedRectangle(cornerRadius: 8) - .fill(Color(NSColor.textBackgroundColor)) - ) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color.secondary.opacity(0.2), lineWidth: 1) - ) - } - .padding(.horizontal) - - if case .add = mode { - // Templates Section with modern styling + if isEditingPredefinedPrompt { + // Simplified view for predefined prompts - only trigger word editing VStack(alignment: .leading, spacing: 16) { - Text("Start with a Predefined Template") + Text("Editing: \(title)") .font(.title2) .fontWeight(.semibold) .foregroundColor(.primary) + .padding(.horizontal) + .padding(.top, 8) - let columns = [ - GridItem(.flexible(), spacing: 16), - GridItem(.flexible(), spacing: 16) - ] + Text("You can only customize the trigger word for system prompts.") + .font(.subheadline) + .foregroundColor(.secondary) + .padding(.horizontal) - LazyVGrid(columns: columns, spacing: 16) { - ForEach(PromptTemplates.all) { template in - CleanTemplateButton(prompt: template) { - title = template.title - promptText = template.promptText - selectedIcon = template.icon - description = template.description + // Trigger Word Field with same styling as custom prompts + VStack(alignment: .leading, spacing: 8) { + Text("Trigger Word") + .font(.headline) + .foregroundColor(.secondary) + + TextField("Enter a trigger word (optional)", text: $triggerWord) + .textFieldStyle(.roundedBorder) + .font(.body) + } + .padding(.horizontal) + } + .padding(.vertical, 20) + + } else { + // Full editing interface for custom prompts + // Title and Icon Section with improved layout + HStack(spacing: 20) { + // Title Field + VStack(alignment: .leading, spacing: 8) { + Text("Title") + .font(.headline) + .foregroundColor(.secondary) + TextField("Enter a short, descriptive title", text: $title) + .textFieldStyle(.roundedBorder) + .font(.body) + } + .frame(maxWidth: .infinity) + + // Icon Selector with preview + VStack(alignment: .leading, spacing: 8) { + Text("Icon") + .font(.headline) + .foregroundColor(.secondary) + + Menu { + IconMenuContent(selectedIcon: $selectedIcon) + } label: { + HStack { + Image(systemName: selectedIcon.rawValue) + .font(.system(size: 16)) + .foregroundColor(.accentColor) + .frame(width: 24) + + Text(selectedIcon.title) + .foregroundColor(.primary) + + Spacer() + + Image(systemName: "chevron.up.chevron.down") + .font(.system(size: 12)) + .foregroundColor(.secondary) } + .padding(8) + .background(Color(NSColor.controlBackgroundColor)) + .cornerRadius(8) } + .frame(width: 180) } } .padding(.horizontal) - .padding(.vertical, 16) - .background( - RoundedRectangle(cornerRadius: 16) - .fill(Color(.windowBackgroundColor).opacity(0.6)) - ) + .padding(.top, 8) + + // Description Field + VStack(alignment: .leading, spacing: 8) { + Text("Description") + .font(.headline) + .foregroundColor(.secondary) + + Text("Add a brief description of what this prompt does") + .font(.subheadline) + .foregroundColor(.secondary) + + TextField("Enter a description", text: $description) + .textFieldStyle(.roundedBorder) + .font(.body) + } .padding(.horizontal) + + // Prompt Text Section with improved styling + VStack(alignment: .leading, spacing: 8) { + Text("Prompt Instructions") + .font(.headline) + .foregroundColor(.secondary) + + Text("Define how AI should enhance your transcriptions") + .font(.subheadline) + .foregroundColor(.secondary) + + TextEditor(text: $promptText) + .font(.system(.body, design: .monospaced)) + .frame(minHeight: 200) + .padding(12) + .background( + RoundedRectangle(cornerRadius: 8) + .fill(Color(NSColor.textBackgroundColor)) + ) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.secondary.opacity(0.2), lineWidth: 1) + ) + } + .padding(.horizontal) + + if case .add = mode { + // Templates Section with modern styling + VStack(alignment: .leading, spacing: 16) { + Text("Start with a Predefined Template") + .font(.title2) + .fontWeight(.semibold) + .foregroundColor(.primary) + + let columns = [ + GridItem(.flexible(), spacing: 16), + GridItem(.flexible(), spacing: 16) + ] + + LazyVGrid(columns: columns, spacing: 16) { + ForEach(PromptTemplates.all) { template in + CleanTemplateButton(prompt: template) { + title = template.title + promptText = template.promptText + selectedIcon = template.icon + description = template.description + } + } + } + } + .padding(.horizontal) + .padding(.vertical, 16) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(Color(.windowBackgroundColor).opacity(0.6)) + ) + .padding(.horizontal) + } } } .padding(.vertical, 20) @@ -236,11 +259,12 @@ struct PromptEditorView: View { case .edit(let prompt): let updatedPrompt = CustomPrompt( id: prompt.id, - title: title, - promptText: promptText, + title: prompt.isPredefined ? prompt.title : title, + promptText: prompt.isPredefined ? prompt.promptText : promptText, isActive: prompt.isActive, - icon: selectedIcon, - description: description.isEmpty ? nil : description, + icon: prompt.isPredefined ? prompt.icon : selectedIcon, + description: prompt.isPredefined ? prompt.description : (description.isEmpty ? nil : description), + isPredefined: prompt.isPredefined, triggerWord: triggerWord.isEmpty ? nil : triggerWord ) enhancementService.updatePrompt(updatedPrompt)