add poi type to feed item shape

This commit is contained in:
2026-01-10 18:50:15 +00:00
parent f463bae19c
commit c13a4f3247
9 changed files with 496 additions and 26 deletions

View File

@@ -162,18 +162,23 @@ final class ContextOrchestrator: NSObject, ObservableObject {
return (true, "initial")
}
let now = Date()
if let last = lastRecomputeAt, now.timeIntervalSince(last) > 15 * 60 {
return (true, "timer_15m")
let speed = max(0, location.speed)
let moving = speed >= 1.0
let maxInterval: TimeInterval = moving ? 5 * 60 : 15 * 60
if let last = lastRecomputeAt, now.timeIntervalSince(last) > maxInterval {
return (true, moving ? "timer_5m_moving" : "timer_15m")
}
if let lastLoc = lastRecomputeLocation {
let dist = location.distance(from: lastLoc)
if dist > 250 {
return (true, "moved_250m")
let distanceThreshold: CLLocationDistance = moving ? 100 : 250
if dist > distanceThreshold {
return (true, moving ? "moved_100m_moving" : "moved_250m")
}
}
if let lastAcc = lastRecomputeAccuracy, location.horizontalAccuracy > 0, lastAcc > 0 {
if lastAcc - location.horizontalAccuracy > 50 {
return (true, "accuracy_improved_50m")
let improvementThreshold: CLLocationAccuracy = moving ? 30 : 50
if lastAcc - location.horizontalAccuracy > improvementThreshold {
return (true, moving ? "accuracy_improved_30m" : "accuracy_improved_50m")
}
}
return (false, "no_trigger")
@@ -217,6 +222,7 @@ final class ContextOrchestrator: NSObject, ObservableObject {
var rightNowCandidates: [HeuristicRanker.Ranked] = []
var calendarItems: [FeedItem] = []
var poiItems: [FeedItem] = []
var weatherNowItem: FeedItem? = nil
var fetchFailed = false
var wxDiagnostics: [String: String] = [:]
@@ -317,8 +323,29 @@ final class ContextOrchestrator: NSObject, ObservableObject {
}
switch poiRes {
case .success:
break
case .success(let pois):
if pois.isEmpty {
logger.info("no points of interests found")
}
for poi in pois.prefix(2) {
let subtitle = poiSubtitle(for: poi)
let confidence = min(max(poi.confidence, 0.0), 1.0)
let item = FeedItem(
id: poi.id,
type: .poiNearby,
title: poi.name.truncated(maxLength: TextConstraints.titleMax),
subtitle: subtitle.truncated(maxLength: TextConstraints.subtitleMax),
priority: confidence,
ttlSec: max(1, poi.ttlSec),
condition: nil,
startsAt: nil,
poiType: poi.poiType,
bucket: .fyi,
actions: ["DISMISS"]
)
poiItems.append(item)
rightNowCandidates.append(.init(item: item, confidence: confidence, isEligibleForRightNow: true))
}
case .failure(let error):
fetchFailed = true
logger.error("poi fetch failed: \(String(describing: error), privacy: .public)")
@@ -364,7 +391,7 @@ final class ContextOrchestrator: NSObject, ObservableObject {
lastWeatherDiagnostics = wxDiagnostics
lastCalendarDiagnostics = calDiagnostics
logger.info("pipeline right_now_candidates=\(rightNowCandidates.count) calendar_items=\(calendarItems.count) fetchFailed=\(fetchFailed) elapsed_ms=\(elapsedMs)")
logger.info("pipeline right_now_candidates=\(rightNowCandidates.count) calendar_items=\(calendarItems.count) poi_items=\(poiItems.count) fetchFailed=\(fetchFailed) elapsed_ms=\(elapsedMs)")
if fetchFailed, rightNowCandidates.isEmpty, calendarItems.isEmpty, weatherNowItem == nil {
let fallbackFeed = store.getFeed(now: nowEpoch)
@@ -399,6 +426,28 @@ final class ContextOrchestrator: NSObject, ObservableObject {
ttlSec: max(1, item.ttlSec),
condition: item.condition,
startsAt: item.startsAt,
poiType: item.poiType,
bucket: .fyi,
actions: ["DISMISS"]
)
})
let fyiPOI = poiItems
.filter { $0.id != winnerItem.id }
.filter { !store.isSuppressed(id: $0.id, type: $0.type, now: nowEpoch) }
.prefix(2)
fyi.append(contentsOf: fyiPOI.map { item in
FeedItem(
id: item.id,
type: item.type,
title: item.title.truncated(maxLength: TextConstraints.titleMax),
subtitle: item.subtitle.truncated(maxLength: TextConstraints.subtitleMax),
priority: min(max(item.priority, 0.0), 1.0),
ttlSec: max(1, item.ttlSec),
condition: item.condition,
startsAt: item.startsAt,
poiType: item.poiType,
bucket: .fyi,
actions: ["DISMISS"]
)
@@ -467,6 +516,22 @@ final class ContextOrchestrator: NSObject, ObservableObject {
meta: FeedMeta(winnerId: base.meta.winnerId, unreadCount: cards.count)
)
}
private func poiSubtitle(for poi: POIDataSource.POI) -> String {
let distance = distanceText(meters: poi.distanceMeters)
let walk = "\(poi.walkingMinutes) min walk"
let parts = [poi.category, distance, walk]
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
return parts.joined(separator: "")
}
private func distanceText(meters: CLLocationDistance) -> String {
if meters >= 1000 {
return String(format: "%.1f km", meters / 1000.0)
}
return "\(Int(meters.rounded())) m"
}
}
extension ContextOrchestrator: CLLocationManagerDelegate {