import SwiftUI import UniformTypeIdentifiers struct EnhancementSettingsView: View { @EnvironmentObject private var enhancementService: AIEnhancementService @State private var isEditingPrompt = false @State private var isShortcutsExpanded = false @State private var selectedPromptForEdit: CustomPrompt? var body: some View { Form { Section { Toggle(isOn: $enhancementService.isEnhancementEnabled) { HStack(spacing: 4) { Text("Enable Enhancement") InfoTip( title: "AI Enhancement", message: "AI enhancement lets you pass the transcribed audio through LLMs to post-process using different prompts suitable for different use cases like e-mails, summary, writing, etc.", learnMoreURL: "https://tryvoiceink.com/docs/enhancements-configuring-models" ) } } .toggleStyle(.switch) // Context Toggles in the same row HStack(spacing: 24) { Toggle(isOn: $enhancementService.useClipboardContext) { HStack(spacing: 4) { Text("Clipboard Context") InfoTip( title: "Clipboard Context", message: "Use text from clipboard to understand the context" ) } } .toggleStyle(.switch) Toggle(isOn: $enhancementService.useScreenCaptureContext) { HStack(spacing: 4) { Text("Screen Context") InfoTip( title: "Context Awareness", message: "Learn what is on the screen to understand the context" ) } } .toggleStyle(.switch) } .opacity(enhancementService.isEnhancementEnabled ? 1.0 : 0.8) } header: { Text("General") } // API Key Management (Consolidated Section) APIKeyManagementView() .opacity(enhancementService.isEnhancementEnabled ? 1.0 : 0.8) Section("Enhancement Prompts") { // Reorderable prompts grid with drag-and-drop ReorderablePromptGrid( selectedPromptId: enhancementService.selectedPromptId, onPromptSelected: { prompt in enhancementService.setActivePrompt(prompt) }, onEditPrompt: { prompt in selectedPromptForEdit = prompt }, onDeletePrompt: { prompt in enhancementService.deletePrompt(prompt) }, onAddNewPrompt: { isEditingPrompt = true } ) .padding(.vertical, 8) } .opacity(enhancementService.isEnhancementEnabled ? 1.0 : 0.8) Section { DisclosureGroup(isExpanded: $isShortcutsExpanded) { EnhancementShortcutsView() .padding(.vertical, 8) } label: { Text("Shortcuts") .font(.headline) .foregroundColor(.primary) } } .opacity(enhancementService.isEnhancementEnabled ? 1.0 : 0.8) } .formStyle(.grouped) .scrollContentBackground(.hidden) .background(Color(NSColor.controlBackgroundColor)) .frame(minWidth: 500, minHeight: 400) .sheet(isPresented: $isEditingPrompt) { PromptEditorView(mode: .add) } .sheet(item: $selectedPromptForEdit) { prompt in PromptEditorView(mode: .edit(prompt)) } } } // MARK: - Drag & Drop Reorderable Grid private struct ReorderablePromptGrid: View { @EnvironmentObject private var enhancementService: AIEnhancementService let selectedPromptId: UUID? let onPromptSelected: (CustomPrompt) -> Void let onEditPrompt: ((CustomPrompt) -> Void)? let onDeletePrompt: ((CustomPrompt) -> Void)? let onAddNewPrompt: (() -> Void)? @State private var draggingItem: CustomPrompt? var body: some View { VStack(alignment: .leading, spacing: 12) { if enhancementService.customPrompts.isEmpty { Text("No prompts available") .foregroundColor(.secondary) .font(.caption) } else { let columns = [ GridItem(.adaptive(minimum: 80, maximum: 100), spacing: 36) ] LazyVGrid(columns: columns, spacing: 16) { ForEach(enhancementService.customPrompts) { prompt in prompt.promptIcon( isSelected: selectedPromptId == prompt.id, onTap: { withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { onPromptSelected(prompt) } }, onEdit: onEditPrompt, onDelete: onDeletePrompt ) .opacity(draggingItem?.id == prompt.id ? 0.3 : 1.0) .scaleEffect(draggingItem?.id == prompt.id ? 1.05 : 1.0) .overlay( RoundedRectangle(cornerRadius: 14) .stroke( draggingItem != nil && draggingItem?.id != prompt.id ? Color.accentColor.opacity(0.25) : Color.clear, lineWidth: 1 ) ) .animation(.easeInOut(duration: 0.15), value: draggingItem?.id == prompt.id) .onDrag { draggingItem = prompt return NSItemProvider(object: prompt.id.uuidString as NSString) } .onDrop( of: [UTType.text], delegate: PromptDropDelegate( item: prompt, prompts: $enhancementService.customPrompts, draggingItem: $draggingItem ) ) } if let onAddNewPrompt = onAddNewPrompt { CustomPrompt.addNewButton { onAddNewPrompt() } .help("Add new prompt") .onDrop( of: [UTType.text], delegate: PromptEndDropDelegate( prompts: $enhancementService.customPrompts, draggingItem: $draggingItem ) ) } } .padding(.vertical, 12) .padding(.horizontal, 16) HStack { Image(systemName: "info.circle") .font(.caption) .foregroundColor(.secondary) Text("Double-click to edit • Right-click for more options") .font(.caption) .foregroundColor(.secondary) } .padding(.top, 8) .padding(.horizontal, 16) } } } } // MARK: - Drop Delegates private struct PromptDropDelegate: DropDelegate { let item: CustomPrompt @Binding var prompts: [CustomPrompt] @Binding var draggingItem: CustomPrompt? func dropEntered(info: DropInfo) { guard let draggingItem = draggingItem, draggingItem != item else { return } guard let fromIndex = prompts.firstIndex(of: draggingItem), let toIndex = prompts.firstIndex(of: item) else { return } // Move item as you hover for immediate visual update if prompts[toIndex].id != draggingItem.id { withAnimation(.easeInOut(duration: 0.12)) { let from = fromIndex let to = toIndex prompts.move(fromOffsets: IndexSet(integer: from), toOffset: to > from ? to + 1 : to) } } } func dropUpdated(info: DropInfo) -> DropProposal? { DropProposal(operation: .move) } func performDrop(info: DropInfo) -> Bool { draggingItem = nil return true } } private struct PromptEndDropDelegate: DropDelegate { @Binding var prompts: [CustomPrompt] @Binding var draggingItem: CustomPrompt? func validateDrop(info: DropInfo) -> Bool { true } func dropUpdated(info: DropInfo) -> DropProposal? { DropProposal(operation: .move) } func performDrop(info: DropInfo) -> Bool { guard let draggingItem = draggingItem, let currentIndex = prompts.firstIndex(of: draggingItem) else { self.draggingItem = nil return false } // Move to end if dropped on the trailing "Add New" tile withAnimation(.easeInOut(duration: 0.12)) { prompts.move(fromOffsets: IndexSet(integer: currentIndex), toOffset: prompts.endIndex) } self.draggingItem = nil return true } }