Merge pull request #465 from Beingpax/better-powermode-ui
Better powermode UI
This commit is contained in:
commit
f712652278
@ -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,371 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
Section("Trigger Scenarios") {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack {
|
||||
Text("Applications")
|
||||
Spacer()
|
||||
AddIconButton(helpText: "Add application") {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Button("Cancel") {
|
||||
.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() }
|
||||
|
||||
AddIconButton(helpText: "Add website", isDisabled: newWebsiteURL.isEmpty) {
|
||||
addWebsite()
|
||||
}
|
||||
}
|
||||
|
||||
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: $isDefault) {
|
||||
HStack(spacing: 6) {
|
||||
Text("Set as default")
|
||||
InfoTip(
|
||||
title: "Default Power Mode",
|
||||
message: "Default power mode is used when no specific app or website matches are found"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color(NSColor.controlBackgroundColor))
|
||||
.navigationTitle(mode.title)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button("Save") {
|
||||
saveConfiguration()
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.disabled(!canSave)
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.regular)
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
|
||||
if case .edit = mode {
|
||||
ToolbarItem {
|
||||
Button("Delete", role: .destructive) {
|
||||
isShowingDeleteConfirmation = true
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.regular)
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
.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()
|
||||
}
|
||||
.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)
|
||||
}
|
||||
.padding(.vertical)
|
||||
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 +499,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 +515,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
extension View {
|
||||
func placeholder<Content: View>(
|
||||
@ -69,6 +70,7 @@ struct PowerModeView: View {
|
||||
var body: some View {
|
||||
NavigationStack(path: $navigationPath) {
|
||||
VStack(spacing: 0) {
|
||||
// Header Section
|
||||
VStack(spacing: 12) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
@ -80,7 +82,7 @@ struct PowerModeView: View {
|
||||
InfoTip(
|
||||
title: "What is Power Mode?",
|
||||
message: "Automatically apply custom configurations based on the app/website you are using",
|
||||
learnMoreURL: "https://www.youtube.com/@tryvoiceink/videos"
|
||||
learnMoreURL: "https://tryvoiceink.com/docs/power-mode"
|
||||
)
|
||||
}
|
||||
|
||||
@ -135,120 +137,122 @@ struct PowerModeView: View {
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.top, 20)
|
||||
.padding(.bottom, 16)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color(NSColor.windowBackgroundColor))
|
||||
|
||||
Rectangle()
|
||||
.fill(Color(NSColor.separatorColor))
|
||||
.frame(height: 1)
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
if isReorderMode {
|
||||
VStack(spacing: 12) {
|
||||
List {
|
||||
ForEach(powerModeManager.configurations) { config in
|
||||
HStack(spacing: 12) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color(NSColor.controlBackgroundColor))
|
||||
.frame(width: 40, height: 40)
|
||||
Text(config.emoji)
|
||||
.font(.system(size: 20))
|
||||
}
|
||||
|
||||
Text(config.name)
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 6) {
|
||||
if config.isDefault {
|
||||
Text("Default")
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Capsule().fill(Color.accentColor))
|
||||
.foregroundColor(.white)
|
||||
// Content Section
|
||||
Group {
|
||||
if isReorderMode {
|
||||
VStack(spacing: 12) {
|
||||
List {
|
||||
ForEach(powerModeManager.configurations) { config in
|
||||
HStack(spacing: 12) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color(NSColor.controlBackgroundColor))
|
||||
.frame(width: 40, height: 40)
|
||||
Text(config.emoji)
|
||||
.font(.system(size: 20))
|
||||
}
|
||||
if !config.isEnabled {
|
||||
Text("Disabled")
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(Capsule().fill(Color(NSColor.controlBackgroundColor)))
|
||||
.overlay(
|
||||
Capsule().stroke(Color(NSColor.separatorColor), lineWidth: 0.5)
|
||||
)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 14)
|
||||
.background(CardBackground(isSelected: false))
|
||||
.listRowInsets(EdgeInsets())
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
.onMove(perform: powerModeManager.moveConfigurations)
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.listRowSeparator(.hidden)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color(NSColor.controlBackgroundColor))
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.vertical, 20)
|
||||
} else {
|
||||
GeometryReader { geometry in
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
if powerModeManager.configurations.isEmpty {
|
||||
VStack(spacing: 24) {
|
||||
|
||||
Text(config.name)
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
|
||||
Spacer()
|
||||
.frame(height: geometry.size.height * 0.2)
|
||||
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "square.grid.2x2.fill")
|
||||
.font(.system(size: 48, weight: .regular))
|
||||
.foregroundColor(.secondary.opacity(0.6))
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text("No Power Modes Yet")
|
||||
.font(.system(size: 20, weight: .medium))
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text("Create first power mode to automate your VoiceInk workflow based on apps/website you are using")
|
||||
.font(.system(size: 14))
|
||||
|
||||
HStack(spacing: 6) {
|
||||
if config.isDefault {
|
||||
Text("Default")
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Capsule().fill(Color.accentColor))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
if !config.isEnabled {
|
||||
Text("Disabled")
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(Capsule().fill(Color(NSColor.controlBackgroundColor)))
|
||||
.overlay(
|
||||
Capsule().stroke(Color(NSColor.separatorColor), lineWidth: 0.5)
|
||||
)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineSpacing(2)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(minHeight: geometry.size.height)
|
||||
} else {
|
||||
VStack(spacing: 0) {
|
||||
PowerModeConfigurationsGrid(
|
||||
powerModeManager: powerModeManager,
|
||||
onEditConfig: { config in
|
||||
configurationMode = .edit(config)
|
||||
navigationPath.append(configurationMode!)
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 14)
|
||||
.background(CardBackground(isSelected: false))
|
||||
.listRowInsets(EdgeInsets())
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
.onMove(perform: powerModeManager.moveConfigurations)
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.listRowSeparator(.hidden)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color(NSColor.controlBackgroundColor))
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 20)
|
||||
} else {
|
||||
GeometryReader { geometry in
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
if powerModeManager.configurations.isEmpty {
|
||||
VStack(spacing: 24) {
|
||||
Spacer()
|
||||
.frame(height: geometry.size.height * 0.2)
|
||||
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "square.grid.2x2.fill")
|
||||
.font(.system(size: 48, weight: .regular))
|
||||
.foregroundColor(.secondary.opacity(0.6))
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text("No Power Modes Yet")
|
||||
.font(.system(size: 20, weight: .medium))
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text("Create first power mode to automate your VoiceInk workflow based on apps/website you are using")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineSpacing(2)
|
||||
}
|
||||
}
|
||||
)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.vertical, 20)
|
||||
|
||||
Spacer()
|
||||
.frame(height: 40)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(minHeight: geometry.size.height)
|
||||
} else {
|
||||
VStack(spacing: 0) {
|
||||
PowerModeConfigurationsGrid(
|
||||
powerModeManager: powerModeManager,
|
||||
onEditConfig: { config in
|
||||
configurationMode = .edit(config)
|
||||
navigationPath.append(configurationMode!)
|
||||
}
|
||||
)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.vertical, 20)
|
||||
|
||||
Spacer()
|
||||
.frame(height: 40)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color(NSColor.controlBackgroundColor))
|
||||
}
|
||||
.background(Color(NSColor.controlBackgroundColor))
|
||||
.navigationDestination(for: ConfigurationMode.self) { mode in
|
||||
@ -259,11 +263,9 @@ struct PowerModeView: View {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// New component for section headers
|
||||
struct SectionHeader: View {
|
||||
let title: String
|
||||
|
||||
|
||||
var body: some View {
|
||||
Text(title)
|
||||
.font(.system(size: 16, weight: .bold))
|
||||
|
||||
@ -65,7 +65,26 @@ struct PowerModeConfigurationsGrid: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
|
||||
/// Small, consistent icon-only add button used across Power Mode configuration rows.
|
||||
struct AddIconButton: View {
|
||||
let helpText: String
|
||||
var isDisabled: Bool = false
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.system(size: 18))
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.help(helpText)
|
||||
.accessibilityLabel(helpText)
|
||||
.disabled(isDisabled)
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,7 +185,7 @@ struct ConfigurationRow: View {
|
||||
.font(.caption2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if websiteCount > 0 {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "globe")
|
||||
@ -176,6 +195,7 @@ struct ConfigurationRow: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top, 2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
@ -193,7 +213,6 @@ struct ConfigurationRow: View {
|
||||
|
||||
if selectedModel != nil || selectedLanguage != nil || config.isAIEnhancementEnabled || config.isAutoSendEnabled {
|
||||
Divider()
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
if let model = selectedModel, model != "Default" {
|
||||
@ -203,8 +222,8 @@ struct ConfigurationRow: View {
|
||||
Text(model)
|
||||
.font(.caption)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Capsule()
|
||||
.fill(Color(NSColor.controlBackgroundColor)))
|
||||
.overlay(
|
||||
@ -220,8 +239,8 @@ struct ConfigurationRow: View {
|
||||
Text(language)
|
||||
.font(.caption)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Capsule()
|
||||
.fill(Color(NSColor.controlBackgroundColor)))
|
||||
.overlay(
|
||||
@ -237,8 +256,8 @@ struct ConfigurationRow: View {
|
||||
Text(modelName.count > 20 ? String(modelName.prefix(18)) + "..." : modelName)
|
||||
.font(.caption)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Capsule()
|
||||
.fill(Color(NSColor.controlBackgroundColor)))
|
||||
.overlay(
|
||||
@ -254,8 +273,8 @@ struct ConfigurationRow: View {
|
||||
Text("Auto Send")
|
||||
.font(.caption)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Capsule()
|
||||
.fill(Color(NSColor.controlBackgroundColor)))
|
||||
.overlay(
|
||||
@ -271,8 +290,8 @@ struct ConfigurationRow: View {
|
||||
Text("Context Awareness")
|
||||
.font(.caption)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Capsule()
|
||||
.fill(Color(NSColor.controlBackgroundColor)))
|
||||
.overlay(
|
||||
@ -287,8 +306,8 @@ struct ConfigurationRow: View {
|
||||
Text(selectedPrompt?.title ?? "AI")
|
||||
.font(.caption)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Capsule()
|
||||
.fill(Color.accentColor.opacity(0.1)))
|
||||
.foregroundColor(.accentColor)
|
||||
@ -296,10 +315,13 @@ struct ConfigurationRow: View {
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
|
||||
.padding(.vertical, 6)
|
||||
.padding(.horizontal, 16)
|
||||
.background(Color.secondary.opacity(0.1))
|
||||
}
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
.background(CardBackground(isSelected: isEditing))
|
||||
.opacity(config.isEnabled ? 1.0 : 0.5)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user