✨ feat(cockpit-kit): 📸 add bump screenshot overlay
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
2bfb0e080d
commit
d114d9d375
7 changed files with 83 additions and 21 deletions
|
|
@ -11,6 +11,9 @@ public struct ActivityView: View {
|
|||
public var body: some View {
|
||||
Scroll {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if let shot = model.bumpScreenshot, let img = decodeImage(shot) {
|
||||
BumpScreenshotCard(image: img).padding(.bottom, t.s4)
|
||||
}
|
||||
HStack {
|
||||
SectionLabel(text: "Agent actions · live")
|
||||
Spacer()
|
||||
|
|
@ -41,3 +44,29 @@ public struct ActivityView: View {
|
|||
.background(t.bg)
|
||||
}
|
||||
}
|
||||
|
||||
/// The latest availability-bump overlay screenshot — the visual "here's exactly
|
||||
/// what the automation focused on" record captured by the Tryst driver each bump.
|
||||
private struct BumpScreenshotCard: View {
|
||||
@Environment(\.tokens) private var t
|
||||
let image: Image
|
||||
|
||||
var body: some View {
|
||||
Card {
|
||||
VStack(alignment: .leading, spacing: t.s2) {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "cursorarrow.rays").font(.system(size: 12, weight: .bold))
|
||||
.foregroundStyle(t.accent)
|
||||
SectionLabel(text: "Latest Tryst bump")
|
||||
Spacer()
|
||||
}
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: .infinity)
|
||||
.clipShape(RoundedRectangle(cornerRadius: t.radiusSm))
|
||||
.overlay(RoundedRectangle(cornerRadius: t.radiusSm).strokeBorder(t.line2, lineWidth: 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,10 @@
|
|||
import SwiftUI
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#elseif canImport(AppKit)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
// Asset library — the photo pool, newest-first, AI-classified. Real thumbnails
|
||||
// stream from platform.api's authenticated image proxy; a class-tinted swatch
|
||||
// stands in until each loads (and stays for mock, which has no backing media).
|
||||
// Shows content-class + quality + whether it's already scheduled into a drop.
|
||||
|
||||
/// Decode image bytes to a SwiftUI `Image` on whichever Apple UI framework is
|
||||
/// present. NSImage/UIImage decode HEIC natively, so iPhone-original HEICs render
|
||||
/// without a conversion step.
|
||||
private func decodeImage(_ data: Data) -> Image? {
|
||||
#if canImport(UIKit)
|
||||
guard let ui = UIImage(data: data) else { return nil }
|
||||
return Image(uiImage: ui)
|
||||
#elseif canImport(AppKit)
|
||||
guard let ns = NSImage(data: data) else { return nil }
|
||||
return Image(nsImage: ns)
|
||||
#else
|
||||
return nil
|
||||
#endif
|
||||
}
|
||||
// `decodeImage` is shared from Components.swift.
|
||||
|
||||
public struct AssetLibraryView: View {
|
||||
@Environment(\.tokens) private var t
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ public protocol CockpitAPI: Sendable {
|
|||
/// nil when the asset has no backing media (mock/composed) or the fetch fails —
|
||||
/// the tile falls back to its placeholder swatch, never errors.
|
||||
func fetchImageData(for asset: Asset) async -> Data?
|
||||
/// Best-effort fetch of the newest availability-bump overlay screenshot for a
|
||||
/// surface (authenticated proxy). Returns nil when none has been captured yet
|
||||
/// or the fetch fails — the card simply doesn't render, never errors.
|
||||
func fetchBumpScreenshot(surface: String) async -> Data?
|
||||
}
|
||||
|
||||
public enum CockpitAPIError: Error, Sendable {
|
||||
|
|
@ -58,4 +62,6 @@ public struct MockCockpitAPI: CockpitAPI {
|
|||
}
|
||||
// No real media in the demo dataset — tiles render their placeholder swatch.
|
||||
public func fetchImageData(for asset: Asset) async -> Data? { nil }
|
||||
// No live browser in the demo — the bump screenshot card stays hidden.
|
||||
public func fetchBumpScreenshot(surface: String) async -> Data? { nil }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ public final class CockpitModel {
|
|||
public private(set) var receipts: [ApprovalReceipt] = []
|
||||
/// Ingestion control + progress (nil until first loaded from the API).
|
||||
public private(set) var ingest: IngestStatus?
|
||||
/// Newest availability-bump overlay screenshot (PNG bytes); nil until one is
|
||||
/// captured — the durable "what the automation focused on" record.
|
||||
public private(set) var bumpScreenshot: Data?
|
||||
public private(set) var connection: ConnectionState = .idle
|
||||
|
||||
private let api: CockpitAPI
|
||||
|
|
@ -93,6 +96,9 @@ public final class CockpitModel {
|
|||
await load({ try await api.fetchSpecialists() }) { specialists = $0 }
|
||||
await load({ try await api.fetchMetrics() }) { metrics = $0 }
|
||||
await load({ try await api.fetchIngestStatus() }) { ingest = $0 }
|
||||
// Best-effort, non-throwing: keep the prior image on a nil (transient or
|
||||
// none-yet) so the card never flickers empty between captures.
|
||||
if let shot = await api.fetchBumpScreenshot(surface: "tryst") { bumpScreenshot = shot }
|
||||
return out
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,28 @@
|
|||
import SwiftUI
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#elseif canImport(AppKit)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
// Shared cockpit components — every color/space comes from @Environment(\.tokens).
|
||||
|
||||
/// Decode image bytes to a SwiftUI `Image` on whichever Apple UI framework is
|
||||
/// present. NSImage/UIImage decode HEIC + PNG natively. Shared by the asset
|
||||
/// library tiles and the bump-screenshot card.
|
||||
func decodeImage(_ data: Data) -> Image? {
|
||||
#if canImport(UIKit)
|
||||
guard let ui = UIImage(data: data) else { return nil }
|
||||
return Image(uiImage: ui)
|
||||
#elseif canImport(AppKit)
|
||||
guard let ns = NSImage(data: data) else { return nil }
|
||||
return Image(nsImage: ns)
|
||||
#else
|
||||
return nil
|
||||
#endif
|
||||
}
|
||||
|
||||
struct SectionLabel: View {
|
||||
@Environment(\.tokens) private var t
|
||||
let text: String
|
||||
|
|
|
|||
|
|
@ -120,6 +120,11 @@ public struct LiveCockpitAPI: CockpitAPI {
|
|||
return try? await client.sendData(Endpoint.contentAssetImage(id: assetId), scope: scope)
|
||||
}
|
||||
|
||||
public func fetchBumpScreenshot(surface: String) async -> Data? {
|
||||
// 404 (none captured yet) surfaces as a thrown APIError → nil; the card hides.
|
||||
try? await client.sendData(Endpoint.bumpScreenshotLatest(surface: surface), scope: scope)
|
||||
}
|
||||
|
||||
// MARK: - Platform wire → Kit model mapping
|
||||
|
||||
private static func kitDrop(_ d: CocottePlatformModels.ContentDrop, assetCount: Int = 0) -> ContentDrop {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
import Foundation
|
||||
|
||||
/// Per-surface automation endpoints (bump policy / tier / screenshots) backed by
|
||||
/// platform.api's `/surfaces/*`. The cockpit reads these for the bookings-*
|
||||
/// specialists.
|
||||
extension Endpoint {
|
||||
|
||||
/// Authenticated proxy for the newest availability-bump overlay screenshot —
|
||||
/// the "here's what the automation focused on" image. The file path is
|
||||
/// resolved server-side from the tenant-scoped audit row; the client only
|
||||
/// supplies the surface.
|
||||
public static func bumpScreenshotLatest(surface: String = "tryst") -> Endpoint {
|
||||
Endpoint(path: "/surfaces/bump-screenshot/latest", method: .get, queryParams: ["surface": surface])
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue