Skip to content

Commit 04cdbc4

Browse files
committed
Flag AI post-processing fallbacks in transcription history
Builds on altic-dev#279. After that PR a failed AI cleanup silently falls back to the raw transcription with only a log line — a broken API key can go unnoticed. This PR adds a durable visible signal. Added `aiProcessingError: String?` to TranscriptionHistoryEntry (Codable via decodeIfPresent — fully back-compat with existing persisted history, old entries decode with nil). Threaded the fallback reason into addEntry at the two dictation callsites. UI: - History list row shows an orange ⚠ triangle next to entries with a set aiProcessingError; hover reveals the error message. - History detail view shows an orange callout card at the top with the full error message when the entry has one. I also tried surfacing the failure as a transient message in the notch overlay, but it was overridden by the post-typing overlay cleanup (the NotchOverlayManager hide() that fires immediately after typing completes), so that attempt is left as a follow-up.
1 parent 53fbbfc commit 04cdbc4

3 files changed

Lines changed: 69 additions & 5 deletions

File tree

Sources/Fluid/ContentView.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1813,6 +1813,7 @@ struct ContentView: View {
18131813
}
18141814

18151815
var finalText: String
1816+
var aiFallbackReason: String?
18161817

18171818
let shouldUseAI = activeDictationSlot.map { DictationAIPostProcessingGate.isConfigured(for: $0) } ??
18181819
DictationAIPostProcessingGate.isConfigured()
@@ -1843,6 +1844,7 @@ struct ContentView: View {
18431844
"AI post-processing failed, falling back to raw transcription: \(error.localizedDescription)",
18441845
source: "ContentView"
18451846
)
1847+
aiFallbackReason = error.localizedDescription
18461848
finalText = transcribedText
18471849
}
18481850
let postProcessingLatencyMs = Int((Date().timeIntervalSince(postProcessingStart) * 1000).rounded())
@@ -1908,7 +1910,8 @@ struct ContentView: View {
19081910
rawText: transcribedText,
19091911
processedText: finalText,
19101912
appName: appInfo.name,
1911-
windowTitle: appInfo.windowTitle
1913+
windowTitle: appInfo.windowTitle,
1914+
aiProcessingError: aiFallbackReason
19121915
)
19131916
}
19141917

@@ -2118,6 +2121,7 @@ struct ContentView: View {
21182121
await Task.yield()
21192122

21202123
var finalText = transcribedText
2124+
var aiFallbackReason: String?
21212125
let shouldUseAI = DictationAIPostProcessingGate.isConfigured()
21222126
if shouldUseAI {
21232127
do {
@@ -2127,6 +2131,7 @@ struct ContentView: View {
21272131
"AI reprocess failed, falling back to raw transcription: \(error.localizedDescription)",
21282132
source: "ContentView"
21292133
)
2134+
aiFallbackReason = error.localizedDescription
21302135
finalText = transcribedText
21312136
}
21322137
}
@@ -2142,7 +2147,8 @@ struct ContentView: View {
21422147
rawText: transcribedText,
21432148
processedText: finalText,
21442149
appName: appInfo.name,
2145-
windowTitle: appInfo.windowTitle
2150+
windowTitle: appInfo.windowTitle,
2151+
aiProcessingError: aiFallbackReason
21462152
)
21472153
}
21482154

Sources/Fluid/Persistence/TranscriptionHistoryStore.swift

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,19 @@ struct TranscriptionHistoryEntry: Codable, Identifiable, Equatable {
1919
let windowTitle: String
2020
let characterCount: Int
2121
let wasAIProcessed: Bool
22+
/// Non-nil when AI post-processing was configured but failed and we fell
23+
/// back to typing the raw transcription. The string carries the error
24+
/// message for display / debugging.
25+
let aiProcessingError: String?
2226

2327
init(
2428
id: UUID = UUID(),
2529
timestamp: Date = Date(),
2630
rawText: String,
2731
processedText: String,
2832
appName: String,
29-
windowTitle: String
33+
windowTitle: String,
34+
aiProcessingError: String? = nil
3035
) {
3136
self.id = id
3237
self.timestamp = timestamp
@@ -36,6 +41,25 @@ struct TranscriptionHistoryEntry: Codable, Identifiable, Equatable {
3641
self.windowTitle = windowTitle
3742
self.characterCount = processedText.count
3843
self.wasAIProcessed = rawText != processedText
44+
self.aiProcessingError = aiProcessingError
45+
}
46+
47+
init(from decoder: Decoder) throws {
48+
let container = try decoder.container(keyedBy: CodingKeys.self)
49+
self.id = try container.decode(UUID.self, forKey: .id)
50+
self.timestamp = try container.decode(Date.self, forKey: .timestamp)
51+
self.rawText = try container.decode(String.self, forKey: .rawText)
52+
self.processedText = try container.decode(String.self, forKey: .processedText)
53+
self.appName = try container.decode(String.self, forKey: .appName)
54+
self.windowTitle = try container.decode(String.self, forKey: .windowTitle)
55+
self.characterCount = try container.decode(Int.self, forKey: .characterCount)
56+
self.wasAIProcessed = try container.decode(Bool.self, forKey: .wasAIProcessed)
57+
self.aiProcessingError = try container.decodeIfPresent(String.self, forKey: .aiProcessingError)
58+
}
59+
60+
private enum CodingKeys: String, CodingKey {
61+
case id, timestamp, rawText, processedText, appName, windowTitle
62+
case characterCount, wasAIProcessed, aiProcessingError
3963
}
4064

4165
/// Preview text for list display (first 80 chars)
@@ -95,7 +119,8 @@ final class TranscriptionHistoryStore: ObservableObject {
95119
rawText: String,
96120
processedText: String,
97121
appName: String,
98-
windowTitle: String
122+
windowTitle: String,
123+
aiProcessingError: String? = nil
99124
) {
100125
// Skip empty transcriptions
101126
guard !processedText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
@@ -104,7 +129,8 @@ final class TranscriptionHistoryStore: ObservableObject {
104129
rawText: rawText,
105130
processedText: processedText,
106131
appName: appName,
107-
windowTitle: windowTitle
132+
windowTitle: windowTitle,
133+
aiProcessingError: aiProcessingError
108134
)
109135

110136
// Insert at beginning (newest first)

Sources/Fluid/UI/TranscriptionHistoryView.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,13 @@ struct TranscriptionHistoryView: View {
143143
)
144144
}
145145

146+
if entry.aiProcessingError != nil {
147+
Image(systemName: "exclamationmark.triangle.fill")
148+
.font(.system(size: 10, weight: .bold))
149+
.foregroundStyle(isSelected ? .white : Color.orange)
150+
.help(entry.aiProcessingError ?? "")
151+
}
152+
146153
Spacer()
147154

148155
Text(entry.relativeTimeString)
@@ -294,6 +301,31 @@ struct TranscriptionHistoryView: View {
294301
Divider()
295302
.opacity(0.3)
296303

304+
if let aiError = entry.aiProcessingError {
305+
HStack(alignment: .top, spacing: 8) {
306+
Image(systemName: "exclamationmark.triangle.fill")
307+
.foregroundStyle(Color.orange)
308+
VStack(alignment: .leading, spacing: 2) {
309+
Text("AI cleanup failed — raw transcription was typed instead")
310+
.font(.system(size: 12, weight: .semibold))
311+
Text(aiError)
312+
.font(.system(size: 11))
313+
.foregroundStyle(.secondary)
314+
.textSelection(.enabled)
315+
}
316+
}
317+
.padding(10)
318+
.frame(maxWidth: .infinity, alignment: .leading)
319+
.background(
320+
RoundedRectangle(cornerRadius: 6, style: .continuous)
321+
.fill(Color.orange.opacity(0.08))
322+
.overlay(
323+
RoundedRectangle(cornerRadius: 6, style: .continuous)
324+
.stroke(Color.orange.opacity(0.3), lineWidth: 1)
325+
)
326+
)
327+
}
328+
297329
// Final Text Section
298330
self.detailSection(
299331
title: "Final Text",

0 commit comments

Comments
 (0)