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:
@@ -34,20 +34,20 @@ final class SpeakerEditingTests: XCTestCase {
|
||||
func testRemapStructuredAndWordBoundaryText() {
|
||||
let result = RecapResult(
|
||||
sections: [TopicSection(title: "Grant intro", summary: "Grant and Unknown_0 talk; Grantham stays.", startIndex: 0, endIndex: 1)],
|
||||
extras: MeetingExtras(
|
||||
tldr: .init(summary: "Grant led.", primarySpeakers: ["Grant"]),
|
||||
decisions: [.init(statement: "ship", agreedBy: ["Grant", "Unknown_0"], supportingOffset: 1)],
|
||||
actionItems: [.init(description: "Unknown_0 sends doc", owner: "Unknown_0", dueHint: nil, supportingOffset: nil)],
|
||||
openQuestions: [],
|
||||
keyQuotes: [.init(speaker: "Unknown_0", offset: 2, quote: "go", whyNotable: "")]))
|
||||
extras: RecapExtras(tldr: "Grant led.", primarySpeakers: ["Grant"], sections: [
|
||||
RenderedSection(title: "Decisions", kind: .items,
|
||||
items: [RecapItem(text: "Unknown_0 sends doc", who: "Unknown_0", when: 1, note: nil)]),
|
||||
RenderedSection(title: "Takeaways", kind: .bullets, bullets: ["Unknown_0 and Grant agree"]),
|
||||
]))
|
||||
let map = ["Unknown_0": "Caitlyn", "Grant": "Grant Gilliam"]
|
||||
let out = SpeakerEditing.remap(result, names: map)
|
||||
XCTAssertEqual(out.sections[0].title, "Grant Gilliam intro")
|
||||
XCTAssertEqual(out.sections[0].summary, "Grant Gilliam and Caitlyn talk; Grantham stays.") // word boundary keeps "Grantham"
|
||||
XCTAssertEqual(out.extras?.tldr.primarySpeakers, ["Grant Gilliam"])
|
||||
XCTAssertEqual(out.extras?.decisions.first?.agreedBy, ["Grant Gilliam", "Caitlyn"])
|
||||
XCTAssertEqual(out.extras?.actionItems.first?.owner, "Caitlyn")
|
||||
XCTAssertEqual(out.extras?.keyQuotes.first?.speaker, "Caitlyn")
|
||||
XCTAssertEqual(out.extras?.tldr, "Grant Gilliam led.")
|
||||
XCTAssertEqual(out.extras?.primarySpeakers, ["Grant Gilliam"])
|
||||
XCTAssertEqual(out.extras?.sections[0].items.first?.who, "Caitlyn")
|
||||
XCTAssertEqual(out.extras?.sections[0].items.first?.text, "Caitlyn sends doc")
|
||||
XCTAssertEqual(out.extras?.sections[1].bullets, ["Caitlyn and Grant Gilliam agree"])
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -69,7 +69,9 @@ final class SpeakerEditingTests: XCTestCase {
|
||||
|
||||
let store = VoiceprintStore(fileURL: dir.appendingPathComponent("voiceprints.json"))
|
||||
let model = try XCTUnwrap(RecapEditModel(folder: dir, voiceprints: store,
|
||||
baseURL: "https://localhost:1", skipTLS: true))
|
||||
baseURL: "https://localhost:1", skipTLS: true,
|
||||
templates: RecapTemplate.builtIns,
|
||||
defaultTemplateId: RecapTemplate.builtIns.first!.id))
|
||||
model.rename("Unknown_0", to: "Caitlyn")
|
||||
XCTAssertTrue(model.speakers.contains("Caitlyn"))
|
||||
XCTAssertFalse(model.speakers.contains("Unknown_0"))
|
||||
|
||||
Reference in New Issue
Block a user