- fix(web): cast window through unknown in platform-sync.ts (TS2352) - docs: add AGENTS.md, README.md, CLAUDE.md, .windsurfrules, .cursorrules, env.example - feat(ios): port Recurrence.swift from web/src/lib/recurrence.ts - feat(ios): port NLParser.swift from web/src/lib/nl-parser.ts - feat(ios): port ContextMessages.swift from web/src/lib/context-messages.ts - feat(ios): add CMRoutine model + Routines.swift engine with state machine + templates - feat(ios): add RoutineListView, RoutineRunnerView, RoutineEditorView - feat(android): add RoutineScreen.kt with list, runner, templates, step controls Web: 373 tests passing, build succeeds with --webpack flag
177 lines
5.9 KiB
Swift
177 lines
5.9 KiB
Swift
// ── Contextual Pre-Warning Messages ───────────────────────────
|
|
// Keyword → helpful prep message mapping. Expandable, no LLM needed.
|
|
// Used in notification body and on timeline to give actionable context.
|
|
// Ported from web/src/lib/context-messages.ts
|
|
|
|
import Foundation
|
|
|
|
// MARK: - Context Rule
|
|
|
|
struct ContextRule {
|
|
let keywords: [String]
|
|
let messages: [String]
|
|
}
|
|
|
|
// MARK: - Rules Engine
|
|
|
|
private let contextRules: [ContextRule] = [
|
|
// Meetings & calls
|
|
ContextRule(
|
|
keywords: ["meeting", "standup", "sync", "huddle", "scrum", "1:1", "one-on-one"],
|
|
messages: ["Review your agenda", "Check meeting notes", "Prepare talking points"]
|
|
),
|
|
ContextRule(
|
|
keywords: ["call", "phone", "dial"],
|
|
messages: ["Have the number ready", "Review call notes", "Find a quiet spot"]
|
|
),
|
|
ContextRule(
|
|
keywords: ["interview", "screening"],
|
|
messages: ["Review the job description", "Prepare your questions", "Test your camera and mic"]
|
|
),
|
|
ContextRule(
|
|
keywords: ["presentation", "demo", "pitch"],
|
|
messages: ["Run through your slides", "Check your screen sharing", "Have backup ready"]
|
|
),
|
|
|
|
// Travel & transport
|
|
ContextRule(
|
|
keywords: ["flight", "plane", "airport"],
|
|
messages: ["Check in online", "Verify gate number", "Pack your carry-on"]
|
|
),
|
|
ContextRule(
|
|
keywords: ["train", "bus", "subway", "metro"],
|
|
messages: ["Check for delays", "Have your ticket ready"]
|
|
),
|
|
ContextRule(
|
|
keywords: ["drive", "commute", "carpool", "uber", "lyft", "taxi", "cab"],
|
|
messages: ["Check traffic conditions", "Grab your keys"]
|
|
),
|
|
ContextRule(
|
|
keywords: ["pickup", "pick up", "drop off", "dropoff"],
|
|
messages: ["Confirm the location", "Check for any updates"]
|
|
),
|
|
|
|
// Health & wellness
|
|
ContextRule(
|
|
keywords: ["doctor", "dentist", "therapist", "appointment", "clinic", "hospital"],
|
|
messages: ["Bring your insurance card", "Leave with travel buffer", "Note any symptoms to mention"]
|
|
),
|
|
ContextRule(
|
|
keywords: ["medicine", "medication", "pill", "vitamin"],
|
|
messages: ["Take with water", "Check dosage"]
|
|
),
|
|
ContextRule(
|
|
keywords: ["workout", "exercise", "gym", "run", "yoga", "stretch"],
|
|
messages: ["Hydrate beforehand", "Change into workout clothes", "Warm up first"]
|
|
),
|
|
|
|
// Food & cooking
|
|
ContextRule(
|
|
keywords: ["cook", "cooking", "bake", "baking", "oven", "recipe"],
|
|
messages: ["Preheat the oven", "Gather your ingredients", "Check you have everything"]
|
|
),
|
|
ContextRule(
|
|
keywords: ["pasta", "noodle", "rice", "boil"],
|
|
messages: ["Start boiling water", "Salt the water"]
|
|
),
|
|
ContextRule(
|
|
keywords: ["dinner", "lunch", "breakfast", "meal", "eat"],
|
|
messages: ["Start prepping ingredients", "Set the table"]
|
|
),
|
|
ContextRule(
|
|
keywords: ["laundry", "washer", "dryer", "clothes"],
|
|
messages: ["Move clothes to dryer", "Check pockets first"]
|
|
),
|
|
|
|
// Work & productivity
|
|
ContextRule(
|
|
keywords: ["deadline", "due", "submit", "submission"],
|
|
messages: ["Final review before submitting", "Double-check requirements"]
|
|
),
|
|
ContextRule(
|
|
keywords: ["class", "lecture", "lesson", "school", "study"],
|
|
messages: ["Pack your materials", "Review last session notes"]
|
|
),
|
|
ContextRule(
|
|
keywords: ["focus", "deep work", "concentrate"],
|
|
messages: ["Close unnecessary tabs", "Put phone on silent", "Grab water"]
|
|
),
|
|
|
|
// Personal
|
|
ContextRule(
|
|
keywords: ["birthday", "anniversary", "celebration", "party"],
|
|
messages: ["Check if the gift is ready", "Confirm the plan"]
|
|
),
|
|
ContextRule(
|
|
keywords: ["walk", "dog", "pet"],
|
|
messages: ["Grab the leash", "Bring waste bags"]
|
|
),
|
|
ContextRule(
|
|
keywords: ["sleep", "bed", "bedtime", "wind down", "rest"],
|
|
messages: ["Start winding down", "Put away screens", "Set tomorrow's alarm"]
|
|
),
|
|
ContextRule(
|
|
keywords: ["wake", "morning", "alarm"],
|
|
messages: ["Time to get up!", "Stretch and hydrate"]
|
|
),
|
|
]
|
|
|
|
// MARK: - Lookup
|
|
|
|
/// Get a contextual message for a timer label.
|
|
/// Returns the first matching message, or nil if no match.
|
|
func getContextMessage(label: String) -> String? {
|
|
let lower = label.lowercased()
|
|
for rule in contextRules {
|
|
if rule.keywords.contains(where: { lower.contains($0) }) {
|
|
return rule.messages.first
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
/// Get all matching contextual messages for a timer label.
|
|
/// Returns an array of messages from all matching rules.
|
|
func getAllContextMessages(label: String) -> [String] {
|
|
let lower = label.lowercased()
|
|
var messages: [String] = []
|
|
for rule in contextRules {
|
|
if rule.keywords.contains(where: { lower.contains($0) }) {
|
|
if let first = rule.messages.first {
|
|
messages.append(first)
|
|
}
|
|
}
|
|
}
|
|
return messages
|
|
}
|
|
|
|
/// Get a contextual message formatted for a pre-warning notification.
|
|
/// Includes the time remaining context.
|
|
func getWarningMessage(label: String, minutesBefore: Int) -> String {
|
|
let contextMsg = getContextMessage(label: label)
|
|
let timeStr: String
|
|
if minutesBefore >= 60 {
|
|
let hours = minutesBefore / 60
|
|
let mins = minutesBefore % 60
|
|
timeStr = mins > 0 ? "\(hours)h \(mins)m" : "\(hours)h"
|
|
} else {
|
|
timeStr = "\(minutesBefore)m"
|
|
}
|
|
|
|
let base = "\(label) in \(timeStr)"
|
|
if let msg = contextMsg {
|
|
return "\(base) — \(msg)"
|
|
}
|
|
return base
|
|
}
|
|
|
|
/// Check if a label matches any context rule.
|
|
func hasContextMatch(label: String) -> Bool {
|
|
return getContextMessage(label: label) != nil
|
|
}
|
|
|
|
/// Get all registered context rules (for UI display / editing).
|
|
func getContextRules() -> [ContextRule] {
|
|
return contextRules
|
|
}
|