Configurable recap templates (categories per meeting type, in Settings)

Takeaways categories are no longer hardcoded — they're editable templates. A
template = the always-on TLDR + an ordered list of sections, each with a title, a
type (attributed items / bulleted list / paragraph), and an instruction (the prompt
text for that category). The analyzer assembles the LLM prompt FROM the template
and parses generically, so adding/removing/renaming a category needs zero code and
the output always renders.

- RecapTemplate / TemplateSection / SectionKind + TopicGranularity; built-in
  defaults (Internal Meeting, 1:1, Company/Sales Call), all editable.
- Generic extras: RecapExtras{tldr, primarySpeakers, sections:[RenderedSection]} +
  RecapItem{text,who,when,note} replaces the fixed MeetingExtras. Analyzer builds
  per-section sec_N fields + parses by kind; renderer + remap are generic.
- Topic granularity (coarse/auto/fine) answers 'should chunking be configurable' —
  it scales the target topic count; raw window sizes stay as tuned defaults.
- AppSettings persists templates + defaultTemplateId (seeded once). Settings gets a
  default-template picker + 'Manage…' → TemplatesView (CRUD, edit sections/
  instructions, set default, **Preview prompt** for full transparency).
- Recap editor gains a template picker; Regenerate uses the chosen template. Auto
  recap uses the default template.

54/54 XCTest (template prompt build, generic parse/remap/render updated).
This commit is contained in:
Grant Gilliam
2026-06-06 19:26:03 -05:00
parent 10ddf9992a
commit c539b78a58
14 changed files with 580 additions and 227 deletions
@@ -98,8 +98,12 @@ struct TranscriptEditorView: View {
Button("Save corrections") { model.save() }
.keyboardShortcut("s", modifiers: .command)
.disabled(!model.dirty || model.regenerating)
Picker("", selection: $model.selectedTemplateId) {
ForEach(model.templates) { Text($0.name).tag($0.id) }
}
.labelsHidden().frame(maxWidth: 150).disabled(model.regenerating)
Button("Regenerate recap") { Task { await model.regenerate() } }
.help("Re-run the analysis on the corrected transcript so summaries use the fixed names.")
.help("Re-run the analysis on the corrected transcript with the chosen template.")
.disabled(model.regenerating)
if model.regenerating { ProgressView().controlSize(.small) }
if let s = model.status { Text(s).font(.caption).foregroundStyle(.secondary) }