Skip to content

Commit 6e56feb

Browse files
author
ComputelessComputer
committed
Make fallback summaries read like daily notes
Turn the heuristic journal formatter into section-based narrative notes with time-plus-activity headings so the Today view stays useful without an LLM.
1 parent 007c213 commit 6e56feb

2 files changed

Lines changed: 90 additions & 4 deletions

File tree

Sources/OpenbirdKit/Services/JournalGenerator.swift

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,12 @@ public actor JournalGenerator {
171171
return "No activity captured yet for \(OpenbirdDateFormatting.weekdayFormatter.string(from: date))."
172172
}
173173

174+
let eventsByID = Dictionary(uniqueKeysWithValues: events.map { ($0.id, $0) })
174175
let appCount = Set(events.map(\.appName)).count
175-
var markdown = "Captured \(sections.count) focus block\(sections.count == 1 ? "" : "s") across \(appCount) app\(appCount == 1 ? "" : "s") on \(OpenbirdDateFormatting.weekdayFormatter.string(from: date)).\n\n"
176+
var markdown = "Stitched together from your local activity logs: \(sections.count) section\(sections.count == 1 ? "" : "s") across \(appCount) app\(appCount == 1 ? "" : "s") on \(OpenbirdDateFormatting.weekdayFormatter.string(from: date)).\n\n"
176177
for section in sections {
177-
markdown += "## \(section.timeRange)\(section.heading)\n\n"
178-
markdown += section.bullets.map { "- \($0)" }.joined(separator: "\n")
178+
markdown += "## \(sectionHeading(for: section, eventsByID: eventsByID))\n\n"
179+
markdown += sectionNarrative(for: section, eventsByID: eventsByID)
179180
markdown += "\n\n"
180181
}
181182
return markdown.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -224,6 +225,90 @@ public actor JournalGenerator {
224225

225226
return summary.count > 80 ? String(summary.prefix(80)) + "" : summary
226227
}
228+
229+
private func sectionHeading(
230+
for section: JournalSection,
231+
eventsByID: [String: ActivityEvent]
232+
) -> String {
233+
"\(section.timeRange) - \(displayTopic(for: section, eventsByID: eventsByID))"
234+
}
235+
236+
private func sectionNarrative(
237+
for section: JournalSection,
238+
eventsByID: [String: ActivityEvent]
239+
) -> String {
240+
let events = section.sourceEventIDs.compactMap { eventsByID[$0] }
241+
let apps = Array(events.map(\.appName).deduplicatedByNormalizedText())
242+
let topic = displayTopic(for: section, eventsByID: eventsByID)
243+
let topicKey = topic.normalizedComparisonKey
244+
let appKeys = Set(apps.map(\.normalizedComparisonKey))
245+
246+
var sentences: [String] = []
247+
if appKeys.contains(topicKey) {
248+
sentences.append("Spent this block in \(naturalLanguageList(apps)).")
249+
} else if apps.isEmpty {
250+
sentences.append("Spent this block on \(topic).")
251+
} else {
252+
sentences.append("Spent this block on \(topic) in \(naturalLanguageList(apps)).")
253+
}
254+
255+
let highlights = sectionHighlights(for: section, events: events)
256+
if highlights.isEmpty == false {
257+
sentences.append("Main notes: \(highlights.joined(separator: "; ")).")
258+
}
259+
260+
return sentences.joined(separator: " ")
261+
}
262+
263+
private func displayTopic(
264+
for section: JournalSection,
265+
eventsByID: [String: ActivityEvent]
266+
) -> String {
267+
let topic = section.heading.trimmingCharacters(in: .whitespacesAndNewlines)
268+
guard topic.isEmpty == false else {
269+
return section.sourceEventIDs
270+
.compactMap { eventsByID[$0]?.appName }
271+
.first ?? "Activity"
272+
}
273+
274+
return topic
275+
}
276+
277+
private func sectionHighlights(
278+
for section: JournalSection,
279+
events: [ActivityEvent]
280+
) -> [String] {
281+
let excluded = Set(
282+
([section.heading] + events.map(\.appName))
283+
.map(\.normalizedComparisonKey)
284+
)
285+
286+
var pieces: [String] = []
287+
for bullet in section.bullets {
288+
for segment in bullet.split(separator: "") {
289+
let piece = segment.trimmingCharacters(in: .whitespacesAndNewlines)
290+
guard piece.isEmpty == false else { continue }
291+
guard excluded.contains(piece.normalizedComparisonKey) == false else { continue }
292+
pieces.append(piece)
293+
}
294+
}
295+
296+
return Array(pieces.deduplicatedByNormalizedText().prefix(3))
297+
}
298+
299+
private func naturalLanguageList(_ values: [String]) -> String {
300+
switch values.count {
301+
case 0:
302+
return "activity"
303+
case 1:
304+
return values[0]
305+
case 2:
306+
return "\(values[0]) and \(values[1])"
307+
default:
308+
let prefix = values.dropLast().joined(separator: ", ")
309+
return "\(prefix), and \(values[values.count - 1])"
310+
}
311+
}
227312
}
228313

229314
private extension Array where Element == String {

Tests/OpenbirdKitTests/JournalGeneratorTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ struct JournalGeneratorTests {
4848
)
4949

5050
#expect(journal.sections.count >= 1)
51-
#expect(journal.markdown.contains("Captured 2 focus blocks across 2 apps"))
51+
#expect(journal.markdown.contains("Stitched together from your local activity logs"))
5252
#expect(journal.markdown.contains("## 9:00"))
53+
#expect(journal.markdown.contains("Spent this block"))
5354
}
5455

5556
@Test func prefersSpecificHeadingsAndDeduplicatedBullets() async throws {

0 commit comments

Comments
 (0)