284 lines
11 KiB
Swift
284 lines
11 KiB
Swift
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?
|
|
|
|
private var isPanelOpen: Bool {
|
|
isEditingPrompt || selectedPromptForEdit != nil
|
|
}
|
|
|
|
private func closePanel() {
|
|
withAnimation(.spring(response: 0.4, dampingFraction: 0.9)) {
|
|
isEditingPrompt = false
|
|
selectedPromptForEdit = nil
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack(alignment: .topLeading) {
|
|
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)
|
|
|
|
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")
|
|
}
|
|
|
|
APIKeyManagementView()
|
|
.opacity(enhancementService.isEnhancementEnabled ? 1.0 : 0.8)
|
|
|
|
Section {
|
|
ReorderablePromptGrid(
|
|
selectedPromptId: enhancementService.selectedPromptId,
|
|
onPromptSelected: { prompt in
|
|
enhancementService.setActivePrompt(prompt)
|
|
},
|
|
onEditPrompt: { prompt in
|
|
withAnimation(.spring(response: 0.4, dampingFraction: 0.9)) {
|
|
selectedPromptForEdit = prompt
|
|
}
|
|
},
|
|
onDeletePrompt: { prompt in
|
|
enhancementService.deletePrompt(prompt)
|
|
}
|
|
)
|
|
.padding(.vertical, 8)
|
|
} header: {
|
|
HStack {
|
|
Text("Enhancement Prompts")
|
|
Spacer()
|
|
Button {
|
|
withAnimation(.spring(response: 0.4, dampingFraction: 0.9)) {
|
|
isEditingPrompt = true
|
|
}
|
|
} label: {
|
|
Image(systemName: "plus.circle.fill")
|
|
.font(.system(size: 18))
|
|
.symbolRenderingMode(.hierarchical)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.help("Add new prompt")
|
|
}
|
|
}
|
|
.opacity(enhancementService.isEnhancementEnabled ? 1.0 : 0.8)
|
|
|
|
Section {
|
|
DisclosureGroup(isExpanded: $isShortcutsExpanded) {
|
|
EnhancementShortcutsView()
|
|
.padding(.vertical, 8)
|
|
} label: {
|
|
HStack {
|
|
Text("Shortcuts")
|
|
.font(.headline)
|
|
.foregroundColor(.primary)
|
|
Spacer()
|
|
}
|
|
.contentShape(Rectangle())
|
|
.onTapGesture {
|
|
withAnimation {
|
|
isShortcutsExpanded.toggle()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.opacity(enhancementService.isEnhancementEnabled ? 1.0 : 0.8)
|
|
}
|
|
.formStyle(.grouped)
|
|
.scrollContentBackground(.hidden)
|
|
.background(Color(NSColor.controlBackgroundColor))
|
|
.disabled(isPanelOpen)
|
|
.blur(radius: isPanelOpen ? 2 : 0)
|
|
.animation(.spring(response: 0.4, dampingFraction: 0.9), value: isPanelOpen)
|
|
|
|
if isPanelOpen {
|
|
Color.black.opacity(0.2)
|
|
.ignoresSafeArea()
|
|
.onTapGesture {
|
|
closePanel()
|
|
}
|
|
.transition(.opacity)
|
|
.zIndex(1)
|
|
}
|
|
|
|
if isPanelOpen {
|
|
HStack(spacing: 0) {
|
|
Spacer()
|
|
|
|
Group {
|
|
if let prompt = selectedPromptForEdit {
|
|
PromptEditorView(mode: .edit(prompt)) {
|
|
closePanel()
|
|
}
|
|
} else if isEditingPrompt {
|
|
PromptEditorView(mode: .add) {
|
|
closePanel()
|
|
}
|
|
}
|
|
}
|
|
.frame(width: 450)
|
|
.frame(maxHeight: .infinity)
|
|
.background(
|
|
Color(NSColor.windowBackgroundColor)
|
|
)
|
|
.overlay(
|
|
Divider(), alignment: .leading
|
|
)
|
|
.shadow(color: .black.opacity(0.15), radius: 12, x: -4, y: 0)
|
|
.transition(.move(edge: .trailing).combined(with: .opacity))
|
|
}
|
|
.ignoresSafeArea()
|
|
.zIndex(2)
|
|
}
|
|
}
|
|
.frame(minWidth: 500, minHeight: 400)
|
|
}
|
|
}
|
|
|
|
// MARK: - 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)?
|
|
|
|
@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
|
|
)
|
|
)
|
|
}
|
|
}
|
|
.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 Delegate
|
|
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 }
|
|
|
|
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
|
|
}
|
|
}
|