diff --git a/notetaker/Models/Meeting.swift b/notetaker/Models/Meeting.swift index 6730210..3e01aa6 100644 --- a/notetaker/Models/Meeting.swift +++ b/notetaker/Models/Meeting.swift @@ -7,9 +7,18 @@ enum AudioSource: String, Codable, CaseIterable { var displayName: String { switch self { case .mic: - return "Microphone" + return "Me" case .system: - return "System Audio" + return "Them" + } + } + + var copyPrefix: String { + switch self { + case .mic: + return "Me" + case .system: + return "Them" } } @@ -39,6 +48,20 @@ struct TranscriptChunk: Codable, Identifiable, Hashable { } } +struct CollapsedTranscriptChunk: Identifiable { + let id: UUID + let timestamp: Date + let source: AudioSource + let combinedText: String + + init(id: UUID = UUID(), timestamp: Date, source: AudioSource, combinedText: String) { + self.id = id + self.timestamp = timestamp + self.source = source + self.combinedText = combinedText + } +} + struct Meeting: Codable, Identifiable, Hashable { let id: UUID let date: Date @@ -66,6 +89,86 @@ struct Meeting: Codable, Identifiable, Hashable { .joined(separator: " ") } + // Formatted transcript for copying with collapsed sequential chunks + var formattedTranscript: String { + let finalChunks = transcriptChunks.filter { $0.isFinal } + + guard !finalChunks.isEmpty else { return "" } + + var result: [String] = [] + var currentSource: AudioSource? + var currentTexts: [String] = [] + + for chunk in finalChunks { + if chunk.source != currentSource { + // Finish previous section if exists + if let source = currentSource, !currentTexts.isEmpty { + let combinedText = currentTexts.joined(separator: " ") + result.append("\(source.copyPrefix): \(combinedText)") + } + + // Start new section + currentSource = chunk.source + currentTexts = [chunk.text] + } else { + // Same source, add to current section + currentTexts.append(chunk.text) + } + } + + // Finish last section + if let source = currentSource, !currentTexts.isEmpty { + let combinedText = currentTexts.joined(separator: " ") + result.append("\(source.copyPrefix): \(combinedText)") + } + + return result.joined(separator: " \n") + } + + // Collapsed chunks for UI display + var collapsedTranscriptChunks: [CollapsedTranscriptChunk] { + guard !transcriptChunks.isEmpty else { return [] } + + var result: [CollapsedTranscriptChunk] = [] + var currentSource: AudioSource? + var currentTexts: [String] = [] + var currentTimestamp: Date? + + for chunk in transcriptChunks { + if chunk.source != currentSource { + // Finish previous section if exists + if let source = currentSource, !currentTexts.isEmpty, let timestamp = currentTimestamp { + let combinedText = currentTexts.joined(separator: " ") + result.append(CollapsedTranscriptChunk( + timestamp: timestamp, + source: source, + combinedText: combinedText + )) + } + + // Start new section + currentSource = chunk.source + currentTexts = [chunk.text] + currentTimestamp = chunk.timestamp + } else { + // Same source, add to current section + currentTexts.append(chunk.text) + } + } + + // Finish last section + if let source = currentSource, !currentTexts.isEmpty, let timestamp = currentTimestamp { + let combinedText = currentTexts.joined(separator: " ") + result.append(CollapsedTranscriptChunk( + timestamp: timestamp, + source: source, + combinedText: combinedText + )) + } + + return result + } + // Separate computed properties for mic and system transcripts var micTranscript: String { return transcriptChunks diff --git a/notetaker/ViewModels/MeetingViewModel.swift b/notetaker/ViewModels/MeetingViewModel.swift index 15af9a7..4f08398 100644 --- a/notetaker/ViewModels/MeetingViewModel.swift +++ b/notetaker/ViewModels/MeetingViewModel.swift @@ -75,17 +75,7 @@ class MeetingViewModel: ObservableObject { func copyTranscript() { NSPasteboard.general.clearContents() - NSPasteboard.general.setString(meeting.transcript, forType: .string) - } - - func copyMicTranscript() { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(meeting.micTranscript, forType: .string) - } - - func copySystemTranscript() { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(meeting.systemTranscript, forType: .string) + NSPasteboard.general.setString(meeting.formattedTranscript, forType: .string) } func copyNotes() { diff --git a/notetaker/Views/MeetingDetailView.swift b/notetaker/Views/MeetingDetailView.swift index d2da02d..8cec0e5 100644 --- a/notetaker/Views/MeetingDetailView.swift +++ b/notetaker/Views/MeetingDetailView.swift @@ -1,7 +1,7 @@ import SwiftUI -struct TranscriptChunkView: View { - let chunk: TranscriptChunk +struct CollapsedTranscriptChunkView: View { + let chunk: CollapsedTranscriptChunk var body: some View { HStack(alignment: .top, spacing: 8) { @@ -11,58 +11,30 @@ struct TranscriptChunkView: View { .font(.caption) .foregroundColor(chunk.source == .mic ? .blue : .orange) - Text(chunk.source.rawValue) + Text(chunk.source.displayName) .font(.caption) .fontWeight(.medium) .foregroundColor(chunk.source == .mic ? .blue : .orange) } - .frame(width: 60, alignment: .leading) + .frame(width: 50, alignment: .leading) // Transcript text - Text(chunk.text) + Text(chunk.combinedText) .font(.body) - .foregroundColor(chunk.isFinal ? .primary : .secondary) - .italic(!chunk.isFinal) + .foregroundColor(.primary) .frame(maxWidth: .infinity, alignment: .leading) } .padding(.vertical, 2) - .opacity(chunk.isFinal ? 1.0 : 0.7) } } struct MeetingDetailView: View { @StateObject private var viewModel: MeetingViewModel - @State private var selectedTranscriptFilter: TranscriptFilter = .all init(meeting: Meeting) { self._viewModel = StateObject(wrappedValue: MeetingViewModel(meeting: meeting)) } - enum TranscriptFilter: String, CaseIterable { - case all = "All" - case mic = "Microphone" - case system = "System Audio" - - var icon: String { - switch self { - case .all: return "waveform" - case .mic: return "mic.fill" - case .system: return "speaker.wave.2.fill" - } - } - } - - private var filteredTranscriptChunks: [TranscriptChunk] { - switch selectedTranscriptFilter { - case .all: - return viewModel.meeting.transcriptChunks - case .mic: - return viewModel.meeting.transcriptChunks.filter { $0.source == .mic } - case .system: - return viewModel.meeting.transcriptChunks.filter { $0.source == .system } - } - } - var body: some View { VStack(spacing: 20) { // Recording Controls @@ -105,50 +77,23 @@ struct MeetingDetailView: View { Text("Live Transcript") .font(.headline) Spacer() - - // Filter controls - Picker("Filter", selection: $selectedTranscriptFilter) { - ForEach(TranscriptFilter.allCases, id: \.self) { filter in - Label(filter.rawValue, systemImage: filter.icon) - .tag(filter) - } - } - .pickerStyle(SegmentedPickerStyle()) - .frame(width: 200) - - Menu { - Button { - viewModel.copyTranscript() - } label: { - Label("Copy All", systemImage: "doc.on.doc") - } - - Button { - viewModel.copyMicTranscript() - } label: { - Label("Copy Microphone Only", systemImage: "mic.fill") - } - - Button { - viewModel.copySystemTranscript() - } label: { - Label("Copy System Audio Only", systemImage: "speaker.wave.2.fill") - } + Button { + viewModel.copyTranscript() } label: { Label("Copy", systemImage: "doc.on.doc") } } ScrollView { - if filteredTranscriptChunks.isEmpty { + if viewModel.meeting.collapsedTranscriptChunks.isEmpty { Text("Transcript will appear here...") .frame(maxWidth: .infinity, alignment: .leading) .padding() .foregroundColor(.secondary) } else { LazyVStack(alignment: .leading, spacing: 4) { - ForEach(filteredTranscriptChunks) { chunk in - TranscriptChunkView(chunk: chunk) + ForEach(viewModel.meeting.collapsedTranscriptChunks) { chunk in + CollapsedTranscriptChunkView(chunk: chunk) } } .padding() @@ -226,4 +171,5 @@ struct MeetingDetailView: View { NavigationStack { MeetingDetailView(meeting: Meeting()) } -} \ No newline at end of file +} +