@@ -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
229314private extension Array where Element == String {
0 commit comments