From 253d385d8ef455549c4d645e67bfbd5c3196ba86 Mon Sep 17 00:00:00 2001 From: Beingpax Date: Sun, 28 Sep 2025 23:16:36 +0545 Subject: [PATCH] Changed the Transcript History/audio transcribe results with tab view --- VoiceInk/Views/AudioTranscribeView.swift | 98 +---- VoiceInk/Views/TranscriptionCard.swift | 369 +++++++++++-------- VoiceInk/Views/TranscriptionResultView.swift | 106 ++++++ 3 files changed, 331 insertions(+), 242 deletions(-) create mode 100644 VoiceInk/Views/TranscriptionResultView.swift diff --git a/VoiceInk/Views/AudioTranscribeView.swift b/VoiceInk/Views/AudioTranscribeView.swift index 3cebfa0..647dca1 100644 --- a/VoiceInk/Views/AudioTranscribeView.swift +++ b/VoiceInk/Views/AudioTranscribeView.swift @@ -30,69 +30,7 @@ struct AudioTranscribeView: View { // Show current transcription result if let transcription = transcriptionManager.currentTranscription { - ScrollView { - VStack(alignment: .leading, spacing: 16) { - Text("Transcription Result") - .font(.headline) - - if let enhancedText = transcription.enhancedText { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("Enhanced") - .font(.subheadline) - .foregroundColor(.secondary) - Spacer() - HStack(spacing: 8) { - AnimatedCopyButton(textToCopy: enhancedText) - AnimatedSaveButton(textToSave: enhancedText) - } - } - Text(enhancedText) - .textSelection(.enabled) - } - - Divider() - - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("Original") - .font(.subheadline) - .foregroundColor(.secondary) - Spacer() - HStack(spacing: 8) { - AnimatedCopyButton(textToCopy: transcription.text) - AnimatedSaveButton(textToSave: transcription.text) - } - } - Text(transcription.text) - .textSelection(.enabled) - } - } else { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("Transcription") - .font(.subheadline) - .foregroundColor(.secondary) - Spacer() - HStack(spacing: 8) { - AnimatedCopyButton(textToCopy: transcription.text) - AnimatedSaveButton(textToSave: transcription.text) - } - } - Text(transcription.text) - .textSelection(.enabled) - } - } - - HStack { - Text("Duration: \(formatDuration(transcription.duration))") - .font(.caption) - .foregroundColor(.secondary) - Spacer() - } - } - .padding() - } + TranscriptionResultView(transcription: transcription) } } } @@ -146,40 +84,6 @@ struct AudioTranscribeView: View { HStack(spacing: 8) { Text("Prompt:") .font(.subheadline) - - Menu { - ForEach(enhancementService.allPrompts) { prompt in - Button { - enhancementService.setActivePrompt(prompt) - selectedPromptId = prompt.id - } label: { - HStack { - Image(systemName: prompt.icon.rawValue) - .foregroundColor(.accentColor) - Text(prompt.title) - if selectedPromptId == prompt.id { - Spacer() - Image(systemName: "checkmark") - } - } - } - } - } label: { - HStack { - Text(enhancementService.allPrompts.first(where: { $0.id == selectedPromptId })?.title ?? "Select Prompt") - .foregroundColor(.primary) - Image(systemName: "chevron.down") - .font(.caption) - } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background( - RoundedRectangle(cornerRadius: 6) - .fill(Color(.controlBackgroundColor)) - ) - } - .fixedSize() - .disabled(!isEnhancementEnabled) } } } diff --git a/VoiceInk/Views/TranscriptionCard.swift b/VoiceInk/Views/TranscriptionCard.swift index 5117b1c..e4a1b2c 100644 --- a/VoiceInk/Views/TranscriptionCard.swift +++ b/VoiceInk/Views/TranscriptionCard.swift @@ -1,17 +1,155 @@ import SwiftUI import SwiftData +enum ContentTab: String, CaseIterable { + case original = "Original" + case enhanced = "Enhanced" + case aiRequest = "AI Request" +} + struct TranscriptionCard: View { let transcription: Transcription let isExpanded: Bool let isSelected: Bool let onDelete: () -> Void let onToggleSelection: () -> Void - @State private var isAIRequestExpanded: Bool = false - + + @State private var selectedTab: ContentTab = .original + + private var availableTabs: [ContentTab] { + var tabs = [ContentTab.original] + if transcription.enhancedText != nil { + tabs.append(.enhanced) + } + if transcription.aiRequestSystemMessage != nil || transcription.aiRequestUserMessage != nil { + tabs.append(.aiRequest) + } + return tabs + } + + private var hasAudioFile: Bool { + if let urlString = transcription.audioFileURL, + let url = URL(string: urlString), + FileManager.default.fileExists(atPath: url.path) { + return true + } + return false + } + + private var copyTextForCurrentTab: String { + switch selectedTab { + case .original: + return transcription.text + case .enhanced: + return transcription.enhancedText ?? transcription.text + case .aiRequest: + var result = "" + if let systemMsg = transcription.aiRequestSystemMessage, !systemMsg.isEmpty { + result += systemMsg + } + if let userMsg = transcription.aiRequestUserMessage, !userMsg.isEmpty { + if !result.isEmpty { + result += "\n\n" + } + result += userMsg + } + return result.isEmpty ? transcription.text : result + } + } + + private var originalContentView: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Original") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.secondary) + Text(transcription.text) + .font(.system(size: 15, weight: .regular, design: .default)) + .lineSpacing(2) + .textSelection(.enabled) + } + } + + private func enhancedContentView(_ enhancedText: String) -> some View { + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 4) { + Image(systemName: "sparkles") + .foregroundColor(.blue) + Text("Enhanced") + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.blue) + } + Text(enhancedText) + .font(.system(size: 15, weight: .regular, design: .default)) + .lineSpacing(2) + .textSelection(.enabled) + } + } + + private var aiRequestContentView: some View { + VStack(alignment: .leading, spacing: 12) { + HStack(spacing: 6) { + Image(systemName: "paperplane.fill") + .foregroundColor(.purple) + Text("AI Request") + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(.purple) + } + + if let systemMsg = transcription.aiRequestSystemMessage, !systemMsg.isEmpty { + VStack(alignment: .leading, spacing: 6) { + Text("System Prompt") + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(.secondary) + Text(systemMsg) + .font(.system(size: 13, weight: .regular, design: .monospaced)) + .lineSpacing(2) + .textSelection(.enabled) + } + } + + if let userMsg = transcription.aiRequestUserMessage, !userMsg.isEmpty { + VStack(alignment: .leading, spacing: 6) { + Text("User Message") + .font(.system(size: 13, weight: .semibold)) + .foregroundColor(.secondary) + Text(userMsg) + .font(.system(size: 13, weight: .regular, design: .monospaced)) + .lineSpacing(2) + .textSelection(.enabled) + } + } + } + } + + private struct TabButton: View { + let title: String + let isSelected: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + Text(title) + .font(.system(size: 13, weight: isSelected ? .semibold : .medium)) + .foregroundColor(isSelected ? .white : .secondary) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(isSelected ? Color.accentColor : Color.clear) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(isSelected ? Color.clear : Color.secondary.opacity(0.3), lineWidth: 1) + ) + .contentShape(.capsule) + ) + } + .buttonStyle(.plain) + .animation(.easeInOut(duration: 0.2), value: isSelected) + } + } + var body: some View { HStack(spacing: 12) { - // Selection checkbox in macOS style Toggle("", isOn: Binding( get: { isSelected }, set: { _ in onToggleSelection() } @@ -19,14 +157,13 @@ struct TranscriptionCard: View { .toggleStyle(CircularCheckboxStyle()) .labelsHidden() - VStack(alignment: .leading, spacing: 8) { - // Header with date and duration + VStack(alignment: .leading, spacing: 12) { HStack { Text(transcription.timestamp, format: .dateTime.month(.abbreviated).day().year().hour().minute()) .font(.system(size: 14, weight: .medium, design: .default)) .foregroundColor(.secondary) Spacer() - + Text(formatTiming(transcription.duration)) .font(.system(size: 14, weight: .medium, design: .default)) .padding(.horizontal, 8) @@ -35,148 +172,90 @@ struct TranscriptionCard: View { .foregroundColor(.blue) .cornerRadius(6) } - - // Original text section - VStack(alignment: .leading, spacing: 8) { + + if isExpanded { + if availableTabs.count > 1 { + HStack(spacing: 2) { + ForEach(availableTabs, id: \.self) { tab in + TabButton( + title: tab.rawValue, + isSelected: selectedTab == tab, + action: { selectedTab = tab } + ) + } + + Spacer() + + AnimatedCopyButton(textToCopy: copyTextForCurrentTab) + } + .padding(.vertical, 8) + .padding(.horizontal, 4) + } + + ScrollView { + VStack(alignment: .leading, spacing: 12) { + switch selectedTab { + case .original: + originalContentView + case .enhanced: + if let enhancedText = transcription.enhancedText { + enhancedContentView(enhancedText) + } + case .aiRequest: + aiRequestContentView + } + } + .padding(.vertical, 8) + } + .frame(maxHeight: 300) + .background(Color(.controlBackgroundColor).opacity(0.5)) + .cornerRadius(8) + + if hasAudioFile, let urlString = transcription.audioFileURL, + let url = URL(string: urlString) { + Divider() + .padding(.vertical, 8) + AudioPlayerView(url: url) + } + + if hasMetadata { + Divider() + .padding(.vertical, 8) + + VStack(alignment: .leading, spacing: 10) { + if let powerModeValue = powerModeDisplay( + name: transcription.powerModeName, + emoji: transcription.powerModeEmoji + ) { + metadataRow( + icon: "bolt.fill", + label: "Power Mode", + value: powerModeValue + ) + } + metadataRow(icon: "hourglass", label: "Audio Duration", value: formatTiming(transcription.duration)) + if let modelName = transcription.transcriptionModelName { + metadataRow(icon: "cpu.fill", label: "Transcription Model", value: modelName) + } + if let aiModel = transcription.aiEnhancementModelName { + metadataRow(icon: "sparkles", label: "Enhancement Model", value: aiModel) + } + if let promptName = transcription.promptName { + metadataRow(icon: "text.bubble.fill", label: "Prompt Used", value: promptName) + } + if let duration = transcription.transcriptionDuration { + metadataRow(icon: "clock.fill", label: "Transcription Time", value: formatTiming(duration)) + } + if let duration = transcription.enhancementDuration { + metadataRow(icon: "clock.fill", label: "Enhancement Time", value: formatTiming(duration)) + } + } + } + } else { Text(transcription.text) .font(.system(size: 15, weight: .regular, design: .default)) - .lineLimit(isExpanded ? nil : 2) + .lineLimit(2) .lineSpacing(2) - - if isExpanded { - HStack { - Text("Original") - .font(.system(size: 14, weight: .medium)) - .foregroundColor(.secondary) - Spacer() - AnimatedCopyButton(textToCopy: transcription.text) - } - } - } - - // Enhanced text section (only when expanded) - if isExpanded, let enhancedText = transcription.enhancedText { - Divider() - .padding(.vertical, 8) - - VStack(alignment: .leading, spacing: 8) { - Text(enhancedText) - .font(.system(size: 15, weight: .regular, design: .default)) - .lineSpacing(2) - - HStack { - HStack(spacing: 4) { - Image(systemName: "sparkles") - .foregroundColor(.blue) - Text("Enhanced") - .font(.system(size: 14, weight: .medium)) - .foregroundColor(.blue) - } - Spacer() - AnimatedCopyButton(textToCopy: enhancedText) - } - } - } - - // NEW: AI Request payload (System + User messages) - folded by default - if isExpanded, (transcription.aiRequestSystemMessage != nil || transcription.aiRequestUserMessage != nil) { - Divider() - .padding(.vertical, 8) - - VStack(alignment: .leading, spacing: 8) { - HStack(spacing: 6) { - Image(systemName: "paperplane.fill") - .foregroundColor(.purple) - Text("AI Request") - .fontWeight(.semibold) - .foregroundColor(.purple) - Spacer() - } - .contentShape(Rectangle()) - .onTapGesture { - withAnimation(.easeInOut) { - isAIRequestExpanded.toggle() - } - } - - if isAIRequestExpanded { - VStack(alignment: .leading, spacing: 12) { - if let systemMsg = transcription.aiRequestSystemMessage, !systemMsg.isEmpty { - VStack(alignment: .leading, spacing: 6) { - HStack { - Text("System Prompt") - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(.secondary) - Spacer() - AnimatedCopyButton(textToCopy: systemMsg) - } - Text(systemMsg) - .font(.system(size: 13, weight: .regular, design: .monospaced)) - .lineSpacing(2) - } - } - - if let userMsg = transcription.aiRequestUserMessage, !userMsg.isEmpty { - VStack(alignment: .leading, spacing: 6) { - HStack { - Text("User Message") - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(.secondary) - Spacer() - AnimatedCopyButton(textToCopy: userMsg) - } - Text(userMsg) - .font(.system(size: 13, weight: .regular, design: .monospaced)) - .lineSpacing(2) - } - } - } - } - } - } - - // Audio player (if available) - if isExpanded, let urlString = transcription.audioFileURL, - let url = URL(string: urlString), - FileManager.default.fileExists(atPath: url.path) { - Divider() - .padding(.vertical, 8) - AudioPlayerView(url: url) - } - - // Metadata section (when expanded) - if isExpanded && hasMetadata { - Divider() - .padding(.vertical, 8) - - VStack(alignment: .leading, spacing: 10) { - if let powerModeValue = powerModeDisplay( - name: transcription.powerModeName, - emoji: transcription.powerModeEmoji - ) { - metadataRow( - icon: "bolt.fill", - label: "Power Mode", - value: powerModeValue - ) - } - metadataRow(icon: "hourglass", label: "Audio Duration", value: formatTiming(transcription.duration)) - if let modelName = transcription.transcriptionModelName { - metadataRow(icon: "cpu.fill", label: "Transcription Model", value: modelName) - } - if let aiModel = transcription.aiEnhancementModelName { - metadataRow(icon: "sparkles", label: "Enhancement Model", value: aiModel) - } - if let promptName = transcription.promptName { - metadataRow(icon: "text.bubble.fill", label: "Prompt Used", value: promptName) - } - if let duration = transcription.transcriptionDuration { - metadataRow(icon: "clock.fill", label: "Transcription Time", value: formatTiming(duration)) - } - if let duration = transcription.enhancementDuration { - metadataRow(icon: "clock.fill", label: "Enhancement Time", value: formatTiming(duration)) - } - } } } } diff --git a/VoiceInk/Views/TranscriptionResultView.swift b/VoiceInk/Views/TranscriptionResultView.swift new file mode 100644 index 0000000..bbcb5be --- /dev/null +++ b/VoiceInk/Views/TranscriptionResultView.swift @@ -0,0 +1,106 @@ +import SwiftUI + +enum TranscriptionTab: String, CaseIterable { + case original = "Original" + case enhanced = "Enhanced" +} + +struct TranscriptionResultView: View { + let transcription: Transcription + + @State private var selectedTab: TranscriptionTab = .original + + private var availableTabs: [TranscriptionTab] { + var tabs: [TranscriptionTab] = [.original] + if transcription.enhancedText != nil { + tabs.append(.enhanced) + } + return tabs + } + + private var textForSelectedTab: String { + switch selectedTab { + case .original: + return transcription.text + case .enhanced: + return transcription.enhancedText ?? "" + } + } + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + Text("Transcription Result") + .font(.headline) + + if availableTabs.count > 1 { + HStack(spacing: 2) { + ForEach(availableTabs, id: \.self) { tab in + TabButton( + title: tab.rawValue, + isSelected: selectedTab == tab, + action: { selectedTab = tab } + ) + } + Spacer() + AnimatedCopyButton(textToCopy: textForSelectedTab) + AnimatedSaveButton(textToSave: textForSelectedTab) + } + .padding(.vertical, 8) + .padding(.horizontal, 4) + } else { + HStack { + Spacer() + AnimatedCopyButton(textToCopy: textForSelectedTab) + AnimatedSaveButton(textToSave: textForSelectedTab) + } + } + + ScrollView { + Text(textForSelectedTab) + .textSelection(.enabled) + .frame(maxWidth: .infinity, alignment: .leading) + } + + HStack { + Text("Duration: \(formatDuration(transcription.duration))") + .font(.caption) + .foregroundColor(.secondary) + Spacer() + } + } + .padding() + } + + private func formatDuration(_ duration: TimeInterval) -> String { + let minutes = Int(duration) / 60 + let seconds = Int(duration) % 60 + return String(format: "%d:%02d", minutes, seconds) + } + + private struct TabButton: View { + let title: String + let isSelected: Bool + let action: () -> Void + + var body: some View { + Button(action: action) { + Text(title) + .font(.system(size: 13, weight: isSelected ? .semibold : .medium)) + .foregroundColor(isSelected ? .white : .secondary) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(isSelected ? Color.accentColor : Color.clear) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(isSelected ? Color.clear : Color.secondary.opacity(0.3), lineWidth: 1) + ) + .contentShape(.capsule) + ) + } + .buttonStyle(.plain) + .animation(.easeInOut(duration: 0.2), value: isSelected) + } + } +}