410 lines
12 KiB
Swift
410 lines
12 KiB
Swift
// Models.swift — LockInBro data models
|
|
|
|
import Foundation
|
|
|
|
// MARK: - Auth
|
|
|
|
struct User: Codable, Identifiable {
|
|
let id: String
|
|
let email: String?
|
|
let displayName: String?
|
|
let createdAt: String
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id, email
|
|
case displayName = "display_name"
|
|
case createdAt = "created_at"
|
|
}
|
|
}
|
|
|
|
struct AuthResponse: Codable {
|
|
let accessToken: String
|
|
let refreshToken: String
|
|
let expiresIn: Int
|
|
let user: User
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case accessToken = "access_token"
|
|
case refreshToken = "refresh_token"
|
|
case expiresIn = "expires_in"
|
|
case user
|
|
}
|
|
}
|
|
|
|
// MARK: - Tasks
|
|
|
|
struct AppTask: Identifiable, Codable, Hashable {
|
|
let id: String
|
|
var title: String
|
|
var description: String?
|
|
var priority: Int
|
|
var status: String
|
|
var deadline: String?
|
|
var estimatedMinutes: Int?
|
|
let source: String
|
|
var tags: [String]
|
|
var planType: String?
|
|
let createdAt: String
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id, title, description, priority, status, deadline, tags, source
|
|
case estimatedMinutes = "estimated_minutes"
|
|
case planType = "plan_type"
|
|
case createdAt = "created_at"
|
|
}
|
|
|
|
var priorityLabel: String {
|
|
switch priority {
|
|
case 4: return "Urgent"
|
|
case 3: return "High"
|
|
case 2: return "Medium"
|
|
case 1: return "Low"
|
|
default: return "Unset"
|
|
}
|
|
}
|
|
|
|
var priorityColor: String {
|
|
switch priority {
|
|
case 4: return "red"
|
|
case 3: return "orange"
|
|
case 2: return "yellow"
|
|
case 1: return "green"
|
|
default: return "gray"
|
|
}
|
|
}
|
|
|
|
var isActive: Bool { status == "in_progress" }
|
|
var isDone: Bool { status == "done" }
|
|
}
|
|
|
|
// MARK: - Steps
|
|
|
|
struct Step: Identifiable, Codable, Hashable {
|
|
let id: String
|
|
let taskId: String
|
|
let sortOrder: Int
|
|
var title: String
|
|
var description: String?
|
|
var estimatedMinutes: Int?
|
|
var status: String
|
|
var checkpointNote: String?
|
|
var lastCheckedAt: String?
|
|
var completedAt: String?
|
|
let createdAt: String
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id, title, description, status
|
|
case taskId = "task_id"
|
|
case sortOrder = "sort_order"
|
|
case estimatedMinutes = "estimated_minutes"
|
|
case checkpointNote = "checkpoint_note"
|
|
case lastCheckedAt = "last_checked_at"
|
|
case completedAt = "completed_at"
|
|
case createdAt = "created_at"
|
|
}
|
|
|
|
var isDone: Bool { status == "done" }
|
|
var isActive: Bool { status == "in_progress" }
|
|
}
|
|
|
|
// MARK: - Focus Session
|
|
|
|
/// Subset of the JSONB checkpoint dict stored on the backend session record.
|
|
/// Populated by argus when it POSTs to /distractions/analyze-result.
|
|
struct SessionCheckpoint: Codable {
|
|
/// Written by POST /distractions/analyze-result (argus live mode).
|
|
let lastVlmSummary: String?
|
|
/// Written by POST /distractions/analyze-screenshot (Swift fallback).
|
|
let lastScreenshotAnalysis: String?
|
|
/// Concise summary of the last completed action.
|
|
let lastActionSummary: String?
|
|
/// Frontmost application name at last checkpoint.
|
|
let activeApp: String?
|
|
/// Running count of distractions logged during this session.
|
|
let distractionCount: Int?
|
|
|
|
/// Returns whichever VLM summary field is populated, preferring the most recent.
|
|
var vlmSummary: String? { lastVlmSummary ?? lastScreenshotAnalysis }
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case lastVlmSummary = "last_vlm_summary"
|
|
case lastScreenshotAnalysis = "last_screenshot_analysis"
|
|
case lastActionSummary = "last_action_summary"
|
|
case activeApp = "active_app"
|
|
case distractionCount = "distraction_count"
|
|
}
|
|
}
|
|
|
|
struct FocusSession: Identifiable, Codable {
|
|
let id: String
|
|
let userId: String
|
|
var taskId: String?
|
|
let platform: String
|
|
let startedAt: String
|
|
var endedAt: String?
|
|
var status: String
|
|
/// Live checkpoint data written by argus (nil when no checkpoint exists yet).
|
|
var checkpoint: SessionCheckpoint?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id, platform, status, checkpoint
|
|
case userId = "user_id"
|
|
case taskId = "task_id"
|
|
case startedAt = "started_at"
|
|
case endedAt = "ended_at"
|
|
}
|
|
}
|
|
|
|
// MARK: - Brain Dump
|
|
|
|
struct BrainDumpResponse: Codable {
|
|
let parsedTasks: [ParsedTask]
|
|
let unparseableFragments: [String]
|
|
let askForPlans: Bool
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case parsedTasks = "parsed_tasks"
|
|
case unparseableFragments = "unparseable_fragments"
|
|
case askForPlans = "ask_for_plans"
|
|
}
|
|
}
|
|
|
|
struct ParsedTask: Codable, Identifiable {
|
|
// local UUID for list identity before saving
|
|
var localId: String = UUID().uuidString
|
|
var id: String { taskId ?? localId }
|
|
|
|
/// Set by backend when the brain-dump endpoint creates the task automatically.
|
|
let taskId: String?
|
|
let title: String
|
|
let description: String?
|
|
let priority: Int
|
|
let deadline: String?
|
|
let estimatedMinutes: Int?
|
|
let tags: [String]
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case taskId = "task_id"
|
|
case title, description, priority, deadline, tags
|
|
case estimatedMinutes = "estimated_minutes"
|
|
}
|
|
}
|
|
|
|
// MARK: - Step Planning
|
|
|
|
struct StepPlanResponse: Codable {
|
|
let taskId: String
|
|
let planType: String
|
|
let steps: [Step]
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case taskId = "task_id"
|
|
case planType = "plan_type"
|
|
case steps
|
|
}
|
|
}
|
|
|
|
// MARK: - Distraction Analysis
|
|
|
|
/// A single action the proactive agent can take on the user's behalf.
|
|
struct ProposedAction: Codable {
|
|
let label: String // e.g. "Extract all 14 events"
|
|
let actionType: String // e.g. "auto_extract", "brain_dump"
|
|
let details: String?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case label, details
|
|
case actionType = "action_type"
|
|
}
|
|
}
|
|
|
|
/// Friction pattern detected by the upgraded Argus VLM prompt.
|
|
struct FrictionInfo: Codable {
|
|
/// repetitive_loop | stalled | tedious_manual | context_overhead | task_resumption | none
|
|
let type: String
|
|
let confidence: Double
|
|
let description: String?
|
|
let proposedActions: [ProposedAction]
|
|
let sourceContext: String?
|
|
let targetContext: String?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case type, confidence, description
|
|
case proposedActions = "proposed_actions"
|
|
case sourceContext = "source_context"
|
|
case targetContext = "target_context"
|
|
}
|
|
|
|
var isActionable: Bool { type != "none" && confidence > 0.5 }
|
|
var isResumption: Bool { type == "task_resumption" }
|
|
}
|
|
|
|
/// Session lifecycle action suggested by the VLM (new argus feature).
|
|
struct SessionAction: Codable {
|
|
/// resume | switch | complete | start_new | none
|
|
let type: String
|
|
let sessionId: String?
|
|
let reason: String?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case type, reason
|
|
case sessionId = "session_id"
|
|
}
|
|
}
|
|
|
|
struct DistractionAnalysisResponse: Codable {
|
|
let onTask: Bool
|
|
let currentStepId: String?
|
|
let inferredTask: String?
|
|
let checkpointNoteUpdate: String?
|
|
let stepsCompleted: [String]
|
|
// Upgraded Argus prompt fields (nil when backend uses legacy prompt)
|
|
let friction: FrictionInfo?
|
|
let sessionAction: SessionAction? // new argus: session lifecycle suggestions
|
|
let intent: String? // skimming | engaged | unclear | null
|
|
let distractionType: String?
|
|
let appName: String?
|
|
let confidence: Double
|
|
let gentleNudge: String?
|
|
let vlmSummary: String?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case onTask = "on_task"
|
|
case currentStepId = "current_step_id"
|
|
case inferredTask = "inferred_task"
|
|
case checkpointNoteUpdate = "checkpoint_note_update"
|
|
case stepsCompleted = "steps_completed"
|
|
case friction, intent
|
|
case sessionAction = "session_action"
|
|
case distractionType = "distraction_type"
|
|
case appName = "app_name"
|
|
case confidence
|
|
case gentleNudge = "gentle_nudge"
|
|
case vlmSummary = "vlm_summary"
|
|
}
|
|
}
|
|
|
|
// MARK: - Session Resume
|
|
|
|
struct ResumeCard: Codable {
|
|
let welcomeBack: String
|
|
let youWereDoing: String
|
|
let nextStep: String
|
|
let motivation: String
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case welcomeBack = "welcome_back"
|
|
case youWereDoing = "you_were_doing"
|
|
case nextStep = "next_step"
|
|
case motivation
|
|
}
|
|
}
|
|
|
|
struct StepSummary: Codable {
|
|
let id: String
|
|
let title: String
|
|
let checkpointNote: String?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id, title
|
|
case checkpointNote = "checkpoint_note"
|
|
}
|
|
}
|
|
|
|
struct ProgressSummary: Codable {
|
|
let completed: Int
|
|
let total: Int
|
|
let attentionScore: Int?
|
|
let distractionCount: Int?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case completed, total
|
|
case attentionScore = "attention_score"
|
|
case distractionCount = "distraction_count"
|
|
}
|
|
}
|
|
|
|
struct ResumeResponse: Codable {
|
|
let sessionId: String
|
|
let resumeCard: ResumeCard
|
|
let currentStep: StepSummary?
|
|
let progress: ProgressSummary?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case sessionId = "session_id"
|
|
case resumeCard = "resume_card"
|
|
case currentStep = "current_step"
|
|
case progress
|
|
}
|
|
}
|
|
|
|
// MARK: - Proactive Agent
|
|
|
|
struct ProactiveCard: Identifiable {
|
|
enum Source {
|
|
/// VLM detected a friction pattern (primary signal — from upgraded Argus prompt).
|
|
case vlmFriction(frictionType: String, description: String?, actions: [ProposedAction])
|
|
/// Heuristic app-switch loop detected by NSWorkspace observer (fallback when VLM hasn't returned friction yet).
|
|
case appSwitchLoop(apps: [String], switchCount: Int)
|
|
/// VLM suggests a session lifecycle action (new argus: resume, switch, complete, start_new).
|
|
case sessionAction(type: String, taskTitle: String, checkpoint: String, reason: String, sessionId: String?)
|
|
}
|
|
|
|
let id = UUID()
|
|
let source: Source
|
|
|
|
/// Human-readable title for the card header.
|
|
var title: String {
|
|
switch source {
|
|
case .vlmFriction(let frictionType, _, _):
|
|
switch frictionType {
|
|
case "repetitive_loop": return "Repetitive Pattern Detected"
|
|
case "stalled": return "Looks Like You're Stuck"
|
|
case "tedious_manual": return "I Can Help With This"
|
|
case "context_overhead": return "Too Many Windows?"
|
|
default: return "I Noticed Something"
|
|
}
|
|
case .appSwitchLoop:
|
|
return "Repetitive Pattern Detected"
|
|
case .sessionAction(let type, let taskTitle, _, _, _):
|
|
switch type {
|
|
case "resume": return "Resume: \(taskTitle)"
|
|
case "switch": return "Switch to: \(taskTitle)"
|
|
case "complete": return "Done with \(taskTitle)?"
|
|
case "start_new": return "Start a Focus Session?"
|
|
default: return "Session Suggestion"
|
|
}
|
|
}
|
|
}
|
|
|
|
/// SF Symbol name for the card icon.
|
|
var icon: String {
|
|
switch source {
|
|
case .vlmFriction(let frictionType, _, _):
|
|
switch frictionType {
|
|
case "repetitive_loop": return "arrow.triangle.2.circlepath"
|
|
case "stalled": return "pause.circle"
|
|
case "tedious_manual": return "wand.and.stars"
|
|
case "context_overhead": return "macwindow"
|
|
default: return "sparkles"
|
|
}
|
|
case .appSwitchLoop:
|
|
return "arrow.triangle.2.circlepath"
|
|
case .sessionAction(let type, _, _, _, _):
|
|
switch type {
|
|
case "resume": return "arrow.counterclockwise.circle"
|
|
case "switch": return "arrow.left.arrow.right"
|
|
case "complete": return "checkmark.circle"
|
|
case "start_new": return "plus.circle"
|
|
default: return "circle"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - API Error
|
|
|
|
struct APIErrorResponse: Codable {
|
|
let detail: String
|
|
}
|