cocottetech/@platform/codebase/@features/ai-copilot/cockpit-kit/Sources/CocotteCockpitKit/Models.swift
Natalie 66a79ce47f feat(@projects/@cocottetech): enhance asset loading with native image support
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-08 05:08:49 -07:00

281 lines
13 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
// Mock domain model for the social-media cockpit. Mirrors the design specs:
// content-drop-composer (drop + legs), approval-card, fleet-roster, content-social
// contract (9 surfaces + OF/Fansly anchors), and the 1-wk-offset schedule.
// MARK: - Surfaces (content-social contract + OnlyFans)
enum Surface: String, CaseIterable, Identifiable {
case onlyfans, fansly, x, instagram, threads, bluesky, reddit, blog
var id: String { rawValue }
var label: String {
switch self {
case .onlyfans: return "OnlyFans"
case .fansly: return "Fansly"
case .x: return "X"
case .instagram: return "Instagram"
case .threads: return "Threads"
case .bluesky: return "Bluesky"
case .reddit: return "Reddit"
case .blog: return "Blog"
}
}
var glyph: String {
switch self {
case .onlyfans: return "OF"
case .fansly: return "Fa"
case .x: return "𝕏"
case .instagram: return "IG"
case .threads: return "Th"
case .bluesky: return "Bs"
case .reddit: return "Re"
case .blog: return ""
}
}
var isNSFWAnchor: Bool { self == .onlyfans || self == .fansly }
}
enum LegRole: String { case anchor, teaser, longform }
enum TosStatus: String { case ok, flagged, blocked }
struct DropLeg: Identifiable {
let id = UUID()
let surface: Surface
let role: LegRole
let caption: String
let linkTarget: String?
let ppvPrice: Double?
let tos: TosStatus
}
enum DropState: String {
case clustering, arcDraft = "arc-draft", derived, scheduled, dispatched, partial, done
}
public struct ContentDrop: Identifiable {
public let id = UUID()
let title: String
let arc: String
let assetCount: Int
let clusterSource: String
let legs: [DropLeg]
let teaseAt: Date?
let dropAt: Date
let followupAt: Date?
let state: DropState
public var displayTitle: String { title }
}
// MARK: - Approval card (supervised autonomy centerpiece)
enum Stakes: String { case low, medium, high }
public struct PendingApproval: Identifiable {
public let id = UUID()
// Backend content_posts id the approve / set-aside endpoints target this row.
// nil for mock/composed items (no server row to mutate). `var` so it stays in
// the memberwise init with a default (a `let` default is omitted from it).
var postId: UUID? = nil
let title: String
let surfaces: [Surface]
let kind: String // "content-post"
let stakes: Stakes
let confidence: Double
let why: String
let legPreviews: [(surface: Surface, caption: String, tos: TosStatus)]
public var displayTitle: String { title }
public var primaryCaption: String { legPreviews.first?.caption ?? "" }
}
// Transient receipt shown after approving supports a brief undo window.
public struct ApprovalReceipt: Identifiable {
public let id = UUID()
let title: String
let edited: Bool
let restored: PendingApproval
}
// MARK: - Fleet
enum SpecialistStatus: String {
case auto, idle, degraded, draftOnly = "draft-only", retired
var dot: String {
switch self {
case .auto: return ""; case .idle: return ""; case .degraded: return ""
case .draftOnly: return ""; case .retired: return ""
}
}
var group: String {
switch self {
case .auto: return "Working now"; case .idle: return "Idle"
case .degraded: return "Degraded"; case .draftOnly: return "Draft-only"; case .retired: return "Retired"
}
}
}
public struct Specialist: Identifiable {
public let id = UUID()
let name: String
let status: SpecialistStatus
let trust: Double?
let countToday: Int
let countLabel: String // "today" | "drafts"
let note: String?
public var displayName: String { name }
}
// MARK: - Agent actions ticker
public struct AgentAction: Identifiable {
public let id = UUID()
let time: String
let surface: Surface?
let summary: String
let ok: Bool
}
// MARK: - Asset library (the photo pool classified newest-first)
public enum ContentClass: String, CaseIterable {
case explicit, suggestive, sfw, bts // bts = behind-the-scenes
var label: String { self == .bts ? "BTS" : rawValue.capitalized }
}
public struct Asset: Identifiable {
public let id: UUID
/// Platform `content_assets.id` nil for mock/composed assets. Drives the
/// authenticated image fetch; also the STABLE identity so the live poll
/// (which re-decodes the list every few seconds) doesn't churn `ForEach`
/// identity and re-flash every thumbnail.
let assetId: UUID?
let label: String
let contentClass: ContentClass
let isExplicit: Bool
let qualityScore: Double // 01 (AI quality rank)
let capturedAt: Date
let hue: Double // placeholder swatch hue (shown until/unless the image loads)
let scheduled: Bool
init(assetId: UUID? = nil, label: String, contentClass: ContentClass, isExplicit: Bool,
qualityScore: Double, capturedAt: Date, hue: Double, scheduled: Bool) {
self.id = assetId ?? UUID()
self.assetId = assetId
self.label = label
self.contentClass = contentClass
self.isExplicit = isExplicit
self.qualityScore = qualityScore
self.capturedAt = capturedAt
self.hue = hue
self.scheduled = scheduled
}
}
// MARK: - Analytics (per-surface performance)
public struct SurfaceMetric: Identifiable {
public let id = UUID()
let surface: Surface
let posts: Int
let impressions: Int
let engagementPct: Double
let conversions: Int // store unlocks / new subs
}
// MARK: - Mock data
enum Mock {
static func at(_ h: Int, _ m: Int, day: Int = 0) -> Date {
// Deterministic dates (Date.now is unavailable in this build context anyway).
var c = DateComponents(); c.year = 2026; c.month = 6; c.day = 8 + day; c.hour = h; c.minute = m
return Calendar(identifier: .gregorian).date(from: c) ?? Date(timeIntervalSince1970: 0)
}
static let specialists: [Specialist] = [
Specialist(name: "content-onlyfans", status: .auto, trust: 0.85, countToday: 3, countLabel: "today", note: nil),
Specialist(name: "publisher", status: .auto, trust: 0.88, countToday: 11, countLabel: "today", note: nil),
Specialist(name: "producer", status: .idle, trust: 0.74, countToday: 0, countLabel: "today", note: nil),
Specialist(name: "strategist", status: .idle, trust: 0.78, countToday: 0, countLabel: "today", note: nil),
Specialist(name: "content-x", status: .degraded, trust: 0.71, countToday: 2, countLabel: "today",
note: "rate-limited by X — retrying in 14m"),
Specialist(name: "content-social", status: .draftOnly, trust: 0.58, countToday: 8, countLabel: "drafts", note: nil),
]
static let drops: [ContentDrop] = [
ContentDrop(
title: "Red Dress, Berlin",
arc: "A slow unwind after the Berlin tour — the dress comes off one frame at a time. Teases land mid-week, the full set drops Friday, a soft follow-up keeps the thread warm into the weekend.",
assetCount: 14, clusterSource: "shoot · Jun 3",
legs: [
DropLeg(surface: .onlyfans, role: .anchor, caption: "the full Berlin set — 14 frames, unlocked 🤍", linkTarget: nil, ppvPrice: 18, tos: .ok),
DropLeg(surface: .fansly, role: .anchor, caption: "Berlin, all of it. 14 new.", linkTarget: nil, ppvPrice: 16, tos: .ok),
DropLeg(surface: .x, role: .teaser, caption: "berlin did something to me 🌹 full set on the storefront", linkTarget: "quinn.link/store", ppvPrice: nil, tos: .ok),
DropLeg(surface: .instagram, role: .teaser, caption: "red dress diaries — link in bio", linkTarget: "quinn.link/store", ppvPrice: nil, tos: .flagged),
DropLeg(surface: .blog, role: .longform, caption: "Red Dress, Berlin — a tour diary", linkTarget: nil, ppvPrice: nil, tos: .ok),
],
teaseAt: at(11, 0, day: 0), dropAt: at(20, 0, day: 2), followupAt: at(13, 0, day: 4),
state: .scheduled),
ContentDrop(
title: "Sunday Slow Morning",
arc: "Soft, backlit, low-stakes. Backfills the gap between hot drops — stocked content riding the offset.",
assetCount: 8, clusterSource: "stocked · May",
legs: [
DropLeg(surface: .onlyfans, role: .anchor, caption: "lazy sunday, just us 🤍", linkTarget: nil, ppvPrice: nil, tos: .ok),
DropLeg(surface: .x, role: .teaser, caption: "coffee, sheets, nowhere to be ☕", linkTarget: "quinn.link/store", ppvPrice: nil, tos: .ok),
DropLeg(surface: .bluesky, role: .teaser, caption: "slow morning energy", linkTarget: "quinn.link/store", ppvPrice: nil, tos: .ok),
],
teaseAt: at(10, 0, day: 5), dropAt: at(19, 0, day: 6), followupAt: nil,
state: .scheduled),
]
static let pending: [PendingApproval] = [
PendingApproval(
title: "Berlin teaser → X + Instagram",
surfaces: [.x, .instagram],
kind: "content-post", stakes: .medium, confidence: 0.78,
why: "IG leg flagged: caption links to storefront, not of.com (K3b ok). Explicit frame routed to OF only.",
legPreviews: [
(.x, "berlin did something to me 🌹 full set on the storefront", .ok),
(.instagram, "red dress diaries — link in bio", .flagged),
]),
PendingApproval(
title: "Reddit cross-post → r/ subs",
surfaces: [.reddit],
kind: "content-post", stakes: .high, confidence: 0.61,
why: "Per-subreddit rules vary (K3a-4). Draft-only until you pick subs.",
legPreviews: [(.reddit, "[OC] Berlin set — verification in comments", .flagged)]),
]
static let actions: [AgentAction] = [
AgentAction(time: "11:02", surface: .x, summary: "teaser posted · Berlin", ok: true),
AgentAction(time: "10:58", surface: .onlyfans, summary: "drop scheduled 20:00 Fri", ok: true),
AgentAction(time: "10:41", surface: .bluesky, summary: "cross-post ok", ok: true),
AgentAction(time: "10:30", surface: .instagram, summary: "draft held — needs approval", ok: false),
AgentAction(time: "09:52", surface: .fansly, summary: "anchor scheduled", ok: true),
AgentAction(time: "09:14", surface: .x, summary: "rate-limited — retry queued", ok: false),
AgentAction(time: "08:40", surface: .blog, summary: "longform drafted", ok: true),
]
static let assets: [Asset] = [
Asset(label: "berlin_0142", contentClass: .explicit, isExplicit: true, qualityScore: 0.94, capturedAt: at(18, 2, day: -3), hue: 0.96, scheduled: true),
Asset(label: "berlin_0138", contentClass: .suggestive, isExplicit: false, qualityScore: 0.88, capturedAt: at(18, 1, day: -3), hue: 0.93, scheduled: true),
Asset(label: "berlin_0131", contentClass: .explicit, isExplicit: true, qualityScore: 0.91, capturedAt: at(17, 58, day: -3), hue: 0.98, scheduled: false),
Asset(label: "berlin_0120", contentClass: .sfw, isExplicit: false, qualityScore: 0.79, capturedAt: at(17, 50, day: -3), hue: 0.90, scheduled: false),
Asset(label: "studio_0904", contentClass: .suggestive, isExplicit: false, qualityScore: 0.83, capturedAt: at(14, 12, day: -9), hue: 0.02, scheduled: false),
Asset(label: "studio_0888", contentClass: .bts, isExplicit: false, qualityScore: 0.61, capturedAt: at(14, 6, day: -9), hue: 0.08, scheduled: false),
Asset(label: "sunday_0451", contentClass: .suggestive, isExplicit: false, qualityScore: 0.86, capturedAt: at(9, 30, day: -16), hue: 0.05, scheduled: true),
Asset(label: "sunday_0440", contentClass: .sfw, isExplicit: false, qualityScore: 0.72, capturedAt: at(9, 22, day: -16), hue: 0.94, scheduled: false),
]
static let metrics: [SurfaceMetric] = [
SurfaceMetric(surface: .onlyfans, posts: 6, impressions: 4200, engagementPct: 0.31, conversions: 38),
SurfaceMetric(surface: .fansly, posts: 4, impressions: 1800, engagementPct: 0.27, conversions: 12),
SurfaceMetric(surface: .x, posts: 14, impressions: 52000, engagementPct: 0.041, conversions: 21),
SurfaceMetric(surface: .instagram, posts: 3, impressions: 9800, engagementPct: 0.062, conversions: 4),
SurfaceMetric(surface: .bluesky, posts: 7, impressions: 6400, engagementPct: 0.055, conversions: 6),
SurfaceMetric(surface: .reddit, posts: 2, impressions: 14000, engagementPct: 0.088, conversions: 9),
]
}