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:
@@ -18,6 +18,8 @@ final class RecapEditModel: ObservableObject {
|
||||
private var originalSpeakers: [String]
|
||||
private var renameOps: [(from: String, to: String)] = []
|
||||
|
||||
let templates: [RecapTemplate]
|
||||
@Published var selectedTemplateId: String
|
||||
@Published private(set) var segments: [SpeakersFile.Segment]
|
||||
@Published private(set) var speakers: [String]
|
||||
@Published private(set) var dirty = false
|
||||
@@ -25,7 +27,8 @@ final class RecapEditModel: ObservableObject {
|
||||
@Published private(set) var hasRecap: Bool
|
||||
@Published private(set) var status: String?
|
||||
|
||||
init?(folder: URL, voiceprints: VoiceprintStore, baseURL: String, skipTLS: Bool) {
|
||||
init?(folder: URL, voiceprints: VoiceprintStore, baseURL: String, skipTLS: Bool,
|
||||
templates: [RecapTemplate], defaultTemplateId: String) {
|
||||
let speakersURL = folder.appendingPathComponent("speakers.json")
|
||||
guard let data = try? Data(contentsOf: speakersURL),
|
||||
let file = try? JSONDecoder().decode(SpeakersFile.self, from: data),
|
||||
@@ -34,6 +37,9 @@ final class RecapEditModel: ObservableObject {
|
||||
self.voiceprints = voiceprints
|
||||
self.baseURL = baseURL
|
||||
self.skipTLS = skipTLS
|
||||
self.templates = templates.isEmpty ? RecapTemplate.builtIns : templates
|
||||
self.selectedTemplateId = (templates.contains { $0.id == defaultTemplateId } ? defaultTemplateId : templates.first?.id)
|
||||
?? RecapTemplate.builtIns.first!.id
|
||||
self.base = file
|
||||
self.segments = file.segments
|
||||
self.speakers = SpeakerEditing.orderedSpeakers(file.segments)
|
||||
@@ -102,13 +108,14 @@ final class RecapEditModel: ObservableObject {
|
||||
defer { regenerating = false }
|
||||
|
||||
let file = commitCorrections()
|
||||
let template = templates.first { $0.id == selectedTemplateId } ?? templates.first ?? .internalMeeting
|
||||
let llm = GatewayLLMClient(baseURL: baseURL, skipTLS: skipTLS)
|
||||
guard let model = await llm.chatModelId() else {
|
||||
status = "No language model on the gateway — saved corrections only."
|
||||
rebaseline(); return
|
||||
}
|
||||
let analyzer = RecapAnalyzer(llm: llm, model: model)
|
||||
guard let result = try? await analyzer.recap(file: file) else {
|
||||
guard let result = try? await analyzer.recap(file: file, template: template) else {
|
||||
status = "Recap regeneration failed — corrections were saved."
|
||||
rebaseline(); return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user