feat: actually implement system audio + refactor
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MeetingDetailView: View {
|
||||
@StateObject private var viewModel: MeetingViewModel
|
||||
|
||||
init(meeting: Meeting) {
|
||||
self._viewModel = StateObject(wrappedValue: MeetingViewModel(meeting: meeting))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
// Recording Controls
|
||||
VStack(spacing: 12) {
|
||||
Toggle("Capture System Audio", isOn: $viewModel.captureSystemAudio)
|
||||
.disabled(viewModel.isRecording)
|
||||
|
||||
HStack {
|
||||
Button {
|
||||
viewModel.startRecording()
|
||||
} label: {
|
||||
Label("Start Recording", systemImage: "record.circle")
|
||||
}
|
||||
.disabled(viewModel.isRecording)
|
||||
.buttonStyle(.borderedProminent)
|
||||
|
||||
Button {
|
||||
viewModel.stopRecording()
|
||||
} label: {
|
||||
Label("Stop Recording", systemImage: "stop.circle")
|
||||
}
|
||||
.disabled(!viewModel.isRecording)
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
|
||||
if viewModel.isRecording {
|
||||
Label("Recording...", systemImage: "dot.radiowaves.left.and.right")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.gray.opacity(0.1))
|
||||
.cornerRadius(10)
|
||||
|
||||
// Transcript and Notes
|
||||
HStack(spacing: 20) {
|
||||
// Transcript Column
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Text("Live Transcript")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Button {
|
||||
viewModel.copyTranscript()
|
||||
} label: {
|
||||
Label("Copy", systemImage: "doc.on.doc")
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
Text(viewModel.meeting.transcript.isEmpty ? "Transcript will appear here..." : viewModel.meeting.transcript)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.background(Color.gray.opacity(0.05))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
|
||||
// Notes Column
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Your Notes")
|
||||
.font(.headline)
|
||||
|
||||
TextEditor(text: $viewModel.meeting.userNotes)
|
||||
.font(.body)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.gray.opacity(0.05))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: 300)
|
||||
|
||||
// Generated Notes Section
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Text("Generated Notes")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
|
||||
if viewModel.isGeneratingNotes {
|
||||
ProgressView()
|
||||
.scaleEffect(0.7)
|
||||
} else {
|
||||
Button {
|
||||
Task {
|
||||
await viewModel.generateNotes()
|
||||
}
|
||||
} label: {
|
||||
Label("Generate", systemImage: "sparkles")
|
||||
}
|
||||
.disabled(viewModel.meeting.transcript.isEmpty)
|
||||
|
||||
Button {
|
||||
viewModel.copyNotes()
|
||||
} label: {
|
||||
Label("Copy", systemImage: "doc.on.doc")
|
||||
}
|
||||
.disabled(viewModel.meeting.generatedNotes.isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
TextEditor(text: $viewModel.meeting.generatedNotes)
|
||||
.font(.body)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.gray.opacity(0.05))
|
||||
.cornerRadius(8)
|
||||
.frame(minHeight: 150)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.navigationTitle("Meeting Notes")
|
||||
.alert("Error", isPresented: .constant(viewModel.errorMessage != nil)) {
|
||||
Button("OK") {
|
||||
viewModel.errorMessage = nil
|
||||
}
|
||||
} message: {
|
||||
Text(viewModel.errorMessage ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
MeetingDetailView(meeting: Meeting())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MeetingListView: View {
|
||||
@StateObject private var viewModel = MeetingListViewModel()
|
||||
@State private var navigationPath = NavigationPath()
|
||||
|
||||
var body: some View {
|
||||
NavigationStack(path: $navigationPath) {
|
||||
List {
|
||||
if viewModel.meetings.isEmpty && !viewModel.isLoading {
|
||||
ContentUnavailableView(
|
||||
"No Meetings Yet",
|
||||
systemImage: "mic.slash",
|
||||
description: Text("Start a new meeting to begin transcribing")
|
||||
)
|
||||
} else {
|
||||
ForEach(viewModel.meetings) { meeting in
|
||||
NavigationLink(value: meeting) {
|
||||
MeetingRowView(meeting: meeting)
|
||||
}
|
||||
}
|
||||
.onDelete { indexSet in
|
||||
for index in indexSet {
|
||||
viewModel.deleteMeeting(viewModel.meetings[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Meetings")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button {
|
||||
let newMeeting = viewModel.createNewMeeting()
|
||||
navigationPath.append(newMeeting)
|
||||
} label: {
|
||||
Label("New Meeting", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationDestination(for: Meeting.self) { meeting in
|
||||
MeetingDetailView(meeting: meeting)
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
if viewModel.isLoading {
|
||||
ProgressView("Loading meetings...")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MeetingRowView: View {
|
||||
let meeting: Meeting
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(meeting.date, style: .date)
|
||||
.font(.headline)
|
||||
|
||||
if !meeting.transcript.isEmpty {
|
||||
Text(meeting.transcript)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(2)
|
||||
} else {
|
||||
Text("No transcript")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.italic()
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MeetingListView()
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@StateObject private var viewModel = SettingsViewModel()
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section("API Keys") {
|
||||
SecureField("Deepgram API Key", text: $viewModel.settings.deepgramKey)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
|
||||
SecureField("OpenAI API Key", text: $viewModel.settings.openAIKey)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
}
|
||||
|
||||
Section("Personal Information") {
|
||||
VStack(alignment: .leading) {
|
||||
Text("About Yourself")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
TextEditor(text: $viewModel.settings.userBlurb)
|
||||
.frame(minHeight: 80)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.gray.opacity(0.05))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
|
||||
Section("AI Generation") {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text("System Prompt")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Button("Reset to Default") {
|
||||
viewModel.resetToDefaults()
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
TextEditor(text: $viewModel.settings.systemPrompt)
|
||||
.frame(minHeight: 120)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.gray.opacity(0.05))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Save") {
|
||||
viewModel.saveSettings()
|
||||
}
|
||||
.disabled(viewModel.isSaving)
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
if viewModel.saveSuccessful {
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
Text("Settings saved successfully")
|
||||
}
|
||||
.padding()
|
||||
.background(Color.green.opacity(0.1))
|
||||
.cornerRadius(10)
|
||||
.padding()
|
||||
}
|
||||
.transition(.move(edge: .bottom).combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
.alert("Error", isPresented: .constant(viewModel.errorMessage != nil)) {
|
||||
Button("OK") {
|
||||
viewModel.errorMessage = nil
|
||||
}
|
||||
} message: {
|
||||
Text(viewModel.errorMessage ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SettingsView()
|
||||
}
|
||||
Reference in New Issue
Block a user