feat: add rendered notes view

This commit is contained in:
Owen Gretzinger
2025-07-11 23:16:32 -04:00
parent 746bcc4122
commit 78393af91c
4 changed files with 122 additions and 7 deletions
+25 -6
View File
@@ -32,6 +32,7 @@ struct MeetingDetailView: View {
@StateObject private var viewModel: MeetingViewModel
@Environment(\.dismiss) private var dismiss
@State private var showDeleteAlert = false
@State private var isEditing = false
init(meeting: Meeting) {
self._viewModel = StateObject(wrappedValue: MeetingViewModel(meeting: meeting))
@@ -210,6 +211,7 @@ struct MeetingDetailView: View {
Button(action: {
Task {
await viewModel.generateNotes()
isEditing = false
}
}) {
HStack(spacing: 4) {
@@ -225,14 +227,31 @@ struct MeetingDetailView: View {
.buttonStyle(.plain)
.disabled(viewModel.meeting.transcript.isEmpty)
}
Button(action: {
isEditing.toggle()
}) {
Image(systemName: isEditing ? "pencil.circle.fill" : "pencil.circle")
}
.buttonStyle(.plain)
}
TextEditor(text: $viewModel.meeting.generatedNotes)
.font(.body)
.scrollContentBackground(.hidden)
.background(Color.gray.opacity(0.05))
.cornerRadius(8)
.frame(maxHeight: .infinity)
if isEditing {
TextEditor(text: Binding(
get: { viewModel.meeting.generatedNotes },
set: { viewModel.meeting.generatedNotes = $0 }
))
.font(.body)
.scrollContentBackground(.hidden)
.background(Color.gray.opacity(0.05))
.cornerRadius(8)
.frame(maxHeight: .infinity)
} else {
RenderedNotesView(text: viewModel.meeting.generatedNotes)
.font(.body)
.background(Color.gray.opacity(0.05))
.cornerRadius(8)
.frame(maxHeight: .infinity)
}
}
}
}
+87
View File
@@ -0,0 +1,87 @@
import SwiftUI
struct RenderedNotesView: View {
let text: String
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 8) {
ForEach(text.split(separator: "\n"), id: \.self) { line in
self.renderLine(String(line))
}
}
.padding()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
@ViewBuilder
private func renderLine(_ line: String) -> some View {
let trimmed = line.trimmingCharacters(in: .whitespaces)
if trimmed.isEmpty {
EmptyView()
} else if let level = headingLevel(for: trimmed) {
let content = String(trimmed.dropFirst(level + 1)).trimmingCharacters(in: .whitespaces)
Text(content)
.font(.system(size: CGFloat(28 - level * 4), weight: .bold))
.foregroundColor(.white)
.frame(maxWidth: .infinity, alignment: .leading)
} else if let (indentLevel, bullet, content) = listItemInfo(for: line) {
HStack(alignment: .firstTextBaseline, spacing: 4) {
Text(bullet)
.foregroundColor(.gray)
Text(content)
.foregroundColor(.gray)
}
.padding(.leading, CGFloat(indentLevel * 15))
} else {
Text(trimmed)
.foregroundColor(.primary)
}
}
private func headingLevel(for line: String) -> Int? {
guard line.hasPrefix("#") else { return nil }
var count = 0
for char in line {
if char == "#" {
count += 1
} else if char == " " {
return count
} else {
return nil
}
}
return nil
}
private func listItemInfo(for line: String) -> (indentLevel: Int, bullet: String, content: String)? {
let leadingSpaces = line.prefix(while: { $0 == " " }).count
let indentLevel = leadingSpaces / 2
let remaining = String(line.dropFirst(leadingSpaces))
if remaining.hasPrefix("- ") {
return (indentLevel, getBullet(for: indentLevel), String(remaining.dropFirst(2)).trimmingCharacters(in: .whitespaces))
} else if remaining.hasPrefix("* ") {
return (indentLevel, getBullet(for: indentLevel), String(remaining.dropFirst(2)).trimmingCharacters(in: .whitespaces))
} else if let dotIndex = remaining.firstIndex(of: "."), let num = Int(remaining[remaining.startIndex..<dotIndex]), remaining[dotIndex..<remaining.endIndex].hasPrefix(". ") {
let contentStart = remaining.index(dotIndex, offsetBy: 2)
return (indentLevel, "\(num).", String(remaining[contentStart...]).trimmingCharacters(in: .whitespaces))
}
return nil
}
private func getBullet(for level: Int) -> String {
switch level % 3 {
case 0: return ""
case 1: return ""
case 2: return "▪︎"
default: return "-"
}
}
}
#Preview {
RenderedNotesView(text: "# Heading 1\n## Heading 2\n- List item\n - Nested item\n - Deeper item\n1. Ordered\n 1. Nested ordered\nNormal text")
}