Files
meetingnotes/notetaker/Views/MeetingDetailView.swift
T

246 lines
8.7 KiB
Swift

import SwiftUI
struct CollapsedTranscriptChunkView: View {
let chunk: CollapsedTranscriptChunk
var body: some View {
HStack(alignment: .top, spacing: 8) {
// Source indicator
HStack(spacing: 4) {
Image(systemName: chunk.source.icon)
.font(.caption)
.foregroundColor(chunk.source == .mic ? .blue : .orange)
Text(chunk.source.displayName)
.font(.caption)
.fontWeight(.medium)
.foregroundColor(chunk.source == .mic ? .blue : .orange)
}
.frame(width: 50, alignment: .leading)
// Transcript text
Text(chunk.combinedText)
.font(.body)
.foregroundColor(.primary)
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.vertical, 2)
}
}
struct MeetingDetailView: View {
@StateObject private var viewModel: MeetingViewModel
@Environment(\.dismiss) private var dismiss
@State private var showDeleteAlert = false
init(meeting: Meeting) {
self._viewModel = StateObject(wrappedValue: MeetingViewModel(meeting: meeting))
}
var body: some View {
VStack(alignment: .leading, spacing: 20) {
// Meeting Title with Menu
HStack {
TextField("Meeting Title", text: $viewModel.meeting.title)
.font(.title2)
.fontWeight(.semibold)
.textFieldStyle(.plain)
Spacer()
// Ellipsis menu
Menu {
Button("Delete Meeting", role: .destructive) {
showDeleteAlert = true
}
} label: {
Image(systemName: "ellipsis")
.resizable()
.scaledToFit()
.frame(width: 12, height: 12)
.foregroundColor(.secondary)
}
.labelStyle(.iconOnly)
.menuIndicator(.hidden)
.menuStyle(BorderlessButtonMenuStyle())
.frame(width: 20, height: 20)
}
.padding(.bottom, 10)
// Controls Section
HStack {
// Left: Tab Toggles
// Replace checkbox-style toggles with a native segmented control for a cleaner look
Picker("", selection: $viewModel.selectedTab) {
ForEach(MeetingViewTab.allCases, id: \.self) { tab in
Text(tab.rawValue).tag(tab)
}
}
.pickerStyle(.segmented)
.frame(maxWidth: 260) // keep it compact so the recording controls have space
Spacer()
// Right: Recording and Copy Buttons
HStack(spacing: 12) {
Button(action: {
viewModel.toggleRecording()
}) {
HStack(spacing: 4) {
Image(systemName: viewModel.recordingState == .recording ? "stop.circle.fill" : "record.circle")
.foregroundColor(viewModel.recordingState == .recording ? .red : .accentColor)
Text(viewModel.recordingButtonText)
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(viewModel.recordingState == .recording ? Color.red.opacity(0.1) : Color.accentColor.opacity(0.1))
.cornerRadius(8)
}
.buttonStyle(.plain)
Button(action: {
viewModel.copyCurrentTabContent()
}) {
HStack(spacing: 4) {
Image(systemName: "doc.on.doc")
Text("Copy")
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
.buttonStyle(.plain)
}
}
// Content Area
VStack(alignment: .leading, spacing: 8) {
switch viewModel.selectedTab {
case .myNotes:
myNotesView
case .transcript:
transcriptView
case .enhancedNotes:
enhancedNotesView
}
}
.frame(maxHeight: .infinity)
}
.padding()
.navigationTitle("")
.alert("Error", isPresented: .constant(viewModel.errorMessage != nil)) {
Button("OK") {
viewModel.errorMessage = nil
}
} message: {
Text(viewModel.errorMessage ?? "")
}
.alert("Delete Meeting", isPresented: $showDeleteAlert) {
Button("Delete", role: .destructive) {
viewModel.deleteMeeting()
dismiss()
}
Button("Cancel", role: .cancel) { }
} message: {
Text("Are you sure you want to delete this meeting? This action cannot be undone.")
}
.onDisappear {
viewModel.saveMeeting()
}
}
// MARK: - Content Views
private var myNotesView: some View {
VStack(alignment: .leading, spacing: 8) {
Text("My Notes")
.font(.headline)
.foregroundColor(.secondary)
TextEditor(text: $viewModel.meeting.userNotes)
.font(.body)
.scrollContentBackground(.hidden)
.background(Color.gray.opacity(0.05))
.cornerRadius(8)
.frame(maxHeight: .infinity)
}
}
private var transcriptView: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Transcript")
.font(.headline)
.foregroundColor(.secondary)
ScrollView {
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(viewModel.meeting.collapsedTranscriptChunks) { chunk in
CollapsedTranscriptChunkView(chunk: chunk)
}
}
.padding()
}
}
.frame(maxHeight: .infinity)
.background(Color.gray.opacity(0.05))
.cornerRadius(8)
}
}
private var enhancedNotesView: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("Enhanced Notes")
.font(.headline)
.foregroundColor(.secondary)
Spacer()
if viewModel.isGeneratingNotes {
ProgressView()
.scaleEffect(0.7)
} else {
Button(action: {
Task {
await viewModel.generateNotes()
}
}) {
HStack(spacing: 4) {
Image(systemName: "sparkles")
Text("Generate")
}
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.accentColor.opacity(0.1))
.cornerRadius(6)
}
.buttonStyle(.plain)
.disabled(viewModel.meeting.transcript.isEmpty)
}
}
TextEditor(text: $viewModel.meeting.generatedNotes)
.font(.body)
.scrollContentBackground(.hidden)
.background(Color.gray.opacity(0.05))
.cornerRadius(8)
.frame(maxHeight: .infinity)
}
}
}
#Preview {
NavigationStack {
MeetingDetailView(meeting: Meeting())
}
}