Improve Power Mode configuration UI with native macOS form and pickers
This commit is contained in:
parent
c7fe067252
commit
42cde699a8
@ -99,7 +99,6 @@ struct EmojiPickerView: View {
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(.regularMaterial)
|
||||
.frame(minWidth: 260, idealWidth: 300, maxWidth: 320, minHeight: 150, idealHeight: 280, maxHeight: 350)
|
||||
.alert("Emoji in Use", isPresented: $showingEmojiInUseAlert, presenting: emojiForAlert) { emojiStr in
|
||||
Button("OK", role: .cancel) { }
|
||||
@ -161,10 +160,6 @@ private struct EmojiButton: View {
|
||||
Text(emoji)
|
||||
.font(.largeTitle)
|
||||
.frame(width: 44, height: 44)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(isSelected ? Color.accentColor.opacity(0.25) : Color.clear)
|
||||
)
|
||||
.overlay(
|
||||
Circle()
|
||||
.strokeBorder(isSelected ? Color.accentColor : Color.gray.opacity(0.3), lineWidth: isSelected ? 2 : 1)
|
||||
@ -197,10 +192,6 @@ private struct AddEmojiButton: View {
|
||||
.labelStyle(.iconOnly)
|
||||
.foregroundColor(.accentColor)
|
||||
.frame(width: 44, height: 44)
|
||||
.background(
|
||||
Circle()
|
||||
.fill(Color.secondary.opacity(0.1))
|
||||
)
|
||||
.overlay(
|
||||
Circle()
|
||||
.strokeBorder(Color.gray.opacity(0.3), lineWidth: 1)
|
||||
|
||||
@ -39,9 +39,7 @@ struct ConfigurationView: View {
|
||||
@State private var isAutoSendEnabled = false
|
||||
@State private var isDefault = false
|
||||
|
||||
// State for prompt editing (similar to EnhancementSettingsView)
|
||||
@State private var isEditingPrompt = false
|
||||
@State private var selectedPromptForEdit: CustomPrompt?
|
||||
@State private var isShowingDeleteConfirmation = false
|
||||
|
||||
// PowerMode hotkey configuration
|
||||
@State private var powerModeConfigId: UUID = UUID()
|
||||
@ -126,550 +124,373 @@ struct ConfigurationView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
// Header with Title and Cancel button
|
||||
HStack {
|
||||
Text(mode.title)
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
|
||||
Spacer()
|
||||
|
||||
if case .edit(let config) = mode {
|
||||
Button("Delete") {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Delete Power Mode?"
|
||||
alert.informativeText = "Are you sure you want to delete the '\(config.name)' power mode? This action cannot be undone."
|
||||
alert.alertStyle = .warning
|
||||
alert.addButton(withTitle: "Delete")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
|
||||
// Style the Delete button as destructive
|
||||
alert.buttons[0].hasDestructiveAction = true
|
||||
|
||||
let response = alert.runModal()
|
||||
if response == .alertFirstButtonReturn {
|
||||
powerModeManager.removeConfiguration(with: config.id)
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
Form {
|
||||
Section("General") {
|
||||
HStack(spacing: 12) {
|
||||
Button {
|
||||
isShowingEmojiPicker.toggle()
|
||||
} label: {
|
||||
Text(selectedEmoji)
|
||||
.font(.system(size: 22))
|
||||
.frame(width: 32, height: 32)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color(NSColor.controlBackgroundColor))
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.popover(isPresented: $isShowingEmojiPicker, arrowEdge: .bottom) {
|
||||
EmojiPickerView(
|
||||
selectedEmoji: $selectedEmoji,
|
||||
isPresented: $isShowingEmojiPicker
|
||||
)
|
||||
}
|
||||
|
||||
TextField("Name", text: $configName)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.focused($isNameFieldFocused)
|
||||
}
|
||||
|
||||
Toggle(isOn: $isDefault) {
|
||||
HStack(spacing: 6) {
|
||||
Text("Set as default power mode")
|
||||
InfoTip(
|
||||
title: "Default Power Mode",
|
||||
message: "Default power mode is used when no specific app or website matches are found"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("When to Trigger") {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack {
|
||||
Text("Applications")
|
||||
Spacer()
|
||||
Button("Add…") {
|
||||
loadInstalledApps()
|
||||
isShowingAppPicker = true
|
||||
}
|
||||
}
|
||||
.foregroundColor(.red)
|
||||
.padding(.trailing, 8)
|
||||
|
||||
if selectedAppConfigs.isEmpty {
|
||||
Text("No applications added")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.subheadline)
|
||||
} else {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 44, maximum: 50), spacing: 10)], spacing: 10) {
|
||||
ForEach(selectedAppConfigs) { appConfig in
|
||||
ZStack(alignment: .topTrailing) {
|
||||
if let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: appConfig.bundleIdentifier) {
|
||||
Image(nsImage: NSWorkspace.shared.icon(forFile: appURL.path))
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 44, height: 44)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
} else {
|
||||
Image(systemName: "app.fill")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 26, height: 26)
|
||||
.frame(width: 44, height: 44)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(Color(NSColor.controlBackgroundColor))
|
||||
)
|
||||
}
|
||||
|
||||
Button {
|
||||
selectedAppConfigs.removeAll(where: { $0.id == appConfig.id })
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.offset(x: 6, y: -6)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
|
||||
.padding(.vertical, 2)
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("Websites")
|
||||
|
||||
HStack {
|
||||
TextField("Enter website URL (e.g., google.com)", text: $newWebsiteURL)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.onSubmit { addWebsite() }
|
||||
|
||||
Button {
|
||||
addWebsite()
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
.disabled(newWebsiteURL.isEmpty)
|
||||
}
|
||||
|
||||
if websiteConfigs.isEmpty {
|
||||
Text("No websites added")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.subheadline)
|
||||
} else {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 140, maximum: 220), spacing: 10)], spacing: 10) {
|
||||
ForEach(websiteConfigs) { urlConfig in
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "globe")
|
||||
.foregroundColor(.secondary)
|
||||
Text(urlConfig.url)
|
||||
.lineLimit(1)
|
||||
Spacer(minLength: 0)
|
||||
Button {
|
||||
websiteConfigs.removeAll(where: { $0.id == urlConfig.id })
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 6)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color(NSColor.controlBackgroundColor))
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
|
||||
Section("Transcription") {
|
||||
if whisperState.usableModels.isEmpty {
|
||||
Text("No transcription models available. Please connect to a cloud service or download a local model in the AI Models tab.")
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
let modelBinding = Binding<String?>(
|
||||
get: { selectedTranscriptionModelName ?? whisperState.usableModels.first?.name },
|
||||
set: { selectedTranscriptionModelName = $0 }
|
||||
)
|
||||
|
||||
Picker("Model", selection: modelBinding) {
|
||||
ForEach(whisperState.usableModels, id: \.name) { model in
|
||||
Text(model.displayName).tag(model.name as String?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if languageSelectionDisabled() {
|
||||
LabeledContent("Language") {
|
||||
Text("Autodetected")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
} else if let selectedModel = effectiveModelName,
|
||||
let modelInfo = whisperState.allAvailableModels.first(where: { $0.name == selectedModel }),
|
||||
modelInfo.isMultilingualModel {
|
||||
let languageBinding = Binding<String?>(
|
||||
get: { selectedLanguage ?? UserDefaults.standard.string(forKey: "SelectedLanguage") ?? "auto" },
|
||||
set: { selectedLanguage = $0 }
|
||||
)
|
||||
|
||||
Picker("Language", selection: languageBinding) {
|
||||
ForEach(modelInfo.supportedLanguages.sorted(by: {
|
||||
if $0.key == "auto" { return true }
|
||||
if $1.key == "auto" { return false }
|
||||
return $0.value < $1.value
|
||||
}), id: \.key) { key, value in
|
||||
Text(value).tag(key as String?)
|
||||
}
|
||||
}
|
||||
} else if let selectedModel = effectiveModelName,
|
||||
let modelInfo = whisperState.allAvailableModels.first(where: { $0.name == selectedModel }),
|
||||
!modelInfo.isMultilingualModel {
|
||||
EmptyView()
|
||||
.onAppear {
|
||||
if selectedLanguage == nil {
|
||||
selectedLanguage = "en"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("AI Enhancement") {
|
||||
Toggle("Enable AI Enhancement", isOn: $isAIEnhancementEnabled)
|
||||
.onChange(of: isAIEnhancementEnabled) { _, newValue in
|
||||
if newValue {
|
||||
if selectedAIProvider == nil {
|
||||
selectedAIProvider = aiService.selectedProvider.rawValue
|
||||
}
|
||||
if selectedAIModel == nil {
|
||||
selectedAIModel = aiService.currentModel
|
||||
}
|
||||
if selectedPromptId == nil {
|
||||
selectedPromptId = enhancementService.allPrompts.first?.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let providerBinding = Binding<AIProvider>(
|
||||
get: {
|
||||
if let providerName = selectedAIProvider,
|
||||
let provider = AIProvider(rawValue: providerName) {
|
||||
return provider
|
||||
}
|
||||
return aiService.selectedProvider
|
||||
},
|
||||
set: { newValue in
|
||||
selectedAIProvider = newValue.rawValue
|
||||
aiService.selectedProvider = newValue
|
||||
selectedAIModel = nil
|
||||
}
|
||||
)
|
||||
|
||||
if isAIEnhancementEnabled {
|
||||
if aiService.connectedProviders.isEmpty {
|
||||
LabeledContent("AI Provider") {
|
||||
Text("No providers connected")
|
||||
.foregroundColor(.secondary)
|
||||
.italic()
|
||||
}
|
||||
} else {
|
||||
Picker("AI Provider", selection: providerBinding) {
|
||||
ForEach(aiService.connectedProviders.filter { $0 != .elevenLabs && $0 != .deepgram }, id: \.self) { provider in
|
||||
Text(provider.rawValue).tag(provider)
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedAIProvider) { _, newValue in
|
||||
if let provider = newValue.flatMap({ AIProvider(rawValue: $0) }) {
|
||||
selectedAIModel = provider.defaultModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let providerName = selectedAIProvider ?? aiService.selectedProvider.rawValue
|
||||
if let provider = AIProvider(rawValue: providerName),
|
||||
provider != .custom {
|
||||
if aiService.availableModels.isEmpty {
|
||||
LabeledContent("AI Model") {
|
||||
Text(provider == .openRouter ? "No models loaded" : "No models available")
|
||||
.foregroundColor(.secondary)
|
||||
.italic()
|
||||
}
|
||||
} else {
|
||||
let modelBinding = Binding<String>(
|
||||
get: {
|
||||
if let model = selectedAIModel, !model.isEmpty { return model }
|
||||
return aiService.currentModel
|
||||
},
|
||||
set: { newModelValue in
|
||||
selectedAIModel = newModelValue
|
||||
aiService.selectModel(newModelValue)
|
||||
}
|
||||
)
|
||||
|
||||
let models = provider == .openRouter ? aiService.availableModels : (provider == .ollama ? aiService.availableModels : provider.availableModels)
|
||||
|
||||
Picker("AI Model", selection: modelBinding) {
|
||||
ForEach(models, id: \.self) { model in
|
||||
Text(model).tag(model)
|
||||
}
|
||||
}
|
||||
|
||||
if provider == .openRouter {
|
||||
Button("Refresh Models") {
|
||||
Task { await aiService.fetchOpenRouterModels() }
|
||||
}
|
||||
.help("Refresh models")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if enhancementService.allPrompts.isEmpty {
|
||||
LabeledContent("Enhancement Prompt") {
|
||||
Text("No prompts available")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
} else {
|
||||
Picker("Enhancement Prompt", selection: $selectedPromptId) {
|
||||
ForEach(enhancementService.allPrompts) { prompt in
|
||||
Text(prompt.title).tag(prompt.id as UUID?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Toggle("Context Awareness", isOn: $useScreenCapture)
|
||||
}
|
||||
}
|
||||
|
||||
Section("Advanced") {
|
||||
Toggle(isOn: $isAutoSendEnabled) {
|
||||
HStack(spacing: 6) {
|
||||
Text("Auto Send")
|
||||
InfoTip(
|
||||
title: "Auto Send",
|
||||
message: "Automatically presses the Return/Enter key after pasting text. This is useful for chat applications or forms where its not necessary to to make changes to the transcribed text"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Keyboard Shortcut")
|
||||
InfoTip(
|
||||
title: "Power Mode Hotkey",
|
||||
message: "Assign a unique keyboard shortcut to instantly activate this Power Mode and start recording"
|
||||
)
|
||||
|
||||
Spacer()
|
||||
|
||||
KeyboardShortcuts.Recorder(for: .powerMode(id: powerModeConfigId))
|
||||
.controlSize(.regular)
|
||||
.frame(minHeight: 28)
|
||||
}
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.navigationTitle(mode.title)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
.keyboardShortcut(.escape, modifiers: [])
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.top)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
Divider()
|
||||
|
||||
ScrollView {
|
||||
VStack(spacing: 20) {
|
||||
// Main Input Section
|
||||
VStack(spacing: 16) {
|
||||
HStack(spacing: 16) {
|
||||
Button(action: {
|
||||
isShowingEmojiPicker.toggle()
|
||||
}) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.accentColor.opacity(0.15))
|
||||
.frame(width: 48, height: 48)
|
||||
|
||||
Text(selectedEmoji)
|
||||
.font(.system(size: 24))
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.popover(isPresented: $isShowingEmojiPicker, arrowEdge: .bottom) {
|
||||
EmojiPickerView(
|
||||
selectedEmoji: $selectedEmoji,
|
||||
isPresented: $isShowingEmojiPicker
|
||||
)
|
||||
}
|
||||
|
||||
TextField("Name your power mode", text: $configName)
|
||||
.font(.system(size: 18, weight: .bold))
|
||||
.textFieldStyle(.plain)
|
||||
.foregroundColor(.primary)
|
||||
.tint(.accentColor)
|
||||
.focused($isNameFieldFocused)
|
||||
}
|
||||
|
||||
// Default Power Mode Toggle
|
||||
HStack {
|
||||
Toggle("Set as default power mode", isOn: $isDefault)
|
||||
.font(.system(size: 14))
|
||||
|
||||
InfoTip(
|
||||
title: "Default Power Mode",
|
||||
message: "Default power mode is used when no specific app or website matches are found"
|
||||
)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 16)
|
||||
.background(CardBackground(isSelected: false))
|
||||
.padding(.horizontal)
|
||||
.onAppear {
|
||||
// Add a small delay to ensure the view is fully loaded
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
isNameFieldFocused = true
|
||||
}
|
||||
}
|
||||
|
||||
VStack(spacing: 16) {
|
||||
SectionHeader(title: "When to Trigger")
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
Text("Applications")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
loadInstalledApps()
|
||||
isShowingAppPicker = true
|
||||
}) {
|
||||
Label("Add App", systemImage: "plus.circle.fill")
|
||||
.font(.subheadline)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
if selectedAppConfigs.isEmpty {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("No applications added")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.subheadline)
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.background(CardBackground(isSelected: false))
|
||||
} else {
|
||||
// Grid of selected apps that wraps to next line
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 50, maximum: 55), spacing: 10)], spacing: 10) {
|
||||
ForEach(selectedAppConfigs) { appConfig in
|
||||
VStack {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
// App icon - completely filling the container
|
||||
if let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: appConfig.bundleIdentifier) {
|
||||
Image(nsImage: NSWorkspace.shared.icon(forFile: appURL.path))
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 50, height: 50)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
} else {
|
||||
Image(systemName: "app.fill")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 50, height: 50)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
|
||||
// Remove button
|
||||
Button(action: {
|
||||
selectedAppConfigs.removeAll(where: { $0.id == appConfig.id })
|
||||
}) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white)
|
||||
.background(Circle().fill(Color.black.opacity(0.6)))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.offset(x: 6, y: -6)
|
||||
}
|
||||
}
|
||||
.frame(width: 50, height: 50)
|
||||
.background(CardBackground(isSelected: false, cornerRadius: 10))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Websites")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
// Add URL Field
|
||||
HStack {
|
||||
TextField("Enter website URL (e.g., google.com)", text: $newWebsiteURL)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.onSubmit {
|
||||
addWebsite()
|
||||
}
|
||||
|
||||
Button(action: addWebsite) {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
.font(.system(size: 18))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(newWebsiteURL.isEmpty)
|
||||
}
|
||||
|
||||
if websiteConfigs.isEmpty {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("No websites added")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.subheadline)
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.background(CardBackground(isSelected: false))
|
||||
} else {
|
||||
// Grid of website tags that wraps to next line
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100, maximum: 160), spacing: 10)], spacing: 10) {
|
||||
ForEach(websiteConfigs) { urlConfig in
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "globe")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.accentColor)
|
||||
|
||||
Text(urlConfig.url)
|
||||
.font(.system(size: 11))
|
||||
.lineLimit(1)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
Button(action: {
|
||||
websiteConfigs.removeAll(where: { $0.id == urlConfig.id })
|
||||
}) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.system(size: 9))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 6)
|
||||
.frame(height: 28)
|
||||
.background(CardBackground(isSelected: false, cornerRadius: 10))
|
||||
}
|
||||
}
|
||||
.padding(8)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(CardBackground(isSelected: false))
|
||||
.padding(.horizontal)
|
||||
|
||||
VStack(spacing: 16) {
|
||||
SectionHeader(title: "Transcription")
|
||||
|
||||
if whisperState.usableModels.isEmpty {
|
||||
Text("No transcription models available. Please connect to a cloud service or download a local model in the AI Models tab.")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.background(CardBackground(isSelected: false))
|
||||
} else {
|
||||
let modelBinding = Binding<String?>(
|
||||
get: {
|
||||
selectedTranscriptionModelName ?? whisperState.usableModels.first?.name
|
||||
},
|
||||
set: { selectedTranscriptionModelName = $0 }
|
||||
)
|
||||
|
||||
HStack {
|
||||
Text("Model")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Picker("", selection: modelBinding) {
|
||||
ForEach(whisperState.usableModels, id: \.name) { model in
|
||||
Text(model.displayName).tag(model.name as String?)
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
if languageSelectionDisabled() {
|
||||
HStack {
|
||||
Text("Language")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text("Autodetected")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
} else if let selectedModel = effectiveModelName,
|
||||
let modelInfo = whisperState.allAvailableModels.first(where: { $0.name == selectedModel }),
|
||||
modelInfo.isMultilingualModel {
|
||||
|
||||
let languageBinding = Binding<String?>(
|
||||
get: {
|
||||
selectedLanguage ?? UserDefaults.standard.string(forKey: "SelectedLanguage") ?? "auto"
|
||||
},
|
||||
set: { selectedLanguage = $0 }
|
||||
)
|
||||
|
||||
HStack {
|
||||
Text("Language")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Picker("", selection: languageBinding) {
|
||||
ForEach(modelInfo.supportedLanguages.sorted(by: {
|
||||
if $0.key == "auto" { return true }
|
||||
if $1.key == "auto" { return false }
|
||||
return $0.value < $1.value
|
||||
}), id: \.key) { key, value in
|
||||
Text(value).tag(key as String?)
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
|
||||
Spacer()
|
||||
}
|
||||
} else if let selectedModel = effectiveModelName,
|
||||
let modelInfo = whisperState.allAvailableModels.first(where: { $0.name == selectedModel }),
|
||||
!modelInfo.isMultilingualModel {
|
||||
|
||||
EmptyView()
|
||||
.onAppear {
|
||||
if selectedLanguage == nil {
|
||||
selectedLanguage = "en"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(CardBackground(isSelected: false))
|
||||
.padding(.horizontal)
|
||||
|
||||
VStack(spacing: 16) {
|
||||
SectionHeader(title: "AI Enhancement")
|
||||
|
||||
Toggle("Enable AI Enhancement", isOn: $isAIEnhancementEnabled)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.onChange(of: isAIEnhancementEnabled) { oldValue, newValue in
|
||||
if newValue {
|
||||
if selectedAIProvider == nil {
|
||||
selectedAIProvider = aiService.selectedProvider.rawValue
|
||||
}
|
||||
if selectedAIModel == nil {
|
||||
selectedAIModel = aiService.currentModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
let providerBinding = Binding<AIProvider>(
|
||||
get: {
|
||||
if let providerName = selectedAIProvider,
|
||||
let provider = AIProvider(rawValue: providerName) {
|
||||
return provider
|
||||
}
|
||||
return aiService.selectedProvider
|
||||
},
|
||||
set: { newValue in
|
||||
selectedAIProvider = newValue.rawValue
|
||||
aiService.selectedProvider = newValue
|
||||
selectedAIModel = nil
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
if isAIEnhancementEnabled {
|
||||
|
||||
HStack {
|
||||
Text("AI Provider")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if aiService.connectedProviders.isEmpty {
|
||||
Text("No providers connected")
|
||||
.foregroundColor(.secondary)
|
||||
.italic()
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
} else {
|
||||
Picker("", selection: providerBinding) {
|
||||
ForEach(aiService.connectedProviders.filter { $0 != .elevenLabs && $0 != .deepgram }, id: \.self) { provider in
|
||||
Text(provider.rawValue).tag(provider)
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
.onChange(of: selectedAIProvider) { oldValue, newValue in
|
||||
if let provider = newValue.flatMap({ AIProvider(rawValue: $0) }) {
|
||||
selectedAIModel = provider.defaultModel
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
let providerName = selectedAIProvider ?? aiService.selectedProvider.rawValue
|
||||
if let provider = AIProvider(rawValue: providerName),
|
||||
provider != .custom {
|
||||
|
||||
HStack {
|
||||
Text("AI Model")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if aiService.availableModels.isEmpty {
|
||||
Text(provider == .openRouter ? "No models loaded" : "No models available")
|
||||
.foregroundColor(.secondary)
|
||||
.italic()
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
} else {
|
||||
let modelBinding = Binding<String>(
|
||||
get: {
|
||||
if let model = selectedAIModel, !model.isEmpty {
|
||||
return model
|
||||
}
|
||||
return aiService.currentModel
|
||||
},
|
||||
set: { newModelValue in
|
||||
selectedAIModel = newModelValue
|
||||
aiService.selectModel(newModelValue)
|
||||
}
|
||||
)
|
||||
|
||||
let models = provider == .openRouter ? aiService.availableModels : (provider == .ollama ? aiService.availableModels : provider.availableModels)
|
||||
|
||||
Picker("", selection: modelBinding) {
|
||||
ForEach(models, id: \.self) { model in
|
||||
Text(model).tag(model)
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
|
||||
if provider == .openRouter {
|
||||
Button(action: {
|
||||
Task {
|
||||
await aiService.fetchOpenRouterModels()
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.help("Refresh models")
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Enhancement Prompt")
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
PromptSelectionGrid(
|
||||
prompts: enhancementService.allPrompts,
|
||||
selectedPromptId: selectedPromptId,
|
||||
onPromptSelected: { prompt in
|
||||
selectedPromptId = prompt.id
|
||||
},
|
||||
onEditPrompt: { prompt in
|
||||
selectedPromptForEdit = prompt
|
||||
},
|
||||
onDeletePrompt: { prompt in
|
||||
enhancementService.deletePrompt(prompt)
|
||||
},
|
||||
onAddNewPrompt: {
|
||||
isEditingPrompt = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
Toggle("Context Awareness", isOn: $useScreenCapture)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(CardBackground(isSelected: false))
|
||||
.padding(.horizontal)
|
||||
|
||||
VStack(spacing: 16) {
|
||||
SectionHeader(title: "Advanced")
|
||||
|
||||
HStack {
|
||||
Toggle("Auto Send", isOn: $isAutoSendEnabled)
|
||||
|
||||
InfoTip(
|
||||
title: "Auto Send",
|
||||
message: "Automatically presses the Return/Enter key after pasting text. This is useful for chat applications or forms where its not necessary to to make changes to the transcribed text"
|
||||
)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
HStack {
|
||||
Text("Keyboard Shortcut")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
KeyboardShortcuts.Recorder(for: .powerMode(id: powerModeConfigId))
|
||||
.controlSize(.small)
|
||||
|
||||
InfoTip(
|
||||
title: "Power Mode Hotkey",
|
||||
message: "Assign a unique keyboard shortcut to instantly activate this Power Mode and start recording"
|
||||
)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(CardBackground(isSelected: false))
|
||||
.padding(.horizontal)
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: saveConfiguration) {
|
||||
Text(mode.isAdding ? "Add New Power Mode" : "Save Changes")
|
||||
.font(.headline)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 8)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(canSave ? Color(red: 0.3, green: 0.7, blue: 0.4) : Color(red: 0.3, green: 0.7, blue: 0.4).opacity(0.5))
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(!canSave)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button(mode.isAdding ? "Add" : "Save") {
|
||||
saveConfiguration()
|
||||
}
|
||||
.padding(.vertical)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.disabled(!canSave)
|
||||
}
|
||||
|
||||
if case .edit = mode {
|
||||
ToolbarItem {
|
||||
Button("Delete", role: .destructive) {
|
||||
isShowingDeleteConfirmation = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.confirmationDialog(
|
||||
"Delete Power Mode?",
|
||||
isPresented: $isShowingDeleteConfirmation,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
if case .edit(let config) = mode {
|
||||
Button("Delete", role: .destructive) {
|
||||
powerModeManager.removeConfiguration(with: config.id)
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
Button("Cancel", role: .cancel) { }
|
||||
} message: {
|
||||
if case .edit(let config) = mode {
|
||||
Text("Are you sure you want to delete the '\(config.name)' power mode? This action cannot be undone.")
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isShowingAppPicker) {
|
||||
@ -680,15 +501,7 @@ struct ConfigurationView: View {
|
||||
onDismiss: { isShowingAppPicker = false }
|
||||
)
|
||||
}
|
||||
.sheet(isPresented: $isEditingPrompt) {
|
||||
PromptEditorView(mode: .add)
|
||||
}
|
||||
.sheet(item: $selectedPromptForEdit) { prompt in
|
||||
PromptEditorView(mode: .edit(prompt))
|
||||
}
|
||||
.powerModeValidationAlert(errors: validationErrors, isPresented: $showValidationAlert)
|
||||
.navigationTitle("") // Explicitly set an empty title for this view
|
||||
.toolbar(.hidden) // Attempt to hide the navigation bar area
|
||||
.onAppear {
|
||||
// Set AI provider and model for new power modes after environment objects are available
|
||||
if case .add = mode {
|
||||
@ -704,6 +517,11 @@ struct ConfigurationView: View {
|
||||
if isAIEnhancementEnabled && selectedPromptId == nil {
|
||||
selectedPromptId = enhancementService.allPrompts.first?.id
|
||||
}
|
||||
|
||||
// Focus the name field for faster keyboard-driven setup
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
isNameFieldFocused = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user