c539b78a58
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).
102 lines
5.4 KiB
Swift
102 lines
5.4 KiB
Swift
import Foundation
|
|
|
|
/// How a takeaways section is shaped (drives the LLM output shape + rendering).
|
|
enum SectionKind: String, Codable, CaseIterable, Identifiable {
|
|
case items // point + optional who / timestamp / note (decisions, actions, quotes, questions…)
|
|
case bullets // plain short points
|
|
case paragraph // a prose block
|
|
|
|
var id: String { rawValue }
|
|
var label: String {
|
|
switch self {
|
|
case .items: return "Attributed items"
|
|
case .bullets: return "Bulleted list"
|
|
case .paragraph: return "Paragraph"
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One configurable takeaways category in a template.
|
|
struct TemplateSection: Codable, Identifiable, Equatable {
|
|
var id: String
|
|
var title: String
|
|
var kind: SectionKind
|
|
var instruction: String // editable prompt text: what the LLM should extract
|
|
|
|
init(id: String = UUID().uuidString, title: String, kind: SectionKind, instruction: String) {
|
|
self.id = id; self.title = title; self.kind = kind; self.instruction = instruction
|
|
}
|
|
}
|
|
|
|
/// How finely to break the transcript into topic sections.
|
|
enum TopicGranularity: String, Codable, CaseIterable, Identifiable {
|
|
case coarse, auto, fine
|
|
var id: String { rawValue }
|
|
var label: String {
|
|
switch self { case .coarse: return "Fewer, broader topics"; case .auto: return "Automatic"; case .fine: return "More, finer topics" }
|
|
}
|
|
/// Multiplier applied to the auto target-section count.
|
|
var multiplier: Double {
|
|
switch self { case .coarse: return 0.6; case .auto: return 1.0; case .fine: return 1.7 }
|
|
}
|
|
}
|
|
|
|
/// A recap template: the always-on TLDR + an ordered list of takeaways sections.
|
|
/// Fully user-editable in Settings (titles, types, instructions); the analyzer builds
|
|
/// the LLM prompt from it and the renderer renders from it, so categories are data,
|
|
/// not code.
|
|
struct RecapTemplate: Codable, Identifiable, Equatable {
|
|
var id: String
|
|
var name: String
|
|
var includeTLDR: Bool
|
|
var topicGranularity: TopicGranularity
|
|
var sections: [TemplateSection]
|
|
|
|
init(id: String = UUID().uuidString, name: String, includeTLDR: Bool = true,
|
|
topicGranularity: TopicGranularity = .auto, sections: [TemplateSection]) {
|
|
self.id = id; self.name = name; self.includeTLDR = includeTLDR
|
|
self.topicGranularity = topicGranularity; self.sections = sections
|
|
}
|
|
|
|
// MARK: - Built-in defaults (seeded once; all editable thereafter)
|
|
|
|
static var builtIns: [RecapTemplate] { [internalMeeting, oneOnOne, companyCall] }
|
|
|
|
static var internalMeeting: RecapTemplate {
|
|
RecapTemplate(id: "builtin.internal", name: "Internal Meeting", sections: [
|
|
.init(id: "internal.decisions", title: "Decisions", kind: .items,
|
|
instruction: "Things explicitly decided or agreed. Only clear commitments (\"we'll do X\", \"let's go with Y\"), not casual mentions. text = the decision; who = who agreed; when = the timestamp."),
|
|
.init(id: "internal.actions", title: "Action Items", kind: .items,
|
|
instruction: "Explicit ownership (\"I'll send the doc\", \"Matt will follow up\"), not vague \"someone should\". text = the action in imperative form; who = the owner; note = a due date if mentioned; when = the timestamp."),
|
|
.init(id: "internal.questions", title: "Open Questions", kind: .items,
|
|
instruction: "Questions raised that were NOT clearly answered. Skip rhetorical or answered ones. text = the question, self-contained; who = who raised it."),
|
|
.init(id: "internal.quotes", title: "Key Quotes", kind: .items,
|
|
instruction: "3-6 pivotal or insightful statements worth surfacing verbatim. text = the quote (4-30 words); who = the speaker; when = the timestamp; note = why it's notable."),
|
|
])
|
|
}
|
|
|
|
static var oneOnOne: RecapTemplate {
|
|
RecapTemplate(id: "builtin.oneonone", name: "1:1", sections: [
|
|
.init(id: "1on1.takeaways", title: "Key Takeaways", kind: .bullets,
|
|
instruction: "The main points and conclusions from the conversation, as concise bullets."),
|
|
.init(id: "1on1.actions", title: "Action Items", kind: .items,
|
|
instruction: "Anything either person committed to do. text = the action; who = the owner; note = a due date if mentioned."),
|
|
.init(id: "1on1.followups", title: "Follow-ups", kind: .bullets,
|
|
instruction: "Things to revisit or circle back on next time."),
|
|
])
|
|
}
|
|
|
|
static var companyCall: RecapTemplate {
|
|
RecapTemplate(id: "builtin.company", name: "Company / Sales Call", sections: [
|
|
.init(id: "company.takeaways", title: "Key Takeaways", kind: .bullets,
|
|
instruction: "The most important points from the call, as concise bullets."),
|
|
.init(id: "company.needs", title: "Their Asks & Needs", kind: .bullets,
|
|
instruction: "What the other party wants, needs, or is trying to solve."),
|
|
.init(id: "company.objections", title: "Objections & Concerns", kind: .items,
|
|
instruction: "Concerns, hesitations, or objections raised. text = the concern; who = who raised it."),
|
|
.init(id: "company.next", title: "Next Steps", kind: .items,
|
|
instruction: "Agreed next steps. text = the step; who = the owner; note = timing if mentioned; when = the timestamp."),
|
|
])
|
|
}
|
|
}
|