Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
308 changes: 203 additions & 105 deletions Sources/Fluid/ContentView.swift

Large diffs are not rendered by default.

108 changes: 90 additions & 18 deletions Sources/Fluid/Persistence/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,13 @@ final class SettingsStore: ObservableObject {
case write // legacy persisted value (decoded as .edit)
case rewrite // legacy persisted value (decoded as .edit)

var id: String { self.rawValue }
var id: String {
self.rawValue
}

static var visiblePromptModes: [PromptMode] { [.dictate, .edit] }
static var visiblePromptModes: [PromptMode] {
[.dictate, .edit]
}

var normalized: PromptMode {
switch self {
Expand Down Expand Up @@ -723,12 +727,11 @@ final class SettingsStore: ObservableObject {
}
}

let fallback = self.defaultPromptResolution(
return self.defaultPromptResolution(
for: normalizedMode,
source: .appBindingDefault,
appBinding: binding
)
return fallback
}

if let profile = self.selectedPromptProfile(for: normalizedMode) {
Expand Down Expand Up @@ -1234,7 +1237,9 @@ final class SettingsStore: ObservableObject {
case purple = "Purple"
case orange = "Orange"

var id: String { self.rawValue }
var id: String {
self.rawValue
}

var hex: String {
switch self {
Expand All @@ -1254,7 +1259,9 @@ final class SettingsStore: ObservableObject {
case fluidSfx3 = "fluid_sfx_3"
case fluidSfx4 = "fluid_sfx_4"

var id: String { self.rawValue }
var id: String {
self.rawValue
}

var displayName: String {
switch self {
Expand Down Expand Up @@ -1591,6 +1598,52 @@ final class SettingsStore: ObservableObject {
}
}

// MARK: - Prompt Mode Settings (Transcribe with Prompt)

var promptModeShortcutEnabled: Bool {
get {
let value = self.defaults.object(forKey: Keys.promptModeShortcutEnabled)
return value as? Bool ?? false
}
set {
objectWillChange.send()
self.defaults.set(newValue, forKey: Keys.promptModeShortcutEnabled)
}
}

var promptModeHotkeyShortcut: HotkeyShortcut {
get {
if let data = defaults.data(forKey: Keys.promptModeHotkeyShortcut),
let shortcut = try? JSONDecoder().decode(HotkeyShortcut.self, from: data)
{
return shortcut
}
// Default to Right Shift key (keyCode: 60, no modifiers) — avoids conflict with Command Mode (Right Command, keyCode 54)
return HotkeyShortcut(keyCode: 60, modifierFlags: [])
}
set {
objectWillChange.send()
if let data = try? JSONEncoder().encode(newValue) {
self.defaults.set(data, forKey: Keys.promptModeHotkeyShortcut)
}
}
}

var promptModeSelectedPromptID: String? {
get {
let value = self.defaults.string(forKey: Keys.promptModeSelectedPromptID)
return value?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == true ? nil : value
}
set {
objectWillChange.send()
if let id = newValue?.trimmingCharacters(in: .whitespacesAndNewlines), !id.isEmpty {
self.defaults.set(id, forKey: Keys.promptModeSelectedPromptID)
} else {
self.defaults.removeObject(forKey: Keys.promptModeSelectedPromptID)
}
}
}

var commandModeShortcutEnabled: Bool {
get {
let value = self.defaults.object(forKey: Keys.commandModeShortcutEnabled)
Expand Down Expand Up @@ -1986,6 +2039,12 @@ final class SettingsStore: ObservableObject {
self.selectedEditPromptID = nil
}

if let id = self.promptModeSelectedPromptID,
self.dictationPromptProfiles.contains(where: { $0.id == id && $0.mode.normalized == .dictate }) == false
{
self.promptModeSelectedPromptID = nil
}

let validPromptIDsByMode: [PromptMode: Set<String>] = [
.dictate: Set(self.dictationPromptProfiles.filter { $0.mode.normalized == .dictate }.map(\.id)),
.edit: Set(self.dictationPromptProfiles.filter { $0.mode.normalized == .edit }.map(\.id)),
Expand Down Expand Up @@ -2323,8 +2382,8 @@ final class SettingsStore: ObservableObject {
/// Unified speech recognition model selection.
/// Replaces the old TranscriptionProviderOption + WhisperModelSize dual-setting.
enum SpeechModel: String, CaseIterable, Identifiable, Codable {
// Temporarily disabled in UI/runtime while Parakeet word boosting work is prioritized.
// Flip to `true` in a future round to re-enable Qwen without deleting implementation.
/// Temporarily disabled in UI/runtime while Parakeet word boosting work is prioritized.
/// Flip to `true` in a future round to re-enable Qwen without deleting implementation.
static let qwenPreviewEnabled = false

// MARK: - FluidAudio Models (Apple Silicon Only)
Expand All @@ -2347,7 +2406,9 @@ final class SettingsStore: ObservableObject {
case whisperLargeTurbo = "whisper-large-turbo" // temporarily disabled in UI
case whisperLarge = "whisper-large"

var id: String { rawValue }
var id: String {
rawValue
}

// MARK: - Display Properties

Expand Down Expand Up @@ -2789,7 +2850,9 @@ final class SettingsStore: ObservableObject {
case fluidAudio
case whisper

var id: String { rawValue }
var id: String {
rawValue
}

var displayName: String {
switch self {
Expand Down Expand Up @@ -2844,7 +2907,7 @@ final class SettingsStore: ObservableObject {
// swiftlint:enable type_body_length

private extension SettingsStore {
// Keys
/// Keys
enum Keys {
static let enableAIProcessing = "EnableAIProcessing"
static let enableDebugLogs = "EnableDebugLogs"
Expand Down Expand Up @@ -2895,6 +2958,11 @@ private extension SettingsStore {
static let commandModeLinkedToGlobal = "CommandModeLinkedToGlobal"
static let commandModeShortcutEnabled = "CommandModeShortcutEnabled"

// Prompt Mode Keys (Transcribe with Prompt)
static let promptModeHotkeyShortcut = "PromptModeHotkeyShortcut"
static let promptModeShortcutEnabled = "PromptModeShortcutEnabled"
static let promptModeSelectedPromptID = "PromptModeSelectedPromptID"

// Rewrite Mode Keys
static let rewriteModeHotkeyShortcut = "RewriteModeHotkeyShortcut"
static let rewriteModeSelectedModel = "RewriteModeSelectedModel"
Expand All @@ -2914,7 +2982,7 @@ private extension SettingsStore {
static let fillerWords = "FillerWords"
static let removeFillerWordsEnabled = "RemoveFillerWordsEnabled"

// GAAV Mode (removes capitalization and trailing punctuation)
/// GAAV Mode (removes capitalization and trailing punctuation)
static let gaavModeEnabled = "GAAVModeEnabled"

// Custom Dictionary
Expand All @@ -2925,7 +2993,7 @@ private extension SettingsStore {
static let selectedTranscriptionProvider = "SelectedTranscriptionProvider"
static let whisperModelSize = "WhisperModelSize"

// Unified Speech Model (replaces above two)
/// Unified Speech Model (replaces above two)
static let selectedSpeechModel = "SelectedSpeechModel"

// Overlay Position
Expand All @@ -2935,10 +3003,10 @@ private extension SettingsStore {
static let overlaySize = "OverlaySize"
static let transcriptionPreviewCharLimit = "TranscriptionPreviewCharLimit"

// Media Playback Control
/// Media Playback Control
static let pauseMediaDuringTranscription = "PauseMediaDuringTranscription"

// Custom Dictation Prompt
/// Custom Dictation Prompt
static let customDictationPrompt = "CustomDictationPrompt"

// Dictation Prompt Profiles (multi-prompt system)
Expand All @@ -2958,7 +3026,7 @@ private extension SettingsStore {
static let defaultWritePromptOverride = "DefaultWritePromptOverride" // legacy fallback key
static let defaultRewritePromptOverride = "DefaultRewritePromptOverride" // legacy fallback key

// Streak Settings
/// Streak Settings
static let weekendsDontBreakStreak = "WeekendsDontBreakStreak"
}
}
Expand All @@ -2968,7 +3036,9 @@ extension SettingsStore {
case standard
case reliablePaste

var id: String { self.rawValue }
var id: String {
self.rawValue
}

var displayName: String {
switch self {
Expand Down Expand Up @@ -3025,7 +3095,9 @@ extension SettingsStore {
case medium = "ggml-medium.bin"
case large = "ggml-large-v3.bin"

var id: String { rawValue }
var id: String {
rawValue
}

var displayName: String {
switch self {
Expand Down
17 changes: 16 additions & 1 deletion Sources/Fluid/Services/DictationAIPostProcessingGate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ enum DictationAIPostProcessingGate {
/// - For other providers: requires a local endpoint OR a non-empty API key
static func isConfigured() -> Bool {
let settings = SettingsStore.shared
guard settings.enableAIProcessing else { return false }
let hasCustomPrompt = settings.selectedPromptID(for: .dictate) != nil
guard settings.enableAIProcessing || hasCustomPrompt else { return false }

let providerID = settings.selectedProviderID
if providerID == "apple-intelligence" {
Expand All @@ -24,6 +25,20 @@ enum DictationAIPostProcessingGate {
return !apiKey.isEmpty
}

/// Returns true if the selected AI provider is reachable/configured (API key or local endpoint),
/// regardless of the AI toggle or prompt selection. Used to gate prompt-mode hotkey AI processing.
static func isProviderConfigured() -> Bool {
let settings = SettingsStore.shared
let providerID = settings.selectedProviderID
if providerID == "apple-intelligence" {
return AppleIntelligenceService.isAvailable
}
let baseURL = self.baseURL(for: providerID, settings: settings)
if self.isLocalEndpoint(baseURL) { return true }
let apiKey = (settings.getAPIKey(for: providerID) ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
return !apiKey.isEmpty
}

static func baseURL(for providerID: String, settings: SettingsStore) -> String {
if let saved = settings.savedProviders.first(where: { $0.id == providerID }) {
return saved.baseURL.trimmingCharacters(in: .whitespacesAndNewlines)
Expand Down
Loading
Loading