import SwiftUI /// Manage recap templates: the takeaways categories per meeting type. Edit titles, /// types, and the per-category instructions that drive the LLM — and preview the /// exact prompt that gets sent, so nothing is hidden in code. struct TemplatesView: View { @ObservedObject var settings: AppSettings @State private var selectedId: String? @State private var previewText: String? var body: some View { HSplitView { sidebar.frame(minWidth: 200, maxWidth: 260) detail.frame(minWidth: 420) } .frame(minWidth: 720, minHeight: 520) .onAppear { if selectedId == nil { selectedId = settings.defaultTemplateId } } .sheet(item: Binding(get: { previewText.map { IdString($0) } }, set: { previewText = $0?.value })) { item in previewSheet(item.value) } } // MARK: - Sidebar (template list + default picker) private var sidebar: some View { VStack(alignment: .leading, spacing: 0) { List(selection: $selectedId) { ForEach(settings.recapTemplates) { t in HStack { Text(t.name) if t.id == settings.defaultTemplateId { Spacer(); Image(systemName: "star.fill").foregroundStyle(.yellow).font(.caption) } }.tag(t.id) } } Divider() HStack(spacing: 6) { Button { addTemplate() } label: { Image(systemName: "plus") } Button { duplicateSelected() } label: { Image(systemName: "plus.square.on.square") } .disabled(selected == nil) Button { deleteSelected() } label: { Image(systemName: "trash") } .disabled(selected == nil || settings.recapTemplates.count <= 1) Spacer() } .buttonStyle(.borderless).padding(8) } } // MARK: - Detail (edit selected template) private var detail: some View { Group { if let binding = selectedBinding { ScrollView { VStack(alignment: .leading, spacing: 14) { HStack { TextField("Template name", text: binding.name).textFieldStyle(.roundedBorder) if binding.wrappedValue.id != settings.defaultTemplateId { Button("Make default") { settings.defaultTemplateId = binding.wrappedValue.id } } else { Label("Default", systemImage: "star.fill").foregroundStyle(.secondary).font(.caption) } } HStack(spacing: 16) { Toggle("Include summary (TLDR)", isOn: binding.includeTLDR) Picker("Topic detail", selection: binding.topicGranularity) { ForEach(TopicGranularity.allCases) { Text($0.label).tag($0) } }.frame(maxWidth: 240) } Divider() Text("Takeaways sections").font(.headline) Text("Each becomes a section of the recap. The instruction is the prompt text telling the model what to extract.") .font(.caption).foregroundStyle(.secondary) sectionsEditor(binding) Button { addSection(binding) } label: { Label("Add section", systemImage: "plus") } Divider() Button("Preview prompt…") { previewText = buildPreview(binding.wrappedValue) } } .padding(16) } } else { Text("Select a template").foregroundStyle(.secondary).frame(maxWidth: .infinity, maxHeight: .infinity) } } } private func sectionsEditor(_ template: Binding) -> some View { VStack(spacing: 10) { ForEach(Array(template.sections.enumerated()), id: \.element.id) { idx, section in VStack(alignment: .leading, spacing: 6) { HStack { TextField("Section title", text: section.title).textFieldStyle(.roundedBorder) Picker("", selection: section.kind) { ForEach(SectionKind.allCases) { Text($0.label).tag($0) } }.labelsHidden().frame(width: 150) Button { move(template, idx, by: -1) } label: { Image(systemName: "arrow.up") } .disabled(idx == 0).buttonStyle(.borderless) Button { move(template, idx, by: 1) } label: { Image(systemName: "arrow.down") } .disabled(idx == template.wrappedValue.sections.count - 1).buttonStyle(.borderless) Button { removeSection(template, idx) } label: { Image(systemName: "trash") } .buttonStyle(.borderless).foregroundStyle(.red) } TextEditor(text: section.instruction) .font(.callout).frame(minHeight: 48, maxHeight: 90) .overlay(RoundedRectangle(cornerRadius: 5).stroke(.quaternary)) } .padding(10).background(Color(nsColor: .controlBackgroundColor)).cornerRadius(8) } } } // MARK: - Mutations private var selected: RecapTemplate? { settings.recapTemplates.first { $0.id == selectedId } } private var selectedBinding: Binding? { guard let id = selectedId, let i = settings.recapTemplates.firstIndex(where: { $0.id == id }) else { return nil } return Binding(get: { settings.recapTemplates[i] }, set: { settings.recapTemplates[i] = $0 }) } private func addTemplate() { let t = RecapTemplate(name: "New Template", sections: [ .init(title: "Key Takeaways", kind: .bullets, instruction: "The main points from the conversation.") ]) settings.recapTemplates.append(t); selectedId = t.id } private func duplicateSelected() { guard let s = selected else { return } var copy = s; copy.id = UUID().uuidString; copy.name = s.name + " copy" copy.sections = s.sections.map { var c = $0; c.id = UUID().uuidString; return c } settings.recapTemplates.append(copy); selectedId = copy.id } private func deleteSelected() { guard let id = selectedId else { return } settings.recapTemplates.removeAll { $0.id == id } if settings.defaultTemplateId == id { settings.defaultTemplateId = settings.recapTemplates.first?.id ?? "" } selectedId = settings.recapTemplates.first?.id } private func addSection(_ t: Binding) { t.wrappedValue.sections.append(.init(title: "New Section", kind: .bullets, instruction: "")) } private func removeSection(_ t: Binding, _ idx: Int) { guard t.wrappedValue.sections.indices.contains(idx) else { return } t.wrappedValue.sections.remove(at: idx) } private func move(_ t: Binding, _ idx: Int, by delta: Int) { let j = idx + delta guard t.wrappedValue.sections.indices.contains(idx), t.wrappedValue.sections.indices.contains(j) else { return } t.wrappedValue.sections.swapAt(idx, j) } // MARK: - Preview private func buildPreview(_ template: RecapTemplate) -> String { let sample = SpeakersFile(sessionId: "sample", app: "meet", durationSec: 600, speakers: [], segments: [ .init(start: 5, end: 9, speaker: "Grant", text: "Let's review the roadmap."), .init(start: 10, end: 16, speaker: "Caitlyn", text: "I think we should ship dual-channel first."), ], models: [:]) let entries = RecapAnalyzer.entries(from: sample) return RecapAnalyzer.extrasPrompt(file: sample, entries: entries, sections: [], template: template) } private func previewSheet(_ text: String) -> some View { VStack(alignment: .leading, spacing: 10) { Text("Prompt sent to the model (with sample transcript)").font(.headline) ScrollView { Text(text).font(.system(.caption, design: .monospaced)).textSelection(.enabled) .frame(maxWidth: .infinity, alignment: .leading) } .frame(width: 560, height: 380) .overlay(RoundedRectangle(cornerRadius: 6).stroke(.quaternary)) HStack { Spacer(); Button("Done") { previewText = nil }.keyboardShortcut(.defaultAction) } } .padding(16) } } private struct IdString: Identifiable { let value: String; var id: String { value }; init(_ v: String) { value = v } }