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:
@@ -53,6 +53,21 @@ final class AppSettings: ObservableObject {
|
||||
didSet { defaults.set(recapEnabled, forKey: Keys.recapEnabled) }
|
||||
}
|
||||
|
||||
/// User-editable recap templates (takeaways categories per meeting type).
|
||||
@Published var recapTemplates: [RecapTemplate] {
|
||||
didSet { persist(recapTemplates, forKey: Keys.recapTemplates) }
|
||||
}
|
||||
|
||||
/// Id of the template used automatically for new recaps.
|
||||
@Published var defaultTemplateId: String {
|
||||
didSet { defaults.set(defaultTemplateId, forKey: Keys.defaultTemplate) }
|
||||
}
|
||||
|
||||
/// The active default template (falls back to the first available / built-in).
|
||||
var defaultTemplate: RecapTemplate {
|
||||
recapTemplates.first { $0.id == defaultTemplateId } ?? recapTemplates.first ?? .internalMeeting
|
||||
}
|
||||
|
||||
/// Output folder as a resolved file URL (expands a leading `~`).
|
||||
var outputFolderURL: URL {
|
||||
URL(fileURLWithPath: (outputFolderPath as NSString).expandingTildeInPath,
|
||||
@@ -81,6 +96,16 @@ final class AppSettings: ObservableObject {
|
||||
self.selfName = defaults.string(forKey: Keys.selfName) ?? "Me"
|
||||
self.autoSendOnStop = defaults.object(forKey: Keys.autoSend) as? Bool ?? false
|
||||
self.recapEnabled = defaults.object(forKey: Keys.recapEnabled) as? Bool ?? true
|
||||
|
||||
let loaded = (defaults.data(forKey: Keys.recapTemplates))
|
||||
.flatMap { try? JSONDecoder().decode([RecapTemplate].self, from: $0) }
|
||||
self.recapTemplates = (loaded?.isEmpty == false) ? loaded! : RecapTemplate.builtIns
|
||||
self.defaultTemplateId = defaults.string(forKey: Keys.defaultTemplate)
|
||||
?? RecapTemplate.builtIns.first!.id
|
||||
}
|
||||
|
||||
private func persist<T: Encodable>(_ value: T, forKey key: String) {
|
||||
if let data = try? JSONEncoder().encode(value) { defaults.set(data, forKey: key) }
|
||||
}
|
||||
|
||||
private enum Keys {
|
||||
@@ -92,5 +117,7 @@ final class AppSettings: ObservableObject {
|
||||
static let selfName = "selfName"
|
||||
static let autoSend = "autoSendOnStop"
|
||||
static let recapEnabled = "recapEnabled"
|
||||
static let recapTemplates = "recapTemplates"
|
||||
static let defaultTemplate = "defaultTemplateId"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user