diff --git a/VoiceInk/Views/Metrics/MetricCard.swift b/VoiceInk/Views/Metrics/MetricCard.swift index f6247cd..cae403f 100644 --- a/VoiceInk/Views/Metrics/MetricCard.swift +++ b/VoiceInk/Views/Metrics/MetricCard.swift @@ -3,15 +3,31 @@ import SwiftUI struct MetricCard: View { let title: String let value: String + let icon: String + let color: Color var body: some View { - VStack(alignment: .leading, spacing: 8) { - Text(title) - .font(.headline) - .foregroundColor(.secondary) - Text(value) - .font(.title) - .fontWeight(.bold) + VStack(alignment: .leading, spacing: 16) { + HStack(spacing: 12) { + // Icon + Image(systemName: icon) + .font(.system(size: 24)) + .foregroundColor(color) + .frame(width: 32, height: 32) + .background( + Circle() + .fill(color.opacity(0.1)) + ) + + VStack(alignment: .leading, spacing: 8) { + Text(title) + .font(.subheadline) + .foregroundColor(.secondary) + Text(value) + .font(.system(size: 24, weight: .bold, design: .rounded)) + .foregroundColor(.primary) + } + } } .frame(maxWidth: .infinity, alignment: .leading) .padding() diff --git a/VoiceInk/Views/Metrics/MetricsContent.swift b/VoiceInk/Views/Metrics/MetricsContent.swift index cbef956..a9632bb 100644 --- a/VoiceInk/Views/Metrics/MetricsContent.swift +++ b/VoiceInk/Views/Metrics/MetricsContent.swift @@ -38,8 +38,30 @@ struct MetricsContent: View { private var metricsGrid: some View { LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 20) { - MetricCard(title: "Words Captured", value: "\(totalWordsTranscribed)") - MetricCard(title: "Voice-to-Text Sessions", value: "\(transcriptions.count)") + MetricCard( + title: "Words Captured", + value: "\(totalWordsTranscribed)", + icon: "text.word.spacing", + color: .blue + ) + MetricCard( + title: "Voice-to-Text Sessions", + value: "\(transcriptions.count)", + icon: "mic.circle.fill", + color: .green + ) + MetricCard( + title: "Average Words/Minute", + value: String(format: "%.1f", averageWordsPerMinute), + icon: "speedometer", + color: .orange + ) + MetricCard( + title: "Words/Session", + value: String(format: "%.1f", averageWordsPerSession), + icon: "chart.bar.fill", + color: .purple + ) } } @@ -117,4 +139,15 @@ struct MetricsContent: View { return dailyData.reversed() } + + // Add computed properties for new metrics + private var averageWordsPerMinute: Double { + guard totalRecordedTime > 0 else { return 0 } + return Double(totalWordsTranscribed) / (totalRecordedTime / 60.0) + } + + private var averageWordsPerSession: Double { + guard !transcriptions.isEmpty else { return 0 } + return Double(totalWordsTranscribed) / Double(transcriptions.count) + } } diff --git a/VoiceInk/Views/Metrics/TimeEfficiencyView.swift b/VoiceInk/Views/Metrics/TimeEfficiencyView.swift index c56d28b..a4fa3de 100644 --- a/VoiceInk/Views/Metrics/TimeEfficiencyView.swift +++ b/VoiceInk/Views/Metrics/TimeEfficiencyView.swift @@ -45,8 +45,9 @@ struct TimeEfficiencyView: View { bottomSection } .padding(.vertical, 24) - .background(backgroundDesign) - .overlay(borderOverlay) + .background(Color(.controlBackgroundColor)) + .cornerRadius(10) + .shadow(radius: 2) } // MARK: - Subviews @@ -154,34 +155,6 @@ struct TimeEfficiencyView: View { ) } } - - // Extension to allow hex color initialization - - // MARK: - Styling Views - - private var backgroundDesign: some View { - RoundedRectangle(cornerRadius: 12) - .fill(Color(nsColor: .controlBackgroundColor)) - } - - private var borderOverlay: some View { - RoundedRectangle(cornerRadius: 12) - .stroke( - .linearGradient( - colors: [ - Color(nsColor: .controlAccentColor).opacity(0.2), - Color.clear, - Color.clear, - Color(nsColor: .controlAccentColor).opacity(0.1) - ], - startPoint: .topLeading, - endPoint: .bottomTrailing - ), - lineWidth: 1 - ) - } - - private var efficiencyGradient: LinearGradient { LinearGradient( diff --git a/VoiceInk/Views/MetricsView.swift b/VoiceInk/Views/MetricsView.swift index 9cd2fce..93315be 100644 --- a/VoiceInk/Views/MetricsView.swift +++ b/VoiceInk/Views/MetricsView.swift @@ -43,7 +43,7 @@ struct MetricsView: View { } } } - .background(Color(.windowBackgroundColor)) + .background(Color(.controlBackgroundColor)) .task { // Ensure the model context is ready hasLoadedData = true diff --git a/VoiceInk/WhisperState.swift b/VoiceInk/WhisperState.swift index 2bb7f0d..5c36da0 100644 --- a/VoiceInk/WhisperState.swift +++ b/VoiceInk/WhisperState.swift @@ -661,22 +661,31 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate { await toggleRecord() } } else { - // Start a parallel task for both UI and recording + // Serialize audio operations to prevent deadlocks Task { - // Play start sound first - SoundManager.shared.playStartSound() - - // Start audio engine immediately - this can happen in parallel - audioEngine.startAudioEngine() - - // Show UI (this is quick now that we removed animations) - await MainActor.run { - showRecorderPanel() // Modified version that doesn't start audio engine - isMiniRecorderVisible = true + do { + // First start the audio engine + await MainActor.run { + audioEngine.startAudioEngine() + } + + // Small delay to ensure audio system is ready + try await Task.sleep(nanoseconds: 50_000_000) // 50ms + + // Now play the sound + SoundManager.shared.playStartSound() + + // Show UI + await MainActor.run { + showRecorderPanel() + isMiniRecorderVisible = true + } + + // Finally start recording + await toggleRecord() + } catch { + logger.error("Error during recorder initialization: \(error)") } - - // Start recording (this will happen in parallel with UI showing) - await toggleRecord() } } }