Skip to content
Merged
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
8 changes: 8 additions & 0 deletions Murmur.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
A100091A /* FloatingPanelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A100090A /* FloatingPanelController.swift */; };
A100091B /* AuroraRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A100090B /* AuroraRecordingView.swift */; };
A100091C /* AuroraIdleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A100090C /* AuroraIdleView.swift */; };
A100091D /* AuroraProcessingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A100090D /* AuroraProcessingView.swift */; };
A100091E /* AuroraSuccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A100090E /* AuroraSuccessView.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand Down Expand Up @@ -109,6 +111,8 @@
A100090A /* FloatingPanelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingPanelController.swift; sourceTree = "<group>"; };
A100090B /* AuroraRecordingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuroraRecordingView.swift; sourceTree = "<group>"; };
A100090C /* AuroraIdleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuroraIdleView.swift; sourceTree = "<group>"; };
A100090D /* AuroraProcessingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuroraProcessingView.swift; sourceTree = "<group>"; };
A100090E /* AuroraSuccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuroraSuccessView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -212,6 +216,8 @@
A1000908 /* ReviewTrayView.swift */,
A100090B /* AuroraRecordingView.swift */,
A100090C /* AuroraIdleView.swift */,
A100090D /* AuroraProcessingView.swift */,
A100090E /* AuroraSuccessView.swift */,
);
path = Components;
sourceTree = "<group>";
Expand Down Expand Up @@ -399,6 +405,8 @@
A100091A /* FloatingPanelController.swift in Sources */,
A100091B /* AuroraRecordingView.swift in Sources */,
A100091C /* AuroraIdleView.swift in Sources */,
A100091D /* AuroraProcessingView.swift in Sources */,
A100091E /* AuroraSuccessView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
78 changes: 41 additions & 37 deletions Murmur/Core/TranscriptionTaskManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,57 +3,61 @@ import UserNotifications

// MARK: - Display Status for UI (Goal-Gradient Effect)
// Users are more motivated when they can see visible progress
// Simplified to 4 user-focused phases for cognitive clarity

enum DisplayStatus: Equatable {
case idle
case preparing // 0-10%: Loading audio files
case transcribing(progress: Double) // 10-70%: Active transcription
case extractingActionItems // 70-90%: AI processing
case saving // 90-95%: Writing to disk
case transcriptSaved // Complete but no action items
case pendingReview(itemCount: Int) // Waiting for user to review action items
case completed(taskCount: Int) // Complete with action items
case failed(message: String) // Error state

// Processing phases (4 user-focused steps)
case gettingReady // 0-15%: Loading audio, initial setup
case transcribing(progress: Double) // 15-75%: Active transcription
case findingActionItems // 75-95%: AI analysis
case finishing // 95-100%: Saving, final steps

// Completion states
case transcriptSaved // Complete but no action items
case pendingReview(itemCount: Int) // Waiting for user to review action items
case completed(taskCount: Int) // Complete with action items
case failed(message: String) // Error state

/// Computed progress value (0.0 to 1.0) for UI progress bar
var progress: Double {
switch self {
case .idle:
return 0.0
case .preparing:
case .gettingReady:
return 0.10
case .transcribing(let p):
// Map transcription progress (0-1) to (0.10-0.70)
return 0.10 + (p * 0.60)
case .extractingActionItems:
return 0.80
case .saving:
return 0.95
// Map transcription progress (0-1) to (0.15-0.75)
return 0.15 + (p * 0.60)
case .findingActionItems:
return 0.85
case .finishing:
return 0.97
case .transcriptSaved, .pendingReview, .completed:
return 1.0
case .failed:
return 0.0
}
}

/// User-friendly status text
/// User-friendly status text (outcome-oriented, not technical)
var statusText: String {
switch self {
case .idle:
return "Ready"
case .preparing:
return "Preparing..."
case .transcribing(let p):
let pct = Int(p * 100)
return "Transcribing (\(pct)%)"
case .extractingActionItems:
return "Extracting tasks..."
case .saving:
return "Saving..."
case .gettingReady:
return "Getting ready..."
case .transcribing:
return "Transcribing..."
case .findingActionItems:
return "Finding action items..."
case .finishing:
return "Almost done..."
case .transcriptSaved:
return "Saved"
case .pendingReview(let count):
return "\(count) task\(count == 1 ? "" : "s") to review"
return "\(count) item\(count == 1 ? "" : "s") to review"
case .completed(let count):
return "\(count) task\(count == 1 ? "" : "s") added"
case .failed(let message):
Expand All @@ -66,7 +70,7 @@ enum DisplayStatus: Equatable {
switch self {
case .idle:
return "circle"
case .preparing, .transcribing, .extractingActionItems, .saving:
case .gettingReady, .transcribing, .findingActionItems, .finishing:
return "arrow.triangle.2.circlepath"
case .transcriptSaved:
return "checkmark.circle.fill"
Expand All @@ -79,10 +83,10 @@ enum DisplayStatus: Equatable {
}
}

/// Whether this is a "processing" state (show progress bar)
/// Whether this is a "processing" state (show progress indicator)
var isProcessing: Bool {
switch self {
case .preparing, .transcribing, .extractingActionItems, .saving:
case .gettingReady, .transcribing, .findingActionItems, .finishing:
return true
default:
return false
Expand Down Expand Up @@ -147,10 +151,10 @@ class TranscriptionTaskManager: ObservableObject {
func startTranscription(micURL: URL, systemURL: URL?, outputFolder: URL) {
let task = TranscriptionTask(micURL: micURL, systemURL: systemURL, outputFolder: outputFolder)

// Increment active count and set preparing status immediately (Goal-Gradient Effect)
// Increment active count and set initial status immediately (Goal-Gradient Effect)
DispatchQueue.main.async {
self.activeCount += 1
self.displayStatus = .preparing
self.displayStatus = .gettingReady
}

print("馃摑 Starting transcription task \(task.id) (active: \(activeCount))")
Expand Down Expand Up @@ -268,7 +272,7 @@ class TranscriptionTaskManager: ObservableObject {

// Phase 2: Save transcript based on provider
await MainActor.run {
self.displayStatus = .saving
self.displayStatus = .finishing
}

let transcriptURL: URL?
Expand Down Expand Up @@ -336,7 +340,7 @@ class TranscriptionTaskManager: ObservableObject {

if !geminiApiKey.isEmpty && !transcriptionResult.allSpeakerIds.isEmpty {
await MainActor.run {
self.displayStatus = .extractingActionItems // Reuse status for "AI processing"
self.displayStatus = .findingActionItems // AI processing phase
}

print("馃搵 Phase 2: Identifying \(transcriptionResult.allSpeakerIds.count) speakers with Gemini...")
Expand Down Expand Up @@ -372,7 +376,7 @@ class TranscriptionTaskManager: ObservableObject {

// Phase 3: Save transcript WITH speaker names
await MainActor.run {
self.displayStatus = .saving
self.displayStatus = .finishing
}

guard let transcriptURL = TranscriptSaver.saveRichAssemblyAITranscript(
Expand Down Expand Up @@ -415,7 +419,7 @@ class TranscriptionTaskManager: ObservableObject {
// Increment retry count
await MainActor.run {
failedTranscriptionManager.incrementRetryCount(id: failedId)
self.displayStatus = .preparing
self.displayStatus = .gettingReady
}

// Get output folder (same as original)
Expand Down Expand Up @@ -598,9 +602,9 @@ class TranscriptionTaskManager: ObservableObject {
}
}

// Update status to show we're extracting action items
// Update status to show we're finding action items
await MainActor.run {
self.displayStatus = .extractingActionItems
self.displayStatus = .findingActionItems
}

do {
Expand Down
3 changes: 3 additions & 0 deletions Murmur/TranscriptedApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
var onboardingWindowController: OnboardingWindowController?

func applicationDidFinishLaunching(_ notification: Notification) {
// Configure tooltip delay to 1 second
UserDefaults.standard.set(1000, forKey: "NSInitialToolTipDelay")

NSApp.setActivationPolicy(.accessory)

// Check if onboarding is needed
Expand Down
2 changes: 2 additions & 0 deletions Murmur/UI/FloatingPanel/Components/AuroraIdleView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ struct AuroraIdleView: View {
.scaleEffect(isRecordHovered ? 1.1 : 1.0)
}
.buttonStyle(PlainButtonStyle())
.floatingTooltip("Record")
.onHover { hovering in
withAnimation(.spring(response: 0.15, dampingFraction: 0.8)) {
isRecordHovered = hovering
Expand Down Expand Up @@ -126,6 +127,7 @@ struct AuroraIdleView: View {
.scaleEffect(isFilesHovered ? 1.1 : 1.0)
}
.buttonStyle(PlainButtonStyle())
.floatingTooltip("Transcripts")
.onHover { hovering in
withAnimation(.spring(response: 0.15, dampingFraction: 0.8)) {
isFilesHovered = hovering
Expand Down
Loading