feat: Built the native SwiftUI app shell mirroring the WebOS interface (Dashboard, Inventory, Oracle tabs) (#2)

I have attached the screenshots of the native SwiftUI app.

<img width="1705" alt="image.png" src="attachments/59fec2f3-0ae2-4b58-9349-457618ea0678">
<img width="1699" alt="image.png" src="attachments/0bf7c4f9-c883-4929-be36-774685b82fc4">
<img width="1698" alt="image.png" src="attachments/e3407e84-aaf2-45c0-9325-247d4020bace">
<img width="1694" alt="image.png" src="attachments/ee2cd47d-800d-4a40-855c-d54856680e79">
<img width="1694" alt="image.png" src="attachments/a2c902f1-9bc9-4427-8cae-b5801527c1ff">

Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local>
Reviewed-on: #2
Co-authored-by: sayan <sayan@desineuron.in>
Co-committed-by: sayan <sayan@desineuron.in>
This commit was merged in pull request #2.
This commit is contained in:
2026-03-07 18:46:02 +05:30
committed by sagnik
parent 8fe2344e71
commit cb6c752c8e
33 changed files with 6930 additions and 67 deletions

View File

@@ -0,0 +1,173 @@
import SwiftUI
enum AppSection: String, CaseIterable, Hashable, Identifiable {
var id: String { rawValue }
case dashboard = "Dashboard"
case oracle = "Oracle"
case sentinel = "Sentinel"
case inventory = "Inventory"
case settings = "Settings"
var systemImage: String {
switch self {
case .dashboard: return "square.grid.2x2"
case .oracle: return "message.and.waveform"
case .sentinel: return "person.crop.rectangle"
case .inventory: return "shippingbox"
case .settings: return "gearshape"
}
}
var accentColor: Color {
switch self {
case .dashboard: return VelocityTheme.accent
case .oracle: return Color(red: 0.13, green: 0.83, blue: 0.93) // cyan
case .sentinel: return Color(red: 0.60, green: 0.57, blue: 0.99) // indigo
case .inventory: return VelocityTheme.warning
case .settings: return VelocityTheme.mutedFg
}
}
}
struct ContentView: View {
@State private var selectedSection: AppSection? = .dashboard
var body: some View {
NavigationSplitView(columnVisibility: .constant(.all)) {
sidebarContent
} detail: {
detailContent
}
.navigationSplitViewStyle(.balanced)
}
// MARK: Sidebar
private var sidebarContent: some View {
ZStack {
VelocityTheme.sidebarBg.ignoresSafeArea()
VStack(spacing: 0) {
// App title
HStack(spacing: 10) {
ZStack {
RoundedRectangle(cornerRadius: 9)
.fill(VelocityTheme.accent.opacity(0.18))
.frame(width: 34, height: 34)
Image(systemName: "bolt.fill")
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(VelocityTheme.accent)
}
VStack(alignment: .leading, spacing: 1) {
Text("Velocity")
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(VelocityTheme.foreground)
Text("Project Velocity · v1.1")
.font(.system(size: 10))
.foregroundStyle(VelocityTheme.mutedFg)
}
Spacer()
}
.padding(.horizontal, 16)
.padding(.top, 20)
.padding(.bottom, 16)
Divider()
.background(VelocityTheme.borderSubtle)
.padding(.bottom, 8)
// Nav items
VStack(spacing: 2) {
ForEach(AppSection.allCases) { section in
SidebarRow(section: section,
isSelected: selectedSection == section)
.onTapGesture { selectedSection = section }
}
}
.padding(.horizontal, 8)
Spacer()
// User footer
Divider()
.background(VelocityTheme.borderSubtle)
HStack(spacing: 10) {
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(VelocityTheme.accent)
.frame(width: 32, height: 32)
Text("AF")
.font(.system(size: 11, weight: .bold))
.foregroundStyle(.white)
}
VStack(alignment: .leading, spacing: 2) {
Text("Ahmed Al-Farsi")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(VelocityTheme.foreground)
Text("Sales Director")
.font(.system(size: 10))
.foregroundStyle(VelocityTheme.mutedFg)
}
Spacer()
}
.padding(16)
}
}
.navigationTitle("")
.toolbar(.hidden, for: .navigationBar)
}
// MARK: Detail
private var detailContent: some View {
ZStack {
VelocityTheme.background.ignoresSafeArea()
Group {
switch selectedSection {
case .dashboard: DashboardView()
case .oracle: OracleView()
case .sentinel: SentinelView()
case .inventory: InventoryView()
case .settings: SettingsView()
case .none: DashboardView()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
// MARK: Sidebar Row
private struct SidebarRow: View {
let section: AppSection
let isSelected: Bool
var body: some View {
HStack(spacing: 11) {
Image(systemName: section.systemImage)
.font(.system(size: 14, weight: .medium))
.foregroundStyle(isSelected ? section.accentColor : VelocityTheme.mutedFg)
.frame(width: 20)
Text(section.rawValue)
.font(.system(size: 14, weight: isSelected ? .semibold : .regular))
.foregroundStyle(isSelected ? VelocityTheme.foreground : VelocityTheme.mutedFg)
Spacer()
}
.padding(.horizontal, 12)
.padding(.vertical, 9)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(isSelected ? section.accentColor.opacity(0.12) : .clear)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(isSelected ? section.accentColor.opacity(0.25) : .clear, lineWidth: 1)
)
)
.contentShape(Rectangle())
}
}
#Preview {
ContentView()
}

View File

@@ -0,0 +1,11 @@
import SwiftUI
@main
struct VelocityApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.preferredColorScheme(.dark)
}
}
}

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,35 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,92 @@
import CoreLocation
import Foundation
struct SunPosition {
let azimuth: Double // 0...360, degrees clockwise from true north
let elevation: Double // -90...90 degrees above horizon
}
enum SunMath {
static func calculateSunPosition(date: Date, coordinate: CLLocationCoordinate2D) -> SunPosition {
let timezone = TimeZone.current
let localOffsetHours = Double(timezone.secondsFromGMT(for: date)) / 3600.0
let julianDay = date.julianDay
let n = julianDay - 2_451_545.0
let meanLongitude = normalizeDegrees(280.46 + 0.985_647_4 * n)
let meanAnomaly = normalizeDegrees(357.528 + 0.985_600_3 * n)
let lambda = meanLongitude
+ 1.915 * sin(meanAnomaly.radians)
+ 0.020 * sin((2.0 * meanAnomaly).radians)
let obliquity = 23.439 - 0.000_000_4 * n
let rightAscension = atan2(
cos(obliquity.radians) * sin(lambda.radians),
cos(lambda.radians)
).degrees
let declination = asin(sin(obliquity.radians) * sin(lambda.radians)).degrees
let utcHours = date.utcHours
let lst = normalizeDegrees(100.46 + 0.985_647 * n + coordinate.longitude + 15.0 * utcHours + localOffsetHours)
let hourAngle = normalizeDegrees(lst - rightAscension)
let signedHourAngle = hourAngle > 180.0 ? hourAngle - 360.0 : hourAngle
let latitude = coordinate.latitude.radians
let declinationRad = declination.radians
let hourAngleRad = signedHourAngle.radians
let elevation = asin(
sin(latitude) * sin(declinationRad)
+ cos(latitude) * cos(declinationRad) * cos(hourAngleRad)
).degrees
let azimuth = normalizeDegrees(
atan2(
-sin(hourAngleRad),
tan(declinationRad) * cos(latitude) - sin(latitude) * cos(hourAngleRad)
).degrees
)
return SunPosition(azimuth: azimuth, elevation: elevation)
}
static func sunPathSamples(for date: Date, coordinate: CLLocationCoordinate2D) -> [Date: SunPosition] {
let calendar = Calendar.current
let sampleHours = [8, 10, 12, 14, 16]
var output: [Date: SunPosition] = [:]
for hour in sampleHours {
if let sampleDate = calendar.date(bySettingHour: hour, minute: 0, second: 0, of: date) {
output[sampleDate] = calculateSunPosition(date: sampleDate, coordinate: coordinate)
}
}
return output
}
private static func normalizeDegrees(_ value: Double) -> Double {
let reduced = value.truncatingRemainder(dividingBy: 360.0)
return reduced >= 0 ? reduced : reduced + 360.0
}
}
private extension Date {
var utcHours: Double {
let calendar = Calendar(identifier: .gregorian)
let comps = calendar.dateComponents(in: TimeZone(secondsFromGMT: 0)!, from: self)
let hours = Double(comps.hour ?? 0)
let minutes = Double(comps.minute ?? 0)
let seconds = Double(comps.second ?? 0)
return hours + minutes / 60.0 + seconds / 3600.0
}
var julianDay: Double {
let interval = timeIntervalSince1970
return (interval / 86_400.0) + 2_440_587.5
}
}
private extension Double {
var radians: Double { self * .pi / 180.0 }
var degrees: Double { self * 180.0 / .pi }
}

View File

@@ -0,0 +1,100 @@
import Foundation
import UIKit
@preconcurrency import Alamofire
final class ComfyClient {
static let shared = ComfyClient()
private let endpoint = "http://192.168.x.x:8000/dream-weaver"
private let session: Session
private init(session: Session = .default) {
self.session = session
}
func generateImage(source: UIImage, prompt: String) async throws -> UIImage {
let resized = source.resizedSquare(to: 1024)
guard let imageData = resized.jpegData(compressionQuality: 0.9) else {
throw ComfyClientError.encodingFailed
}
let payload = DreamWeaverRequest(
imageBase64: imageData.base64EncodedString(),
prompt: prompt
)
let response = try await session.request(
endpoint,
method: .post,
parameters: payload,
encoder: JSONParameterEncoder.default,
headers: [.contentType("application/json")]
)
.validate(statusCode: 200..<300)
.serializingDecodable(DreamWeaverResponse.self)
.value
guard
let data = Data(base64Encoded: response.outputBase64),
let generated = UIImage(data: data)
else {
throw ComfyClientError.decodingFailed
}
return generated
}
}
private struct DreamWeaverRequest: Encodable, Sendable {
let imageBase64: String
let prompt: String
}
private struct DreamWeaverResponse: Decodable, Sendable {
let outputBase64: String
enum CodingKeys: String, CodingKey {
case outputBase64 = "output_base64"
case imageBase64 = "image_base64"
case image
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let preferred = try container.decodeIfPresent(String.self, forKey: .outputBase64) {
outputBase64 = preferred
return
}
if let legacy = try container.decodeIfPresent(String.self, forKey: .imageBase64) {
outputBase64 = legacy
return
}
outputBase64 = try container.decode(String.self, forKey: .image)
}
}
enum ComfyClientError: Error {
case encodingFailed
case decodingFailed
}
private extension UIImage {
func resizedSquare(to side: CGFloat) -> UIImage {
let format = UIGraphicsImageRendererFormat.default()
format.scale = 1
let renderer = UIGraphicsImageRenderer(size: CGSize(width: side, height: side), format: format)
return renderer.image { _ in
let aspect = size.width / size.height
let targetRect: CGRect
if aspect > 1 {
let width = side * aspect
targetRect = CGRect(x: (side - width) / 2, y: 0, width: width, height: side)
} else {
let height = side / aspect
targetRect = CGRect(x: 0, y: (side - height) / 2, width: side, height: height)
}
draw(in: targetRect)
}
}
}

View File

@@ -0,0 +1,256 @@
import SwiftUI
import Combine
// MARK: Data Models
enum SentimentType: String, CaseIterable {
case excited, interested, neutral, confused, disinterested
var score: Int {
switch self {
case .excited: return 100
case .interested: return 80
case .neutral: return 50
case .confused: return 30
case .disinterested: return 10
}
}
var emoji: String {
switch self {
case .excited: return "😃"
case .interested: return "🤔"
case .neutral: return "😐"
case .confused: return "😕"
case .disinterested: return "😴"
}
}
var color: Color {
switch self {
case .excited: return VelocityTheme.success
case .interested: return VelocityTheme.accent
case .neutral: return VelocityTheme.mutedFg
case .confused: return VelocityTheme.warning
case .disinterested: return VelocityTheme.danger
}
}
}
struct Visitor: Identifiable {
let id: String
let faceId: String
var sentiment: SentimentType
var confidence: Double
var dwellTime: Int // seconds
var zone: String
let timestamp: Date
}
enum LeadSource: String {
case whatsapp = "WhatsApp"
case walkin = "Walk-in"
case website = "Website"
}
enum LeadStatus: String {
case hot = "Hot"
case engaged = "Engaged"
case new = "New"
case qualified = "Qualified"
case closed = "Closed"
var color: Color {
switch self {
case .hot: return VelocityTheme.danger
case .engaged: return VelocityTheme.accent
case .new: return VelocityTheme.mutedFg
case .qualified: return VelocityTheme.success
case .closed: return Color(red: 0.60, green: 0.57, blue: 0.99)
}
}
}
struct Lead: Identifiable {
let id: String
let name: String
let phone: String
let source: LeadSource
var status: LeadStatus
var lastMessage: String
var lastActive: Date
var unreadCount: Int
let qualification: String
let budget: String
let interest: String
var initials: String { String(name.split(separator: " ").prefix(2).compactMap(\.first)) }
}
struct ChatMessage: Identifiable {
let id: String
let sender: String // "user" | "oracle" | "ai"
let content: String
let timestamp: Date
}
struct SystemHealth {
var cpu: Double // 01
var gpu: Double
var memory: Double
}
struct DashboardMetrics {
var activeVisitors: Int
var revenue: String
var aiJobs: Int
var dailyVisitors: Int
var sentimentScore: Double // 0100
var systemHealth: SystemHealth
}
// MARK: Shared Store
@Observable
final class AppStore {
static let shared = AppStore()
private init() { startTimer() }
// Dashboard
var metrics = DashboardMetrics(
activeVisitors: 17,
revenue: "$3.2M",
aiJobs: 24,
dailyVisitors: 128,
sentimentScore: 78,
systemHealth: SystemHealth(cpu: 0.42, gpu: 0.61, memory: 0.55)
)
var dashboardMessages: [ChatMessage] = [
ChatMessage(id: "d0", sender: "ai",
content: "Hello, Ahmed. I've analysed the Q3 pipeline. Would you like a refined strategy for the Apex Innovations deal?",
timestamp: Date().addingTimeInterval(-300))
]
var isDashboardThinking = false
// Visitors
var visitors: [Visitor] = [
Visitor(id: "v1", faceId: "face_001", sentiment: .excited, confidence: 0.92, dwellTime: 450, zone: "Penthouse Show", timestamp: Date()),
Visitor(id: "v2", faceId: "face_002", sentiment: .interested, confidence: 0.87, dwellTime: 320, zone: "Amenity Deck VR", timestamp: Date()),
Visitor(id: "v3", faceId: "face_003", sentiment: .neutral, confidence: 0.78, dwellTime: 180, zone: "Reception", timestamp: Date()),
Visitor(id: "v4", faceId: "face_004", sentiment: .confused, confidence: 0.74, dwellTime: 95, zone: "Penthouse Show", timestamp: Date()),
Visitor(id: "v5", faceId: "face_005", sentiment: .disinterested, confidence: 0.65, dwellTime: 60, zone: "Gallery", timestamp: Date()),
]
// Alerts
var isAlertActive = false
var alertMessage = ""
func triggerAlert(_ msg: String) {
isAlertActive = true
alertMessage = msg
}
func clearAlert() {
isAlertActive = false
alertMessage = ""
}
// Leads (Oracle)
var leads: [Lead] = [
Lead(id: "1", name: "Mohammed Al-Rashid", phone: "+971 55 123 4567", source: .whatsapp,
status: .hot, lastMessage: "Can we schedule a viewing for the penthouse tomorrow?",
lastActive: Date().addingTimeInterval(-300), unreadCount: 2,
qualification: "whale", budget: "AED 15M+", interest: "Penthouse Suite"),
Lead(id: "2", name: "Sarah Chen", phone: "+971 50 987 6543", source: .walkin,
status: .engaged, lastMessage: "Thank you for the brochure. I will review with my partner.",
lastActive: Date().addingTimeInterval(-1800), unreadCount: 0,
qualification: "potential", budget: "AED 58M", interest: "2BR Sea View"),
Lead(id: "3", name: "James Wilson", phone: "+971 52 456 7890", source: .website,
status: .new, lastMessage: "Interested in investment opportunities.",
lastActive: Date().addingTimeInterval(-7200), unreadCount: 1,
qualification: "potential", budget: "AED 35M", interest: "1BR Investment"),
Lead(id: "4", name: "Fatima Hassan", phone: "+971 54 321 0987", source: .whatsapp,
status: .qualified,lastMessage: "What are the payment plan options?",
lastActive: Date().addingTimeInterval(-14400), unreadCount: 0,
qualification: "whale", budget: "AED 12M+", interest: "3BR + Maid"),
Lead(id: "5", name: "David Kumar", phone: "+971 56 789 0123", source: .walkin,
status: .closed, lastMessage: "Contract signed. Thank you!",
lastActive: Date().addingTimeInterval(-86400), unreadCount: 0,
qualification: "whale", budget: "AED 20M", interest: "Full Floor"),
]
var messages: [String: [ChatMessage]] = [
"1": [
ChatMessage(id: "m1", sender: "user", content: "Hi, I am interested in the penthouse units.",
timestamp: Date().addingTimeInterval(-7200)),
ChatMessage(id: "m2", sender: "oracle",
content: "Welcome! Our penthouse collection features 4 exclusive units with panoramic sea views. Prices start at AED 15M.",
timestamp: Date().addingTimeInterval(-7200 + 30)),
ChatMessage(id: "m3", sender: "user", content: "Can we schedule a viewing tomorrow?",
timestamp: Date().addingTimeInterval(-300)),
],
"2": [
ChatMessage(id: "m4", sender: "oracle",
content: "Hello Sarah! Here is the digital brochure for the 2-bedroom units we discussed.",
timestamp: Date().addingTimeInterval(-14400)),
ChatMessage(id: "m5", sender: "user", content: "Thank you. I will review with my partner.",
timestamp: Date().addingTimeInterval(-1800)),
],
]
var activeLeadId: String? = "1"
var isOracleThinking = false
func addDashboardMessage(sender: String, content: String) {
let msg = ChatMessage(id: UUID().uuidString, sender: sender, content: content, timestamp: Date())
dashboardMessages.append(msg)
}
func addOracleMessage(leadId: String, sender: String, content: String) {
let msg = ChatMessage(id: UUID().uuidString, sender: sender, content: content, timestamp: Date())
if messages[leadId] == nil { messages[leadId] = [] }
messages[leadId]!.append(msg)
}
// Live ticker
private var timerTask: AnyCancellable?
private var alertTask: DispatchWorkItem?
private func startTimer() {
timerTask = Timer.publish(every: 5, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in self?.tick() }
}
private func tick() {
// jitter visitor count ±1
let delta = Int.random(in: -1...1)
metrics.activeVisitors = max(10, metrics.activeVisitors + delta)
// jitter sentiment ±2
let sDelta = Double.random(in: -2...2)
metrics.sentimentScore = min(100, max(40, metrics.sentimentScore + sDelta))
// jitter system health
metrics.systemHealth.cpu = Double.random(in: 0.30...0.65)
metrics.systemHealth.gpu = Double.random(in: 0.45...0.75)
metrics.systemHealth.memory = Double.random(in: 0.40...0.70)
// Random alert (same 10% chance as WebOS every tick)
if !isAlertActive && Double.random(in: 0...1) > 0.85 {
triggerAlert("Confusion detected in Zone B Penthouse Gallery")
let work = DispatchWorkItem { [weak self] in self?.clearAlert() }
alertTask = work
DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: work)
}
}
}
// MARK: Helpers
extension Date {
var relativeShort: String {
let diff = Int(Date().timeIntervalSince(self))
if diff < 60 { return "now" }
if diff < 3600 { return "\(diff / 60)m ago" }
if diff < 86400 { return "\(diff / 3600)h ago" }
return "\(diff / 86400)d ago"
}
}

View File

@@ -0,0 +1,17 @@
import SwiftUI
struct GlassBlurView: UIViewRepresentable {
let style: UIBlurEffect.Style
init(style: UIBlurEffect.Style = .systemUltraThinMaterial) {
self.style = style
}
func makeUIView(context: Context) -> UIVisualEffectView {
UIVisualEffectView(effect: UIBlurEffect(style: style))
}
func updateUIView(_ uiView: UIVisualEffectView, context: Context) {
uiView.effect = UIBlurEffect(style: style)
}
}

View File

@@ -0,0 +1,60 @@
import SwiftUI
// MARK: - Design Tokens matching the WebOS dark interface
enum VelocityTheme {
// Backgrounds
/// True black app background
static let background = Color(red: 0.00, green: 0.00, blue: 0.00)
/// Dark surface (#131418)
static let surface = Color(red: 0.074, green: 0.078, blue: 0.094)
/// Slightly lighter surface (#181b20)
static let surface2 = Color(red: 0.095, green: 0.106, blue: 0.125)
/// Card surface (#22262e)
static let surface3 = Color(red: 0.133, green: 0.149, blue: 0.180)
/// Sidebar background (#0B0D10)
static let sidebarBg = Color(red: 0.043, green: 0.051, blue: 0.063)
// Foreground
static let foreground = Color(white: 0.96)
static let mutedFg = Color(red: 0.580, green: 0.620, blue: 0.710)
static let subtleFg = Color(red: 0.35, green: 0.38, blue: 0.44)
// Accent: Blue (#3b82f6)
static let accent = Color(red: 0.231, green: 0.510, blue: 0.965)
static let accentDim = Color(red: 0.160, green: 0.388, blue: 0.820)
static let accentSubtle = Color(red: 0.231, green: 0.510, blue: 0.965).opacity(0.15)
// Semantic
static let success = Color(red: 0.290, green: 0.780, blue: 0.290)
static let warning = Color(red: 0.980, green: 0.745, blue: 0.141)
static let danger = Color(red: 0.973, green: 0.267, blue: 0.267)
// Borders
static let borderSubtle = Color.white.opacity(0.07)
static let borderAccent = Color(red: 0.231, green: 0.510, blue: 0.965).opacity(0.18)
}
// MARK: - Glass card modifier
struct GlassCard: ViewModifier {
var cornerRadius: CGFloat = 16
func body(content: Content) -> some View {
content
.background(
RoundedRectangle(cornerRadius: cornerRadius)
.fill(Color(red: 0.031, green: 0.039, blue: 0.071).opacity(0.82))
.overlay(
RoundedRectangle(cornerRadius: cornerRadius)
.stroke(VelocityTheme.borderAccent, lineWidth: 1)
)
.shadow(color: .black.opacity(0.55), radius: 16, y: 4)
)
}
}
extension View {
func glassCard(cornerRadius: CGFloat = 16) -> some View {
self.modifier(GlassCard(cornerRadius: cornerRadius))
}
}

View File

@@ -0,0 +1,442 @@
import SwiftUI
struct DashboardView: View {
private var store: AppStore { AppStore.shared }
@State private var chatInput = ""
private let columns = [GridItem(.adaptive(minimum: 220), spacing: 14)]
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
pageHeader
// KPI Grid live from store
LazyVGrid(columns: columns, spacing: 14) {
LiveKPICard(
title: "Visitors",
value: "\(store.metrics.activeVisitors)",
subtitle: "Active now",
icon: "person.2",
accentColor: VelocityTheme.accent,
glowColor: VelocityTheme.accent.opacity(0.22),
badge: "LIVE"
)
LiveKPICard(
title: "Revenue",
value: store.metrics.revenue,
subtitle: "30-day forecast",
icon: "chart.line.uptrend.xyaxis",
accentColor: Color(red: 0.13, green: 0.83, blue: 0.93),
glowColor: Color(red: 0.13, green: 0.83, blue: 0.93).opacity(0.18)
)
LiveKPICard(
title: "AI Jobs",
value: "\(store.metrics.aiJobs)",
subtitle: "Queue depth",
icon: "cpu",
accentColor: Color(red: 0.60, green: 0.57, blue: 0.99),
glowColor: Color(red: 0.60, green: 0.57, blue: 0.99).opacity(0.20)
)
LiveKPICard(
title: "Listings",
value: "\(store.metrics.dailyVisitors)",
subtitle: "Active units",
icon: "building.2",
accentColor: VelocityTheme.success,
glowColor: VelocityTheme.success.opacity(0.18)
)
}
.animation(.easeInOut(duration: 0.4), value: store.metrics.activeVisitors)
// Sentiment Gauge
sentimentGauge
// System Health
systemHealthPanel
// AI Chat Widget
aiChatWidget
}
.padding(20)
}
.background(VelocityTheme.background)
.scrollContentBackground(.hidden)
}
// MARK: Page Header
private var pageHeader: some View {
HStack {
VStack(alignment: .leading, spacing: 3) {
Text("Dashboard")
.font(.system(size: 28, weight: .bold))
.foregroundStyle(VelocityTheme.foreground)
Text("Project Velocity · v.1.1")
.font(.system(size: 12))
.foregroundStyle(VelocityTheme.mutedFg)
}
Spacer()
HStack(spacing: 5) {
Circle()
.fill(VelocityTheme.success)
.frame(width: 7, height: 7)
Text("Live")
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(VelocityTheme.success)
}
}
}
// MARK: Sentiment Gauge
private var sentimentGauge: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Image(systemName: "waveform.path.ecg")
.font(.system(size: 12))
.foregroundStyle(VelocityTheme.accent)
Text("Sentiment Thermometer")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(VelocityTheme.foreground)
Spacer()
Text("Showroom Vibe")
.font(.system(size: 11))
.foregroundStyle(VelocityTheme.mutedFg)
let label = store.metrics.sentimentScore >= 70 ? "Excellent" :
store.metrics.sentimentScore >= 50 ? "Good" : "Needs Attention"
let labelColor: Color = store.metrics.sentimentScore >= 70 ? VelocityTheme.success :
store.metrics.sentimentScore >= 50 ? VelocityTheme.warning : VelocityTheme.danger
Text(label)
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(labelColor)
}
GeometryReader { geo in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 5)
.fill(Color.white.opacity(0.05))
.frame(height: 26)
RoundedRectangle(cornerRadius: 5)
.fill(
LinearGradient(
colors: [Color(red: 0.11, green: 0.30, blue: 0.86),
VelocityTheme.accent,
Color(red: 0.38, green: 0.65, blue: 0.98)],
startPoint: .leading, endPoint: .trailing
)
)
.frame(width: geo.size.width * (store.metrics.sentimentScore / 100), height: 26)
.shadow(color: VelocityTheme.accent.opacity(0.6), radius: 6)
.animation(.easeInOut(duration: 0.8), value: store.metrics.sentimentScore)
Text("\(Int(store.metrics.sentimentScore))%")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
}
}
.frame(height: 26)
}
.padding(20)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(red: 0.031, green: 0.039, blue: 0.071))
.overlay(RoundedRectangle(cornerRadius: 16).stroke(VelocityTheme.borderAccent, lineWidth: 1))
)
}
// MARK: System Health
private var systemHealthPanel: some View {
let gauges: [(label: String, value: Double, color: Color)] = [
("CPU", store.metrics.systemHealth.cpu, VelocityTheme.accent),
("GPU", store.metrics.systemHealth.gpu, Color(red: 0.50, green: 0.56, blue: 0.97)),
("Memory", store.metrics.systemHealth.memory, Color(red: 0.13, green: 0.83, blue: 0.93)),
]
return VStack(alignment: .leading, spacing: 14) {
HStack {
Image(systemName: "cpu").font(.system(size: 12)).foregroundStyle(VelocityTheme.accent)
Text("System Health")
.font(.system(size: 13, weight: .semibold)).foregroundStyle(VelocityTheme.foreground)
Spacer()
HStack(spacing: 4) {
Circle().fill(VelocityTheme.success).frame(width: 5, height: 5)
Text("Optimal").font(.system(size: 11, weight: .medium)).foregroundStyle(VelocityTheme.success)
}
}
HStack(spacing: 16) {
ForEach(gauges, id: \.label) { g in
VStack(alignment: .leading, spacing: 6) {
HStack {
Text(g.label).font(.system(size: 10, weight: .medium)).tracking(0.8)
.foregroundStyle(VelocityTheme.mutedFg)
Spacer()
Text("\(Int(g.value * 100))%").font(.system(size: 10, weight: .semibold))
.foregroundStyle(VelocityTheme.foreground)
}
GeometryReader { geo in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 3).fill(Color.white.opacity(0.05)).frame(height: 5)
RoundedRectangle(cornerRadius: 3).fill(g.color)
.frame(width: geo.size.width * g.value, height: 5)
.shadow(color: g.color.opacity(0.6), radius: 4)
.animation(.easeInOut(duration: 0.6), value: g.value)
}
}
.frame(height: 5)
}
.frame(maxWidth: .infinity)
}
}
}
.padding(20)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(red: 0.031, green: 0.039, blue: 0.071))
.overlay(RoundedRectangle(cornerRadius: 16).stroke(VelocityTheme.borderAccent, lineWidth: 1))
)
}
// MARK: AI Chat Widget
private var aiChatWidget: some View {
VStack(spacing: 0) {
// Header
HStack(spacing: 10) {
ZStack {
Circle().fill(VelocityTheme.accent.opacity(0.18)).frame(width: 34, height: 34)
Image(systemName: "sparkles").font(.system(size: 14)).foregroundStyle(VelocityTheme.accent)
}
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 5) {
Text("AI Onboard").font(.system(size: 13, weight: .semibold)).foregroundStyle(VelocityTheme.foreground)
Image(systemName: "staroflife.fill").font(.system(size: 7)).foregroundStyle(VelocityTheme.accent)
}
HStack(spacing: 4) {
Circle().fill(VelocityTheme.success).frame(width: 5, height: 5)
Text("Online").font(.system(size: 10)).foregroundStyle(VelocityTheme.mutedFg)
}
}
Spacer()
}
.padding(16)
Divider().background(VelocityTheme.borderSubtle)
// Messages
ScrollViewReader { proxy in
ScrollView {
VStack(spacing: 12) {
ForEach(store.dashboardMessages) { msg in
ChatBubble(message: msg)
.id(msg.id)
}
if store.isDashboardThinking {
TypingIndicator()
}
}
.padding(16)
}
.frame(height: 240)
.onChange(of: store.dashboardMessages.count) {
if let last = store.dashboardMessages.last {
withAnimation { proxy.scrollTo(last.id, anchor: .bottom) }
}
}
.onChange(of: store.isDashboardThinking) {
if store.isDashboardThinking {
withAnimation { proxy.scrollTo("typing", anchor: .bottom) }
}
}
}
Divider().background(VelocityTheme.borderSubtle)
// Input
HStack(spacing: 10) {
TextField("Ask AI assistant...", text: $chatInput)
.font(.system(size: 13))
.foregroundStyle(VelocityTheme.foreground)
.tint(VelocityTheme.accent)
.onSubmit { sendDashboardMessage() }
Button(action: sendDashboardMessage) {
Image(systemName: "arrow.up.circle.fill")
.font(.system(size: 22))
.foregroundStyle(chatInput.isEmpty ? VelocityTheme.mutedFg : VelocityTheme.accent)
}
.disabled(chatInput.isEmpty || store.isDashboardThinking)
}
.padding(14)
}
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(red: 0.031, green: 0.039, blue: 0.071))
.overlay(RoundedRectangle(cornerRadius: 16).stroke(VelocityTheme.borderAccent, lineWidth: 1))
)
}
private func sendDashboardMessage() {
let text = chatInput.trimmingCharacters(in: .whitespaces)
guard !text.isEmpty else { return }
chatInput = ""
store.addDashboardMessage(sender: "user", content: text)
store.isDashboardThinking = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
store.isDashboardThinking = false
store.addDashboardMessage(
sender: "ai",
content: dashboardAIResponse(for: text)
)
}
}
private func dashboardAIResponse(for prompt: String) -> String {
let p = prompt.lowercased()
if p.contains("penthouse") || p.contains("apex") {
return "Apex Innovations probability score is now at 85%. I recommend scheduling a closing meeting next Tuesday — sentiment analysis shows high engagement."
} else if p.contains("visitor") || p.contains("traffic") {
return "Currently \(store.metrics.activeVisitors) active visitors. Zone B (Penthouse Gallery) is generating the highest dwell time today — average 8 minutes."
} else if p.contains("revenue") || p.contains("deal") {
return "Pipeline value stands at \(store.metrics.revenue) for the 30-day forecast. 3 deals are in final negotiation — I recommend prioritising Mohammed Al-Rashid."
} else if p.contains("sentiment") {
return "Showroom sentiment is at \(Int(store.metrics.sentimentScore))% — above the excellent threshold. Visitors in Zone D (Reception) show the highest satisfaction scores."
}
return "I've analysed the current data. Revenue is tracking at \(store.metrics.revenue) with \(store.metrics.activeVisitors) active visitors. Shall I prepare a detailed pipeline report?"
}
}
// MARK: KPI Card (live-bound)
private struct LiveKPICard: View {
let title: String
let value: String
let subtitle: String
let icon: String
let accentColor: Color
let glowColor: Color
var badge: String? = nil
var body: some View {
VStack(alignment: .leading, spacing: 0) {
HStack {
ZStack {
RoundedRectangle(cornerRadius: 8).fill(accentColor.opacity(0.12)).frame(width: 32, height: 32)
Image(systemName: icon).font(.system(size: 14, weight: .medium)).foregroundStyle(accentColor)
}
Spacer()
if let badge {
HStack(spacing: 4) {
Circle().fill(VelocityTheme.success).frame(width: 5, height: 5)
Text(badge).font(.system(size: 9, weight: .semibold)).foregroundStyle(VelocityTheme.success)
}
}
}
.padding(.bottom, 20)
Text(title.uppercased())
.font(.system(size: 10, weight: .medium)).tracking(1.2)
.foregroundStyle(VelocityTheme.mutedFg).padding(.bottom, 4)
Text(value)
.font(.system(size: 34, weight: .semibold))
.foregroundStyle(VelocityTheme.foreground)
.contentTransition(.numericText())
.minimumScaleFactor(0.7).lineLimit(1).padding(.bottom, 4)
Text(subtitle).font(.system(size: 11)).foregroundStyle(VelocityTheme.subtleFg)
}
.padding(20)
.frame(maxWidth: .infinity, minHeight: 148, alignment: .leading)
.background(
ZStack(alignment: .bottomTrailing) {
RoundedRectangle(cornerRadius: 16).fill(Color(red: 0.031, green: 0.039, blue: 0.071))
Ellipse().fill(glowColor).frame(width: 120, height: 90).blur(radius: 28).offset(x: 20, y: 20)
VStack {
Rectangle()
.fill(LinearGradient(colors: [.clear, .white.opacity(0.10), .clear], startPoint: .leading, endPoint: .trailing))
.frame(height: 1)
Spacer()
}
.clipShape(RoundedRectangle(cornerRadius: 16))
}
.overlay(RoundedRectangle(cornerRadius: 16).stroke(VelocityTheme.borderAccent, lineWidth: 1))
.shadow(color: .black.opacity(0.55), radius: 16, y: 4)
)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
}
// MARK: Chat Bubble
private struct ChatBubble: View {
let message: ChatMessage
private var isUser: Bool { message.sender == "user" }
var body: some View {
HStack(alignment: .bottom, spacing: 8) {
if isUser { Spacer(minLength: 40) }
if !isUser {
ZStack {
Circle().fill(VelocityTheme.accent.opacity(0.18)).frame(width: 26, height: 26)
Image(systemName: "sparkles").font(.system(size: 10)).foregroundStyle(VelocityTheme.accent)
}
}
Text(message.content)
.font(.system(size: 13))
.foregroundStyle(isUser ? .white : VelocityTheme.foreground)
.padding(.horizontal, 12).padding(.vertical, 9)
.background(
RoundedRectangle(cornerRadius: isUser ? 14 : 14)
.fill(isUser
? VelocityTheme.accent.opacity(0.85)
: Color.white.opacity(0.06))
.overlay(
RoundedRectangle(cornerRadius: 14)
.stroke(isUser ? .clear : Color.white.opacity(0.10), lineWidth: 1)
)
)
if isUser {
ZStack {
Circle().fill(Color(red: 0.08, green: 0.10, blue: 0.18)).frame(width: 26, height: 26)
Text("A").font(.system(size: 10, weight: .bold)).foregroundStyle(VelocityTheme.foreground)
}
}
if !isUser { Spacer(minLength: 40) }
}
}
}
// MARK: Typing Indicator
private struct TypingIndicator: View {
@State private var phase = 0
var body: some View {
HStack(alignment: .bottom, spacing: 8) {
ZStack {
Circle().fill(VelocityTheme.accent.opacity(0.18)).frame(width: 26, height: 26)
Image(systemName: "sparkles").font(.system(size: 10)).foregroundStyle(VelocityTheme.accent)
}
HStack(spacing: 4) {
ForEach(0..<3, id: \.self) { i in
Circle()
.fill(VelocityTheme.mutedFg)
.frame(width: 6, height: 6)
.scaleEffect(phase == i ? 1.4 : 0.8)
.animation(.easeInOut(duration: 0.4).repeatForever().delay(Double(i) * 0.15), value: phase)
}
}
.padding(.horizontal, 14).padding(.vertical, 12)
.background(RoundedRectangle(cornerRadius: 14).fill(Color.white.opacity(0.06))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(Color.white.opacity(0.10), lineWidth: 1)))
Spacer(minLength: 40)
}
.id("typing")
.onAppear {
withAnimation { phase = 1 }
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
phase = (phase + 1) % 3
}
}
}
}

View File

@@ -0,0 +1,118 @@
import ARKit
import CoreLocation
import CoreMotion
import SceneKit
import SwiftUI
struct ARSunOverlayView: UIViewRepresentable {
@Binding var sunNodesReady: Bool
func makeCoordinator() -> Coordinator {
Coordinator(sunNodesReady: $sunNodesReady)
}
func makeUIView(context: Context) -> ARSCNView {
let view = ARSCNView(frame: .zero)
view.delegate = context.coordinator
view.scene = SCNScene()
view.automaticallyUpdatesLighting = true
let config = ARWorldTrackingConfiguration()
config.worldAlignment = .gravityAndHeading
view.session.run(config)
context.coordinator.attach(to: view)
return view
}
func updateUIView(_ uiView: ARSCNView, context: Context) {}
static func dismantleUIView(_ uiView: ARSCNView, coordinator: Coordinator) {
uiView.session.pause()
coordinator.stop()
}
final class Coordinator: NSObject, ARSCNViewDelegate, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
private let motionManager = CMMotionManager()
private weak var sceneView: ARSCNView?
private var heading: CLLocationDirection = 0
private var coordinate: CLLocationCoordinate2D?
@Binding private var sunNodesReady: Bool
init(sunNodesReady: Binding<Bool>) {
_sunNodesReady = sunNodesReady
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.headingFilter = 1
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
locationManager.startUpdatingHeading()
startMotion()
}
func attach(to sceneView: ARSCNView) {
self.sceneView = sceneView
addSunPathNodesIfPossible()
}
func stop() {
motionManager.stopDeviceMotionUpdates()
locationManager.stopUpdatingHeading()
locationManager.stopUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard coordinate == nil, let location = locations.last else { return }
coordinate = location.coordinate
addSunPathNodesIfPossible()
}
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
heading = newHeading.trueHeading > 0 ? newHeading.trueHeading : newHeading.magneticHeading
addSunPathNodesIfPossible()
}
private func startMotion() {
guard motionManager.isDeviceMotionAvailable else { return }
motionManager.deviceMotionUpdateInterval = 0.1
motionManager.startDeviceMotionUpdates()
}
private func addSunPathNodesIfPossible() {
guard
let sceneView,
let coordinate,
!sunNodesReady
else { return }
let samples = SunMath.sunPathSamples(for: Date(), coordinate: coordinate)
let sorted = samples.sorted { $0.key < $1.key }
let root = SCNNode()
let northOffset = (heading).radians
let radius: Float = 1.8
for (_, pos) in sorted {
let elevation = Float(pos.elevation.radians)
let azimuth = Float((pos.azimuth).radians) - Float(northOffset)
let x = radius * cos(elevation) * sin(azimuth)
let y = radius * sin(elevation)
let z = -radius * cos(elevation) * cos(azimuth)
let sphere = SCNSphere(radius: 0.03)
sphere.firstMaterial?.diffuse.contents = UIColor.systemYellow
let node = SCNNode(geometry: sphere)
node.position = SCNVector3(x, y, z)
root.addChildNode(node)
}
sceneView.scene.rootNode.addChildNode(root)
sunNodesReady = true
}
}
}
private extension Double {
var radians: Double { self * .pi / 180.0 }
}

View File

@@ -0,0 +1,439 @@
import AVFoundation
import Observation
import SceneKit
import SwiftUI
import UIKit
@Observable
final class InventoryStore {
enum Mode: String, CaseIterable, Identifiable {
case sunseeker = "Sunseeker"
case dreamWeaver = "Dream Weaver"
case dollhouse = "Dollhouse"
var id: String { rawValue }
}
var mode: Mode = .sunseeker
var selectedPrompt: String = "Modern Islamic"
var sourceImage: UIImage?
var generatedImage: UIImage?
var isProcessing: Bool = false
var sunNodesReady: Bool = false
var dollhouseHour: Double = 12
let prompts = ["Modern Islamic", "Minimalist", "Night Mode"]
}
struct InventoryView: View {
@State private var store = InventoryStore()
@State private var showCamera = false
@State private var sliderTickHour = 12
private let haptics = UIImpactFeedbackGenerator(style: .light)
var body: some View {
VStack(alignment: .leading, spacing: 16) {
// Page header
VStack(alignment: .leading, spacing: 4) {
Text("Inventory")
.font(.system(size: 28, weight: .bold))
.foregroundStyle(VelocityTheme.foreground)
Text("Sunseeker · Dream Weaver · Dollhouse")
.font(.system(size: 12))
.foregroundStyle(VelocityTheme.mutedFg)
}
.padding(.horizontal, 20)
.padding(.top, 20)
Picker("Mode", selection: $store.mode) {
ForEach(InventoryStore.Mode.allCases) { mode in
Text(mode.rawValue).tag(mode)
}
}
.pickerStyle(.segmented)
.padding(.horizontal, 20)
.padding(.top, 12)
Group {
switch store.mode {
case .sunseeker:
#if targetEnvironment(simulator)
ZStack {
VStack(spacing: 14) {
Image(systemName: "camera.metering.unknown")
.font(.system(size: 40))
.foregroundStyle(VelocityTheme.mutedFg)
Text("AR Not Available in Simulator")
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(VelocityTheme.foreground)
Text("Sunseeker requires a real device with a camera and compass. Run on iPhone or iPad to use this feature.")
.font(.system(size: 13))
.foregroundStyle(VelocityTheme.mutedFg)
.multilineTextAlignment(.center)
}
.padding(24)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(red: 0.031, green: 0.039, blue: 0.071))
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(VelocityTheme.borderAccent, lineWidth: 1)
)
)
#else
SunseekerPanel(sunNodesReady: $store.sunNodesReady)
#endif
case .dreamWeaver:
DreamWeaverPanel(
sourceImage: $store.sourceImage,
generatedImage: $store.generatedImage,
selectedPrompt: $store.selectedPrompt,
isProcessing: $store.isProcessing,
prompts: store.prompts,
showCamera: $showCamera
)
case .dollhouse:
DollhousePanel(hour: $store.dollhouseHour, tickHour: $sliderTickHour, haptics: haptics)
}
}
.padding(.horizontal, 20)
.padding(.bottom, 20)
.animation(.easeInOut(duration: 0.25), value: store.mode)
}
.background(VelocityTheme.background)
.onAppear {
// Dark-theme the segmented control
UISegmentedControl.appearance().selectedSegmentTintColor = UIColor(
red: 0.231, green: 0.510, blue: 0.965, alpha: 0.85)
UISegmentedControl.appearance().setTitleTextAttributes(
[.foregroundColor: UIColor.white], for: .selected)
UISegmentedControl.appearance().setTitleTextAttributes(
[.foregroundColor: UIColor(white: 0.62, alpha: 1)], for: .normal)
UISegmentedControl.appearance().backgroundColor = UIColor(
red: 0.031, green: 0.039, blue: 0.071, alpha: 1)
}
.sheet(isPresented: $showCamera) {
CameraPicker(image: $store.sourceImage, isPresented: $showCamera)
}
}
}
private struct SunseekerPanel: View {
@Binding var sunNodesReady: Bool
var body: some View {
ZStack(alignment: .topLeading) {
ARSunOverlayView(sunNodesReady: $sunNodesReady)
.clipShape(RoundedRectangle(cornerRadius: 20))
DashedSunLine()
.stroke(Color.yellow.opacity(0.9), style: StrokeStyle(lineWidth: 3, dash: [10, 8]))
.padding(.horizontal, 24)
.padding(.vertical, 80)
VStack(alignment: .leading, spacing: 8) {
Text("Sunseeker")
.font(.headline)
Text("Point the iPad toward windows to inspect yearly sun-entry path.")
.font(.subheadline)
.foregroundStyle(.secondary)
}
.padding(14)
.background {
GlassBlurView(style: .systemThinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 14))
}
.padding(20)
}
}
}
private struct DreamWeaverPanel: View {
@Binding var sourceImage: UIImage?
@Binding var generatedImage: UIImage?
@Binding var selectedPrompt: String
@Binding var isProcessing: Bool
let prompts: [String]
@Binding var showCamera: Bool
var body: some View {
VStack(spacing: 14) {
ZStack {
RoundedRectangle(cornerRadius: 20)
.fill(Color.black.opacity(0.9))
if let sourceImage {
Image(uiImage: sourceImage)
.resizable()
.scaledToFit()
.clipShape(RoundedRectangle(cornerRadius: 16))
.padding(12)
} else {
ContentUnavailableView("No Capture", systemImage: "camera.viewfinder", description: Text("Tap Capture to snap a room."))
.foregroundStyle(.white)
}
if let generatedImage {
Image(uiImage: generatedImage)
.resizable()
.scaledToFit()
.clipShape(RoundedRectangle(cornerRadius: 16))
.padding(12)
.transition(.opacity)
}
if isProcessing {
ProcessingOverlay()
}
}
.frame(maxWidth: .infinity, minHeight: 420)
.animation(.easeInOut(duration: 0.35), value: generatedImage)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 10) {
ForEach(prompts, id: \.self) { prompt in
Text(prompt)
.font(.subheadline.weight(.semibold))
.padding(.horizontal, 14)
.padding(.vertical, 10)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(prompt == selectedPrompt ? Color.yellow.opacity(0.85) : Color.white.opacity(0.12))
)
.onTapGesture { selectedPrompt = prompt }
}
}
}
HStack(spacing: 12) {
Button("Capture") {
showCamera = true
}
.buttonStyle(.borderedProminent)
Button("Reimagine") {
Task { await generate() }
}
.buttonStyle(.bordered)
.disabled(sourceImage == nil || isProcessing)
}
}
.padding(16)
.background {
GlassBlurView(style: .systemThinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 18))
}
}
@MainActor
private func generate() async {
guard let sourceImage, !isProcessing else { return }
isProcessing = true
do {
let result = try await ComfyClient.shared.generateImage(source: sourceImage, prompt: selectedPrompt)
withAnimation(.easeInOut(duration: 0.4)) {
generatedImage = result
}
} catch {
print("Dream Weaver error: \(error)")
}
isProcessing = false
}
}
private struct DollhousePanel: View {
@Binding var hour: Double
@Binding var tickHour: Int
let haptics: UIImpactFeedbackGenerator
var body: some View {
VStack(spacing: 12) {
SceneKitDollhouseView(hour: $hour)
.clipShape(RoundedRectangle(cornerRadius: 20))
.frame(maxWidth: .infinity, minHeight: 460)
VStack(alignment: .leading, spacing: 8) {
Text(String(format: "Time: %02d:00", Int(hour.rounded())))
.font(.headline)
Slider(value: $hour, in: 0...24, step: 0.25)
.onChange(of: hour) { _, newValue in
let rounded = Int(newValue.rounded())
if rounded != tickHour {
tickHour = rounded
haptics.impactOccurred(intensity: 0.7)
}
}
}
.padding(14)
.background {
GlassBlurView(style: .systemThinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 14))
}
}
}
}
private struct SceneKitDollhouseView: UIViewRepresentable {
@Binding var hour: Double
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIView(context: Context) -> SCNView {
let view = SCNView()
view.scene = context.coordinator.scene
view.autoenablesDefaultLighting = false
view.allowsCameraControl = true
view.backgroundColor = UIColor.systemBackground
context.coordinator.setupScene()
context.coordinator.updateSunLight(hour: hour)
return view
}
func updateUIView(_ uiView: SCNView, context: Context) {
context.coordinator.updateSunLight(hour: hour)
}
final class Coordinator {
let scene = SCNScene()
private let sunNode = SCNNode()
func setupScene() {
if let modelScene = SCNScene(named: "Building.usdz") ?? SCNScene(named: "Building.scn") {
let container = SCNNode()
for child in modelScene.rootNode.childNodes {
container.addChildNode(child.clone())
}
scene.rootNode.addChildNode(container)
} else {
let fallback = SCNFloor()
fallback.firstMaterial?.diffuse.contents = UIColor.secondarySystemBackground
scene.rootNode.addChildNode(SCNNode(geometry: fallback))
}
let camera = SCNCamera()
let cameraNode = SCNNode()
cameraNode.camera = camera
cameraNode.position = SCNVector3(0, 4, 10)
scene.rootNode.addChildNode(cameraNode)
let light = SCNLight()
light.type = .directional
light.intensity = 1_200
light.castsShadow = true
sunNode.light = light
scene.rootNode.addChildNode(sunNode)
let ambient = SCNLight()
ambient.type = .ambient
ambient.intensity = 200
let ambientNode = SCNNode()
ambientNode.light = ambient
scene.rootNode.addChildNode(ambientNode)
}
func updateSunLight(hour: Double) {
let normalized = (hour / 24.0) * (2 * Double.pi)
let x = Float(cos(normalized) * 8.0)
let y = Float(max(sin(normalized) * 8.0, 1.0))
let z = Float(sin(normalized + .pi / 3) * 6.0)
sunNode.position = SCNVector3(x, y, z)
sunNode.look(at: SCNVector3(0, 0, 0))
}
}
}
private struct ProcessingOverlay: View {
@State private var animate = false
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 16)
.fill(.black.opacity(0.45))
Text("AI Processing...")
.font(.headline.weight(.bold))
.foregroundStyle(.white)
.padding(.horizontal, 20)
.padding(.vertical, 12)
.background {
GlassBlurView(style: .systemUltraThinMaterialDark)
.clipShape(Capsule())
}
.overlay(
Rectangle()
.fill(
LinearGradient(
colors: [.clear, .white.opacity(0.6), .clear],
startPoint: .leading,
endPoint: .trailing
)
)
.rotationEffect(.degrees(18))
.offset(x: animate ? 160 : -160)
.animation(.linear(duration: 1.2).repeatForever(autoreverses: false), value: animate)
.blendMode(.screen)
.mask(Capsule().frame(height: 44))
)
}
.padding(12)
.onAppear { animate = true }
}
}
private struct DashedSunLine: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.minX, y: rect.maxY * 0.75))
path.addQuadCurve(
to: CGPoint(x: rect.maxX, y: rect.maxY * 0.25),
control: CGPoint(x: rect.midX, y: rect.minY + 30)
)
return path
}
}
private struct CameraPicker: UIViewControllerRepresentable {
@Binding var image: UIImage?
@Binding var isPresented: Bool
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
if UIImagePickerController.isSourceTypeAvailable(.camera) {
picker.sourceType = .camera
picker.cameraCaptureMode = .photo
} else {
picker.sourceType = .photoLibrary
}
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
final class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
private let parent: CameraPicker
init(_ parent: CameraPicker) {
self.parent = parent
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
parent.isPresented = false
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
if let captured = info[.originalImage] as? UIImage {
parent.image = captured
}
parent.isPresented = false
}
}
}

View File

@@ -0,0 +1,960 @@
import SwiftUI
// MARK: Oracle Canvas Modes
enum OracleMode: String, CaseIterable {
case pipeline = "Pipeline"
case teamPerformance = "Team Performance"
case accountTimeline = "Account Timeline"
case leadMap = "Lead Map"
case calendarTasks = "Calendar & Tasks"
var icon: String {
switch self {
case .pipeline: return "square.grid.3x1.below.line.grid.1x2"
case .teamPerformance: return "person.3"
case .accountTimeline: return "clock.arrow.circlepath"
case .leadMap: return "map"
case .calendarTasks: return "calendar"
}
}
}
// MARK: Pipeline mock data (extended with detail fields)
struct OracleLeadCard: Identifiable {
let id = UUID()
let initials: String
let name: String
let company: String
let value: String
let status: LeadStatus
let phone: String
let interest: String
let qualification: String
}
private let pipelineData: [(stage: String, cards: [OracleLeadCard])] = [
("New", [
OracleLeadCard(initials: "JW", name: "James Wilson", company: "Website", value: "AED 3.5M", status: .new,
phone: "+971 52 456 7890", interest: "1BR Investment", qualification: "Potential"),
]),
("Qualified", [
OracleLeadCard(initials: "FH", name: "Fatima Hassan", company: "WhatsApp", value: "AED 12M", status: .qualified,
phone: "+971 54 321 0987", interest: "3BR + Maid", qualification: "Whale"),
OracleLeadCard(initials: "SC", name: "Sarah Chen", company: "Walk-in", value: "AED 6.5M", status: .engaged,
phone: "+971 50 987 6543", interest: "2BR Sea View", qualification: "Potential"),
]),
("Proposal", [
OracleLeadCard(initials: "MA", name: "Mohammed Al-Rashid", company: "WhatsApp", value: "AED 15M+", status: .hot,
phone: "+971 55 123 4567", interest: "Penthouse Suite", qualification: "Whale"),
]),
("Closed", [
OracleLeadCard(initials: "DK", name: "David Kumar", company: "Walk-in", value: "AED 20M", status: .closed,
phone: "+971 56 789 0123", interest: "Full Floor", qualification: "Whale"),
]),
]
struct TeamMemberData: Identifiable {
let id = UUID()
let initials: String; let name: String; let deals: Int; let revenue: String; let trend: String
}
private let teamData: [TeamMemberData] = [
.init(initials: "RA", name: "Rania Al-Farsi", deals: 42, revenue: "$2.1M", trend: "↑ 18%"),
.init(initials: "KM", name: "Khaled Mensah", deals: 31, revenue: "$1.6M", trend: "↑ 12%"),
.init(initials: "LT", name: "Lina Torres", deals: 28, revenue: "$1.3M", trend: "→ 2%"),
.init(initials: "AH", name: "Ahmed Hassan", deals: 19, revenue: "$0.9M", trend: "↓ 5%"),
]
struct OracleTimelineEvent: Identifiable {
let id = UUID()
let badge: String; let summary: String; let when: String; let detail: String
}
private let timelineEvents: [OracleTimelineEvent] = [
.init(badge: "MEETING", summary: "VR Amenity Tour Apex Innovations", when: "2h ago",
detail: "CFO and Legal Director attended. Strong reaction to the panoramic sea view suite. Follow-up proposal requested by Tuesday."),
.init(badge: "EMAIL", summary: "Proposal deck sent to legal team", when: "Yesterday",
detail: "58-page proposal + payment plan schedule sent via DocuSign. Legal team has 5 business days to review."),
.init(badge: "CALL", summary: "Budget discussion CFO confirmed", when: "Mon",
detail: "Budget ceiling confirmed at AED 15M+. CFO expressed strong preference for a penthouse unit with private terrace."),
.init(badge: "VISIT", summary: "Site walkthrough Penthouse Suite", when: "Last week",
detail: "First in-person visit. 45-minute walkthrough of Penthouse A & B. Visitor dwell time analysis showed 'excited' sentiment for 90% of the visit."),
]
struct RegionPin: Identifiable {
let id = UUID()
let label: String; let country: String; let count: Int; let temp: String; let topLead: String
}
private let mapPins: [RegionPin] = [
.init(label: "UAE", country: "🇦🇪", count: 8, temp: "hot", topLead: "Mohammed Al-Rashid"),
.init(label: "Saudi Arabia", country: "🇸🇦", count: 5, temp: "warm", topLead: "Al-Mansour Group"),
.init(label: "UK", country: "🇬🇧", count: 3, temp: "cold", topLead: "Rexford Capital"),
.init(label: "USA", country: "🇺🇸", count: 4, temp: "warm", topLead: "Apex Innovations"),
.init(label: "India", country: "🇮🇳", count: 6, temp: "hot", topLead: "Starlight Systems"),
.init(label: "Germany", country: "🇩🇪", count: 2, temp: "cold", topLead: "TechWave GmbH"),
]
struct CalTask: Identifiable {
let id = UUID()
let title: String; let subtitle: String; let due: String
}
private let calTasks: [CalTask] = [
.init(title: "Follow up with Mohammed", subtitle: "High-value penthouse lead 2 unread messages", due: "Today 3 PM"),
.init(title: "Send contract to Fatima", subtitle: "3BR unit finalised payment plan to confirm", due: "Tomorrow 10 AM"),
.init(title: "Schedule VR tour James", subtitle: "Website lead, potential 1BR investor", due: "Thu 2 PM"),
]
// MARK: OracleView (main)
struct OracleView: View {
@State private var selectedMode: OracleMode = .pipeline
@State private var prompt = "Show me a pipeline view by stage for Q4."
@State private var insightText = "Pipeline is healthy. Mohammed Al-Rashid is your highest-value close opportunity — follow up within 24 hours."
@State private var isSubmitting = false
// Sheet states
@State private var selectedLead: OracleLeadCard? = nil
@State private var selectedMember: TeamMemberData? = nil
@State private var selectedRegion: RegionPin? = nil
@State private var scheduledTask: CalTask? = nil
@State private var showScheduleConfirm = false
var body: some View {
ZStack(alignment: .bottom) {
VStack(alignment: .leading, spacing: 0) {
pageHeader
.padding(.horizontal, 24).padding(.top, 24).padding(.bottom, 16)
insightCard
.padding(.horizontal, 24).padding(.bottom, 14)
ScrollView {
canvasView
.padding(.horizontal, 24)
.padding(.bottom, 120)
}
}
promptBar
.padding(.horizontal, 20)
.padding(.bottom, 12)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.background(VelocityTheme.background)
// Lead detail sheet
.sheet(item: $selectedLead) { card in
LeadDetailSheet(card: card)
}
// Team member sheet
.sheet(item: $selectedMember) { member in
MemberDetailSheet(member: member)
}
// Region callout sheet
.sheet(item: $selectedRegion) { pin in
RegionDetailSheet(pin: pin)
}
// Schedule confirmation alert
.alert("Confirm Schedule",
isPresented: $showScheduleConfirm,
presenting: scheduledTask) { task in
Button("Schedule") {
// In a real app this would create a calendar event
}
Button("Cancel", role: .cancel) {}
} message: { task in
Text("Add \"\(task.title)\" to your calendar for \(task.due)?")
}
}
// MARK: Sub-views
private var pageHeader: some View {
HStack {
VStack(alignment: .leading, spacing: 3) {
Text("Oracle").font(.system(size: 28, weight: .bold)).foregroundStyle(VelocityTheme.foreground)
Text("AI intelligence pipeline").font(.system(size: 12)).foregroundStyle(VelocityTheme.mutedFg)
}
Spacer()
if isSubmitting {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: VelocityTheme.accent))
.scaleEffect(0.8)
}
}
}
private var insightCard: some View {
HStack(alignment: .center, spacing: 0) {
RoundedRectangle(cornerRadius: 2)
.fill(LinearGradient(colors: [Color(red: 0.58, green: 0.77, blue: 0.99), VelocityTheme.accent],
startPoint: .top, endPoint: .bottom))
.frame(width: 3)
HStack {
VStack(alignment: .leading, spacing: 3) {
Text("AI INSIGHT").font(.system(size: 9, weight: .semibold)).tracking(1.5)
.foregroundStyle(VelocityTheme.accent)
Text(insightText).font(.system(size: 13)).foregroundStyle(VelocityTheme.foreground).lineLimit(2)
}
Spacer()
HStack(spacing: 5) {
Image(systemName: selectedMode.icon).font(.system(size: 11)).foregroundStyle(VelocityTheme.accent)
Text(selectedMode.rawValue).font(.system(size: 11, weight: .medium))
.foregroundStyle(Color(red: 0.58, green: 0.77, blue: 0.99))
}
}
.padding(.horizontal, 14).padding(.vertical, 12)
}
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color(red: 0.09, green: 0.15, blue: 0.33).opacity(0.55))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(VelocityTheme.borderAccent, lineWidth: 1))
)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
@ViewBuilder
private var canvasView: some View {
switch selectedMode {
case .pipeline:
PipelineCanvas(onSelectLead: { selectedLead = $0 })
case .teamPerformance:
TeamPerformanceCanvas(onSelectMember: { selectedMember = $0 })
case .accountTimeline:
AccountTimelineCanvas()
case .leadMap:
LeadMapCanvas(onSelectRegion: { selectedRegion = $0 })
case .calendarTasks:
CalendarCanvas(onSchedule: { task in
scheduledTask = task
showScheduleConfirm = true
})
}
}
// MARK: Prompt Bar
private var promptBar: some View {
VStack(spacing: 0) {
TextField("Ask Oracle anything…", text: $prompt)
.font(.system(size: 14))
.foregroundStyle(VelocityTheme.foreground)
.tint(VelocityTheme.accent)
.onSubmit { submitPrompt() }
.padding(.horizontal, 16).padding(.top, 12).padding(.bottom, 8)
HStack {
Menu {
ForEach(OracleMode.allCases, id: \.self) { mode in
Button {
selectedMode = mode
prompt = modeSamplePrompt(mode)
insightText = oracleInsight(for: mode)
} label: {
Label(mode.rawValue, systemImage: mode.icon)
}
}
} label: {
HStack(spacing: 5) {
Image(systemName: selectedMode.icon).font(.system(size: 10))
Text(selectedMode.rawValue).font(.system(size: 11, weight: .medium))
Image(systemName: "chevron.down").font(.system(size: 8))
}
.foregroundStyle(Color(red: 0.58, green: 0.77, blue: 0.99))
.padding(.horizontal, 10).padding(.vertical, 6)
.background(Capsule().fill(VelocityTheme.accent.opacity(0.14))
.overlay(Capsule().stroke(VelocityTheme.accent.opacity(0.3), lineWidth: 1)))
}
Spacer()
Button(action: submitPrompt) {
ZStack {
Circle()
.fill(isSubmitting ? VelocityTheme.mutedFg : VelocityTheme.accent)
.shadow(color: VelocityTheme.accent.opacity(0.5), radius: 8)
if isSubmitting {
ProgressView().progressViewStyle(CircularProgressViewStyle(tint: .white)).scaleEffect(0.6)
} else {
Image(systemName: "paperplane.fill").font(.system(size: 12)).foregroundStyle(.white)
}
}
.frame(width: 34, height: 34)
}
.disabled(isSubmitting || prompt.trimmingCharacters(in: .whitespaces).isEmpty)
}
.padding(.horizontal, 12).padding(.bottom, 12)
}
.background(
RoundedRectangle(cornerRadius: 18)
.fill(Color(red: 0.039, green: 0.043, blue: 0.063).opacity(0.95))
.overlay(RoundedRectangle(cornerRadius: 18).stroke(Color.white.opacity(0.11), lineWidth: 1))
.shadow(color: .black.opacity(0.6), radius: 20, y: -4)
)
}
// MARK: Prompt logic
private func submitPrompt() {
let clean = prompt.trimmingCharacters(in: .whitespaces)
guard !clean.isEmpty && !isSubmitting else { return }
isSubmitting = true
let lower = clean.lowercased()
if lower.contains("team") || lower.contains("performance") || lower.contains("sales") {
selectedMode = .teamPerformance
} else if lower.contains("account") || lower.contains("apex") || lower.contains("timeline") {
selectedMode = .accountTimeline
} else if lower.contains("map") || lower.contains("geographic") || lower.contains("location") {
selectedMode = .leadMap
} else if lower.contains("calendar") || lower.contains("schedule") || lower.contains("task") {
selectedMode = .calendarTasks
} else {
selectedMode = .pipeline
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.9) {
withAnimation(.easeInOut(duration: 0.3)) {
insightText = oracleInsight(for: selectedMode)
isSubmitting = false
}
}
}
private func modeSamplePrompt(_ mode: OracleMode) -> String {
switch mode {
case .pipeline: return "Show me a pipeline view by stage for Q4."
case .teamPerformance: return "What's the performance of the sales team this month?"
case .accountTimeline: return "Find all contacts at Apex Innovations and their recent activity."
case .leadMap: return "Give me a geographic map of all leads."
case .calendarTasks: return "Schedule follow-ups with the top 3 high-value leads."
}
}
private func oracleInsight(for mode: OracleMode) -> String {
switch mode {
case .pipeline: return "Pipeline is healthy. Mohammed Al-Rashid is your highest-value close opportunity — follow up within 24 hours."
case .teamPerformance: return "Rania Al-Farsi leads the team at $2.1M closed. Overall quota attainment is at 87% — ahead of last month."
case .accountTimeline: return "Apex Innovations has 4 active stakeholders. Confusion detected in legal stage — recommend expediting contract review."
case .leadMap: return "UAE and India show the hottest lead concentration. 8 high-value prospects in UAE require immediate outreach."
case .calendarTasks: return "3 high-priority follow-ups scheduled. Mohammed Al-Rashid has 2 unread messages — respond within 24h."
}
}
}
// MARK: Pipeline Canvas
private struct PipelineCanvas: View {
let onSelectLead: (OracleLeadCard) -> Void
private let cols = [GridItem(.adaptive(minimum: 200), spacing: 12)]
var body: some View {
LazyVGrid(columns: cols, alignment: .leading, spacing: 12) {
ForEach(pipelineData, id: \.stage) { col in
VStack(alignment: .leading, spacing: 10) {
HStack {
Text(col.stage.uppercased())
.font(.system(size: 10, weight: .semibold)).tracking(1)
.foregroundStyle(VelocityTheme.mutedFg)
Spacer()
Text("\(col.cards.count)")
.font(.system(size: 10, weight: .semibold))
.foregroundStyle(VelocityTheme.accent)
.padding(.horizontal, 7).padding(.vertical, 3)
.background(Capsule().fill(VelocityTheme.accent.opacity(0.12))
.overlay(Capsule().stroke(VelocityTheme.accent.opacity(0.2), lineWidth: 1)))
}
ForEach(col.cards) { card in
TappableLeadCard(card: card, onTap: { onSelectLead(card) })
}
}
.padding(16)
.background(
RoundedRectangle(cornerRadius: 14)
.fill(Color(red: 0.031, green: 0.039, blue: 0.071))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(VelocityTheme.borderSubtle, lineWidth: 1))
)
}
}
}
}
private struct TappableLeadCard: View {
let card: OracleLeadCard
let onTap: () -> Void
@State private var pressed = false
var body: some View {
HStack(spacing: 10) {
ZStack {
RoundedRectangle(cornerRadius: 8).fill(card.status.color.opacity(0.18)).frame(width: 36, height: 36)
Text(card.initials).font(.system(size: 12, weight: .bold)).foregroundStyle(card.status.color)
}
VStack(alignment: .leading, spacing: 2) {
Text(card.name).font(.system(size: 12, weight: .medium)).foregroundStyle(VelocityTheme.foreground)
Text(card.company).font(.system(size: 10)).foregroundStyle(VelocityTheme.mutedFg)
}
Spacer()
VStack(alignment: .trailing, spacing: 2) {
Text(card.value).font(.system(size: 11, weight: .semibold)).foregroundStyle(VelocityTheme.accent)
Image(systemName: "chevron.right").font(.system(size: 9)).foregroundStyle(VelocityTheme.mutedFg)
}
}
.padding(10)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(pressed ? VelocityTheme.accent.opacity(0.10) : Color.white.opacity(0.04))
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(pressed ? VelocityTheme.accent.opacity(0.35) : Color.white.opacity(0.06), lineWidth: 1))
)
.scaleEffect(pressed ? 0.97 : 1.0)
.animation(.easeInOut(duration: 0.12), value: pressed)
.onTapGesture {
pressed = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) { pressed = false }
onTap()
}
}
}
// MARK: Lead Detail Sheet
private struct LeadDetailSheet: View {
let card: OracleLeadCard
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationStack {
VStack(alignment: .leading, spacing: 20) {
// Avatar + name
HStack(spacing: 16) {
ZStack {
RoundedRectangle(cornerRadius: 14).fill(card.status.color.opacity(0.20)).frame(width: 60, height: 60)
Text(card.initials).font(.system(size: 22, weight: .bold)).foregroundStyle(card.status.color)
}
VStack(alignment: .leading, spacing: 4) {
Text(card.name).font(.system(size: 20, weight: .bold)).foregroundStyle(VelocityTheme.foreground)
HStack(spacing: 6) {
Text(card.status.rawValue)
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(card.status.color)
.padding(.horizontal, 8).padding(.vertical, 3)
.background(Capsule().fill(card.status.color.opacity(0.14)))
Text(card.qualification).font(.system(size: 11)).foregroundStyle(VelocityTheme.mutedFg)
}
}
}
.padding(.top, 8)
Divider().background(VelocityTheme.borderSubtle)
// Details grid
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 14) {
DetailField(label: "Deal Value", value: card.value)
DetailField(label: "Source", value: card.company)
DetailField(label: "Interest", value: card.interest)
DetailField(label: "Phone", value: card.phone)
}
Divider().background(VelocityTheme.borderSubtle)
// Action buttons
HStack(spacing: 12) {
ActionChip(icon: "phone.fill", label: "Call", color: VelocityTheme.success)
ActionChip(icon: "message.fill", label: "Message", color: VelocityTheme.accent)
ActionChip(icon: "calendar.badge.plus", label: "Schedule", color: Color(red: 0.60, green: 0.57, blue: 0.99))
}
Spacer()
}
.padding(24)
.background(VelocityTheme.background.ignoresSafeArea())
.navigationTitle("Lead Details")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") { dismiss() }
.foregroundStyle(VelocityTheme.accent)
}
}
}
}
}
private struct DetailField: View {
let label: String; let value: String
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(label.uppercased()).font(.system(size: 9, weight: .semibold)).tracking(1)
.foregroundStyle(VelocityTheme.mutedFg)
Text(value).font(.system(size: 13, weight: .medium)).foregroundStyle(VelocityTheme.foreground)
}
.padding(12)
.frame(maxWidth: .infinity, alignment: .leading)
.background(RoundedRectangle(cornerRadius: 10).fill(Color.white.opacity(0.04))
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.white.opacity(0.07), lineWidth: 1)))
}
}
private struct ActionChip: View {
let icon: String; let label: String; let color: Color
@State private var pressed = false
var body: some View {
HStack(spacing: 6) {
Image(systemName: icon).font(.system(size: 12))
Text(label).font(.system(size: 12, weight: .semibold))
}
.foregroundStyle(color)
.padding(.horizontal, 16).padding(.vertical, 10)
.frame(maxWidth: .infinity)
.background(RoundedRectangle(cornerRadius: 10).fill(color.opacity(pressed ? 0.22 : 0.13))
.overlay(RoundedRectangle(cornerRadius: 10).stroke(color.opacity(0.30), lineWidth: 1)))
.scaleEffect(pressed ? 0.96 : 1.0)
.animation(.easeInOut(duration: 0.12), value: pressed)
.onTapGesture { pressed = true; DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { pressed = false } }
}
}
// MARK: Team Performance Canvas
private struct TeamPerformanceCanvas: View {
let onSelectMember: (TeamMemberData) -> Void
var body: some View {
VStack(spacing: 14) {
quotaPanel
teamListPanel
}
}
private var quotaPanel: some View {
HStack(spacing: 14) {
ZStack {
Circle().stroke(Color.white.opacity(0.06), lineWidth: 10).frame(width: 110, height: 110)
Circle()
.trim(from: 0, to: 0.87)
.stroke(AngularGradient(colors: [VelocityTheme.accent, Color(red: 0.13, green: 0.83, blue: 0.93)],
center: .center),
style: StrokeStyle(lineWidth: 10, lineCap: .round))
.rotationEffect(.degrees(-90))
.frame(width: 110, height: 110)
VStack(spacing: 2) {
Text("87%").font(.system(size: 26, weight: .bold)).foregroundStyle(VelocityTheme.foreground)
Text("QUOTA").font(.system(size: 8, weight: .semibold)).tracking(1.2).foregroundStyle(VelocityTheme.accent)
}
}
VStack(alignment: .leading, spacing: 4) {
Text("Quota Attainment").font(.system(size: 13, weight: .semibold)).foregroundStyle(VelocityTheme.foreground)
Text("Monthly target exceeded").font(.system(size: 11)).foregroundStyle(VelocityTheme.success)
Text("Q4 FY202526").font(.system(size: 11)).foregroundStyle(VelocityTheme.mutedFg)
}
Spacer()
}
.padding(20)
.background(RoundedRectangle(cornerRadius: 14).fill(Color(red: 0.031, green: 0.039, blue: 0.071))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(VelocityTheme.borderAccent, lineWidth: 1)))
}
private var teamListPanel: some View {
VStack(alignment: .leading, spacing: 2) {
Text("TEAM PERFORMANCE").font(.system(size: 10, weight: .semibold)).tracking(1.2)
.foregroundStyle(VelocityTheme.mutedFg).padding(.bottom, 8)
ForEach(teamData) { member in
TappableTeamRow(member: member, onTap: { onSelectMember(member) })
}
}
.padding(20)
.background(RoundedRectangle(cornerRadius: 14).fill(Color(red: 0.031, green: 0.039, blue: 0.071))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(VelocityTheme.borderAccent, lineWidth: 1)))
}
}
private struct TappableTeamRow: View {
let member: TeamMemberData
let onTap: () -> Void
@State private var pressed = false
var body: some View {
HStack(spacing: 12) {
ZStack {
RoundedRectangle(cornerRadius: 8).fill(VelocityTheme.accent.opacity(0.18)).frame(width: 36, height: 36)
Text(member.initials).font(.system(size: 12, weight: .bold)).foregroundStyle(VelocityTheme.accent)
}
VStack(alignment: .leading, spacing: 2) {
Text(member.name).font(.system(size: 13, weight: .medium)).foregroundStyle(VelocityTheme.foreground)
Text("\(member.deals) deals closed").font(.system(size: 11)).foregroundStyle(VelocityTheme.mutedFg)
}
Spacer()
VStack(alignment: .trailing, spacing: 2) {
Text(member.revenue).font(.system(size: 12, weight: .semibold)).foregroundStyle(VelocityTheme.accent)
Text(member.trend)
.font(.system(size: 10, weight: .medium))
.foregroundStyle(member.trend.hasPrefix("") ? VelocityTheme.success :
member.trend.hasPrefix("") ? VelocityTheme.danger : VelocityTheme.mutedFg)
}
Image(systemName: "chevron.right").font(.system(size: 10)).foregroundStyle(VelocityTheme.mutedFg)
}
.padding(12)
.background(RoundedRectangle(cornerRadius: 10)
.fill(pressed ? VelocityTheme.accent.opacity(0.08) : Color.white.opacity(0.04))
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(pressed ? VelocityTheme.accent.opacity(0.25) : Color.white.opacity(0.06), lineWidth: 1)))
.scaleEffect(pressed ? 0.98 : 1.0)
.animation(.easeInOut(duration: 0.12), value: pressed)
.onTapGesture {
pressed = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) { pressed = false }
onTap()
}
}
}
// MARK: Team Member Detail Sheet
private struct MemberDetailSheet: View {
let member: TeamMemberData
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationStack {
VStack(alignment: .leading, spacing: 20) {
HStack(spacing: 16) {
ZStack {
RoundedRectangle(cornerRadius: 14).fill(VelocityTheme.accent.opacity(0.18)).frame(width: 60, height: 60)
Text(member.initials).font(.system(size: 22, weight: .bold)).foregroundStyle(VelocityTheme.accent)
}
VStack(alignment: .leading, spacing: 4) {
Text(member.name).font(.system(size: 20, weight: .bold)).foregroundStyle(VelocityTheme.foreground)
Text("Sales Executive").font(.system(size: 12)).foregroundStyle(VelocityTheme.mutedFg)
}
}
.padding(.top, 8)
Divider().background(VelocityTheme.borderSubtle)
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 14) {
DetailField(label: "Revenue Closed", value: member.revenue)
DetailField(label: "Deals Closed", value: "\(member.deals)")
DetailField(label: "Trend", value: member.trend)
DetailField(label: "Period", value: "Q4 FY202526")
}
Spacer()
}
.padding(24)
.background(VelocityTheme.background.ignoresSafeArea())
.navigationTitle("Team Member")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") { dismiss() }.foregroundStyle(VelocityTheme.accent)
}
}
}
}
}
// MARK: Account Timeline Canvas
private struct AccountTimelineCanvas: View {
@State private var expandedId: UUID? = nil
var body: some View {
VStack(spacing: 14) {
// Account overview
VStack(alignment: .leading, spacing: 12) {
Text("ACCOUNT OVERVIEW").font(.system(size: 9, weight: .semibold)).tracking(1.5).foregroundStyle(VelocityTheme.accent)
Text("Apex Innovations").font(.system(size: 24, weight: .bold)).foregroundStyle(VelocityTheme.foreground)
HStack(spacing: 14) {
InfoMini(label: "Deal Value", value: "AED 15M+")
InfoMini(label: "Primary Contact", value: "CEO James T.")
InfoMini(label: "Industry", value: "Technology")
}
}
.padding(20)
.background(RoundedRectangle(cornerRadius: 14).fill(Color(red: 0.031, green: 0.039, blue: 0.071))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(VelocityTheme.borderAccent, lineWidth: 1)))
// Expandable timeline
VStack(alignment: .leading, spacing: 0) {
Text("Activity Timeline").font(.system(size: 13, weight: .semibold))
.foregroundStyle(VelocityTheme.foreground).padding(.bottom, 16)
ForEach(Array(timelineEvents.enumerated()), id: \.offset) { i, event in
TimelineEventRow(event: event, isLast: i == timelineEvents.count - 1,
isExpanded: expandedId == event.id) {
withAnimation(.easeInOut(duration: 0.25)) {
expandedId = expandedId == event.id ? nil : event.id
}
}
}
}
.padding(20)
.background(RoundedRectangle(cornerRadius: 14).fill(Color(red: 0.031, green: 0.039, blue: 0.071))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(VelocityTheme.borderAccent, lineWidth: 1)))
}
}
}
private struct TimelineEventRow: View {
let event: OracleTimelineEvent
let isLast: Bool
let isExpanded: Bool
let onTap: () -> Void
var body: some View {
HStack(alignment: .top, spacing: 14) {
VStack(spacing: 0) {
Circle().fill(VelocityTheme.accent).frame(width: 10, height: 10)
.overlay(Circle().stroke(VelocityTheme.background, lineWidth: 2))
if !isLast {
Rectangle()
.fill(LinearGradient(colors: [VelocityTheme.accent.opacity(0.5), .clear],
startPoint: .top, endPoint: .bottom))
.frame(width: 2)
.frame(height: isExpanded ? 100 : 50)
.animation(.easeInOut(duration: 0.25), value: isExpanded)
}
}
VStack(alignment: .leading, spacing: 6) {
HStack {
Text(event.badge).font(.system(size: 9, weight: .bold)).tracking(1.2)
.foregroundStyle(VelocityTheme.accent)
.padding(.horizontal, 7).padding(.vertical, 3)
.background(RoundedRectangle(cornerRadius: 4).fill(VelocityTheme.accent.opacity(0.15))
.overlay(RoundedRectangle(cornerRadius: 4).stroke(VelocityTheme.accent.opacity(0.25), lineWidth: 1)))
Spacer()
Text(event.when).font(.system(size: 10)).foregroundStyle(VelocityTheme.mutedFg)
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
.font(.system(size: 9)).foregroundStyle(VelocityTheme.mutedFg)
}
Text(event.summary).font(.system(size: 13, weight: .medium)).foregroundStyle(VelocityTheme.foreground)
if isExpanded {
Text(event.detail).font(.system(size: 12)).foregroundStyle(VelocityTheme.mutedFg)
.padding(.top, 4)
.transition(.opacity.combined(with: .move(edge: .top)))
}
}
.padding(12)
.background(RoundedRectangle(cornerRadius: 10)
.fill(isExpanded ? VelocityTheme.accent.opacity(0.06) : Color.white.opacity(0.04))
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(isExpanded ? VelocityTheme.accent.opacity(0.2) : Color.white.opacity(0.06), lineWidth: 1)))
.onTapGesture { onTap() }
.padding(.bottom, 8)
}
}
}
private struct InfoMini: View {
let label: String; let value: String
var body: some View {
VStack(alignment: .leading, spacing: 2) {
Text(label).font(.system(size: 9)).tracking(0.8).foregroundStyle(VelocityTheme.mutedFg)
Text(value).font(.system(size: 12, weight: .semibold)).foregroundStyle(VelocityTheme.accent)
}
.padding(10)
.background(RoundedRectangle(cornerRadius: 8).fill(Color.white.opacity(0.04))
.overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.white.opacity(0.06), lineWidth: 1)))
}
}
// MARK: Lead Map Canvas
private struct LeadMapCanvas: View {
let onSelectRegion: (RegionPin) -> Void
private let cols = [GridItem(.adaptive(minimum: 140), spacing: 10)]
var body: some View {
VStack(alignment: .leading, spacing: 14) {
HStack(spacing: 16) {
LegendDot(color: VelocityTheme.danger, label: "Hot Lead")
LegendDot(color: Color(red: 0.13, green: 0.83, blue: 0.93), label: "Warm Lead")
LegendDot(color: VelocityTheme.mutedFg, label: "Cold Lead")
Spacer()
}
LazyVGrid(columns: cols, spacing: 10) {
ForEach(mapPins) { pin in
TappableRegionPin(pin: pin, onTap: { onSelectRegion(pin) })
}
}
}
.padding(20)
.background(RoundedRectangle(cornerRadius: 14).fill(Color(red: 0.031, green: 0.039, blue: 0.071))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(VelocityTheme.borderAccent, lineWidth: 1)))
}
}
private struct TappableRegionPin: View {
let pin: RegionPin
let onTap: () -> Void
@State private var pressed = false
private var pinColor: Color {
pin.temp == "hot" ? VelocityTheme.danger :
pin.temp == "warm" ? Color(red: 0.13, green: 0.83, blue: 0.93) : VelocityTheme.mutedFg
}
var body: some View {
HStack(spacing: 10) {
Text(pin.country).font(.system(size: 24))
VStack(alignment: .leading, spacing: 2) {
Text(pin.label).font(.system(size: 12, weight: .semibold)).foregroundStyle(VelocityTheme.foreground)
HStack(spacing: 4) {
Circle().fill(pinColor).frame(width: 6, height: 6).shadow(color: pinColor.opacity(0.8), radius: 3)
Text("\(pin.count) leads").font(.system(size: 10)).foregroundStyle(VelocityTheme.mutedFg)
}
}
Spacer()
Image(systemName: "arrow.up.right.circle")
.font(.system(size: 13)).foregroundStyle(VelocityTheme.mutedFg)
}
.padding(12)
.background(RoundedRectangle(cornerRadius: 10)
.fill(pressed ? pinColor.opacity(0.12) : Color(red: 0.031, green: 0.039, blue: 0.071))
.overlay(RoundedRectangle(cornerRadius: 10).stroke(pinColor.opacity(pressed ? 0.5 : 0.25), lineWidth: 1)))
.scaleEffect(pressed ? 0.97 : 1.0)
.animation(.easeInOut(duration: 0.12), value: pressed)
.onTapGesture {
pressed = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) { pressed = false }
onTap()
}
}
}
private struct LegendDot: View {
let color: Color; let label: String
var body: some View {
HStack(spacing: 6) {
Circle().fill(color).frame(width: 8, height: 8).shadow(color: color.opacity(0.8), radius: 3)
Text(label).font(.system(size: 11)).foregroundStyle(VelocityTheme.mutedFg)
}
}
}
// MARK: Region Detail Sheet
private struct RegionDetailSheet: View {
let pin: RegionPin
@Environment(\.dismiss) private var dismiss
private var pinColor: Color {
pin.temp == "hot" ? VelocityTheme.danger :
pin.temp == "warm" ? Color(red: 0.13, green: 0.83, blue: 0.93) : VelocityTheme.mutedFg
}
var body: some View {
NavigationStack {
VStack(alignment: .leading, spacing: 20) {
HStack(spacing: 16) {
Text(pin.country).font(.system(size: 52))
VStack(alignment: .leading, spacing: 4) {
Text(pin.label).font(.system(size: 22, weight: .bold)).foregroundStyle(VelocityTheme.foreground)
HStack(spacing: 6) {
Circle().fill(pinColor).frame(width: 7, height: 7)
Text(pin.temp.capitalized + " Market")
.font(.system(size: 12, weight: .medium)).foregroundStyle(pinColor)
}
}
}
.padding(.top, 8)
Divider().background(VelocityTheme.borderSubtle)
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 14) {
DetailField(label: "Active Leads", value: "\(pin.count)")
DetailField(label: "Top Lead", value: pin.topLead)
DetailField(label: "Temperature", value: pin.temp.capitalized)
DetailField(label: "Priority", value: pin.temp == "hot" ? "High 🔴" : pin.temp == "warm" ? "Medium 🟡" : "Low ⚪")
}
Spacer()
}
.padding(24)
.background(VelocityTheme.background.ignoresSafeArea())
.navigationTitle("Region Details")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") { dismiss() }.foregroundStyle(VelocityTheme.accent)
}
}
}
}
}
// MARK: Calendar Canvas
private struct CalendarCanvas: View {
let onSchedule: (CalTask) -> Void
let days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
var body: some View {
VStack(spacing: 14) {
weekPanel
tasksPanel
}
}
private var weekPanel: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Weekly Schedule").font(.system(size: 13, weight: .semibold)).foregroundStyle(VelocityTheme.foreground)
HStack(spacing: 6) {
ForEach(days, id: \.self) { day in
Text(day).font(.system(size: 10, weight: .medium)).tracking(0.5)
.foregroundStyle(VelocityTheme.mutedFg).frame(maxWidth: .infinity)
}
}
HStack(spacing: 6) {
ForEach(Array(days.enumerated()), id: \.offset) { i, _ in
RoundedRectangle(cornerRadius: 8)
.fill(i == 2 ? VelocityTheme.accent.opacity(0.15) : Color.white.opacity(0.03))
.overlay(RoundedRectangle(cornerRadius: 8)
.stroke(i == 2 ? VelocityTheme.accent.opacity(0.3) : Color.white.opacity(0.05), lineWidth: 1))
.frame(height: 60)
}
}
}
.padding(16)
.background(RoundedRectangle(cornerRadius: 14).fill(Color(red: 0.031, green: 0.039, blue: 0.071))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(VelocityTheme.borderAccent, lineWidth: 1)))
}
private var tasksPanel: some View {
VStack(alignment: .leading, spacing: 10) {
HStack(spacing: 5) {
Circle().fill(Color(red: 0.13, green: 0.83, blue: 0.93)).frame(width: 6, height: 6)
Text("Tasks & Actions").font(.system(size: 13, weight: .semibold)).foregroundStyle(VelocityTheme.foreground)
}
.padding(.bottom, 4)
ForEach(calTasks) { task in
CalTaskRow(task: task, onSchedule: { onSchedule(task) })
}
}
.padding(16)
.background(RoundedRectangle(cornerRadius: 14).fill(Color(red: 0.031, green: 0.039, blue: 0.071))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(VelocityTheme.borderAccent, lineWidth: 1)))
}
}
private struct CalTaskRow: View {
let task: CalTask
let onSchedule: () -> Void
@State private var scheduled = false
var body: some View {
VStack(alignment: .leading, spacing: 6) {
HStack {
Text(task.title).font(.system(size: 13, weight: .semibold)).foregroundStyle(VelocityTheme.foreground)
Spacer()
Text(scheduled ? "Scheduled ✓" : "Action")
.font(.system(size: 9, weight: .semibold))
.foregroundStyle(scheduled ? VelocityTheme.success : VelocityTheme.mutedFg)
.padding(.horizontal, 6).padding(.vertical, 3)
.background(RoundedRectangle(cornerRadius: 4)
.fill(scheduled ? VelocityTheme.success.opacity(0.12) : Color.white.opacity(0.06)))
}
Text(task.subtitle).font(.system(size: 11)).foregroundStyle(VelocityTheme.mutedFg).lineLimit(2)
HStack {
Image(systemName: "clock").font(.system(size: 10)).foregroundStyle(VelocityTheme.accent)
Text(task.due).font(.system(size: 11)).foregroundStyle(VelocityTheme.accent)
Spacer()
Button {
onSchedule()
withAnimation(.easeInOut(duration: 0.3)) { scheduled = true }
} label: {
HStack(spacing: 5) {
Image(systemName: scheduled ? "checkmark" : "calendar.badge.plus")
.font(.system(size: 10, weight: .semibold))
Text(scheduled ? "Scheduled" : "Schedule")
.font(.system(size: 11, weight: .semibold))
}
.foregroundStyle(.white)
.padding(.horizontal, 12).padding(.vertical, 5)
.background(RoundedRectangle(cornerRadius: 7)
.fill(scheduled ? VelocityTheme.success : VelocityTheme.accent)
.shadow(color: (scheduled ? VelocityTheme.success : VelocityTheme.accent).opacity(0.4), radius: 6))
}
}
}
.padding(12)
.background(RoundedRectangle(cornerRadius: 10)
.fill(scheduled ? VelocityTheme.success.opacity(0.05) : Color.white.opacity(0.04))
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(scheduled ? VelocityTheme.success.opacity(0.2) : Color.white.opacity(0.06), lineWidth: 1)))
}
}

View File

@@ -0,0 +1,413 @@
import SwiftUI
struct SentinelView: View {
private var store: AppStore { AppStore.shared }
private let indigo = Color(red: 0.60, green: 0.57, blue: 0.99)
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
pageHeader
kpiGrid
analyticsRow
bottomRow
}
.padding(24)
}
.background(VelocityTheme.background)
.scrollContentBackground(.hidden)
}
// MARK: Sub-views extracted so the type-checker can cope
private var pageHeader: some View {
VStack(alignment: .leading, spacing: 4) {
Text("Sentinel")
.font(.system(size: 28, weight: .bold)).foregroundStyle(VelocityTheme.foreground)
Text("FaceID · visitor analytics · real-time alerts")
.font(.system(size: 12)).foregroundStyle(VelocityTheme.mutedFg)
}
}
private var kpiGrid: some View {
let cols = [GridItem(.adaptive(minimum: 180), spacing: 12)]
return LazyVGrid(columns: cols, spacing: 12) {
SentinelKPI(icon: "person.2.fill", iconColor: VelocityTheme.accent,
label: "Active Visitors", value: "\(store.visitors.count)",
sub: "Currently tracked", badge: "LIVE")
SentinelKPI(icon: "waveform.path.ecg", iconColor: VelocityTheme.success,
label: "Avg Sentiment", value: "\(avgSentiment)%",
sub: "Overall mood")
SentinelKPI(icon: "eye.fill", iconColor: indigo,
label: "Detection Accuracy", value: "\(avgConfidence)%",
sub: "Avg confidence")
SentinelKPI(icon: "faceid", iconColor: VelocityTheme.warning,
label: "Tracked Today", value: "47",
sub: "Unique faces")
}
.animation(.easeInOut(duration: 0.4), value: store.visitors.count)
}
private var analyticsRow: some View {
let cols = [GridItem(.flexible(), spacing: 14), GridItem(.flexible(minimum: 260), spacing: 14)]
return LazyVGrid(columns: cols, alignment: .leading, spacing: 14) {
ZoneAnalyticsPanel()
ClientInsightsPanel()
}
}
private var bottomRow: some View {
let cols = [GridItem(.flexible(), spacing: 14),
GridItem(.flexible(), spacing: 14),
GridItem(.flexible(), spacing: 14)]
return LazyVGrid(columns: cols, alignment: .leading, spacing: 14) {
SentimentDistributionPanel(visitors: store.visitors)
DwellTimePanel()
AlertPanel(isActive: store.isAlertActive, message: store.alertMessage)
}
}
private var avgSentiment: Int {
guard !store.visitors.isEmpty else { return 0 }
let total = store.visitors.reduce(0) { $0 + $1.sentiment.score }
return total / store.visitors.count
}
private var avgConfidence: Int {
guard !store.visitors.isEmpty else { return 0 }
let total = store.visitors.reduce(0.0) { $0 + $1.confidence }
return Int((total / Double(store.visitors.count)) * 100)
}
}
// MARK: KPI Card
private struct SentinelKPI: View {
let icon: String; let iconColor: Color
let label: String; let value: String; let sub: String
var badge: String? = nil
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
ZStack {
RoundedRectangle(cornerRadius: 8).fill(iconColor.opacity(0.14)).frame(width: 34, height: 34)
Image(systemName: icon).font(.system(size: 14)).foregroundStyle(iconColor)
}
Spacer()
if let badge {
HStack(spacing: 4) {
Circle().fill(VelocityTheme.success).frame(width: 5, height: 5)
Text(badge).font(.system(size: 9, weight: .semibold)).foregroundStyle(VelocityTheme.success)
}
}
}
Text(label.uppercased()).font(.system(size: 10, weight: .medium)).tracking(1.0)
.foregroundStyle(VelocityTheme.mutedFg)
Text(value).font(.system(size: 30, weight: .semibold)).foregroundStyle(VelocityTheme.foreground)
.contentTransition(.numericText())
Text(sub).font(.system(size: 11)).foregroundStyle(VelocityTheme.subtleFg)
}
.padding(18)
.frame(maxWidth: .infinity, minHeight: 140, alignment: .topLeading)
.background(
RoundedRectangle(cornerRadius: 14)
.fill(Color(red: 0.031, green: 0.039, blue: 0.071))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(iconColor.opacity(0.18), lineWidth: 1))
)
}
}
// MARK: Zone Analytics
private struct ZoneAnalyticsPanel: View {
private let zones: [(id: String, name: String, count: Int, sentiment: Int)] = [
("A", "Main Showroom", 5, 72),
("B", "Penthouse Gallery",3, 85),
("C", "Amenity Deck VR", 2, 68),
("D", "Reception", 2, 90),
]
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 6) {
Image(systemName: "mappin.and.ellipse").font(.system(size: 12)).foregroundStyle(VelocityTheme.accent)
Text("Zone Analytics").font(.system(size: 13, weight: .semibold)).foregroundStyle(VelocityTheme.foreground)
}
ForEach(zones, id: \.id) { zone in
HStack(spacing: 10) {
ZStack {
RoundedRectangle(cornerRadius: 6).fill(VelocityTheme.accent.opacity(0.14)).frame(width: 28, height: 28)
Text(zone.id).font(.system(size: 11, weight: .bold)).foregroundStyle(VelocityTheme.accent)
}
VStack(alignment: .leading, spacing: 2) {
Text(zone.name).font(.system(size: 12, weight: .medium)).foregroundStyle(VelocityTheme.foreground)
Text("\(zone.count) visitors").font(.system(size: 10)).foregroundStyle(VelocityTheme.mutedFg)
}
Spacer()
HStack(spacing: 4) {
let c: Color = zone.sentiment >= 80 ? VelocityTheme.success :
zone.sentiment >= 60 ? VelocityTheme.accent : VelocityTheme.warning
Circle().fill(c).frame(width: 7, height: 7)
Text("\(zone.sentiment)%").font(.system(size: 12, weight: .semibold)).foregroundStyle(VelocityTheme.foreground)
}
}
.padding(10)
.background(RoundedRectangle(cornerRadius: 10).fill(Color.white.opacity(0.04))
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.white.opacity(0.06), lineWidth: 1)))
}
}
.padding(16)
.background(RoundedRectangle(cornerRadius: 14).fill(Color(red: 0.031, green: 0.039, blue: 0.071))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(VelocityTheme.borderAccent, lineWidth: 1)))
}
}
// MARK: Client Insights
private struct ClientInsightsPanel: View {
private struct Insight {
let name: String; let stage: String; let sentiment: String
let score: Int; let insight: String; let color: Color
var icon: String {
score >= 80 ? "arrow.up.right" : score >= 50 ? "minus" : "exclamationmark.triangle"
}
var scoreColor: Color {
score >= 80 ? VelocityTheme.success : score >= 50 ? VelocityTheme.warning : VelocityTheme.danger
}
}
private let insights: [Insight] = [
.init(name: "Quantum Dynamics", stage: "Proposal Review", sentiment: "Positive", score: 92,
insight: "Key decision maker showed high engagement. Emphasise custom penthouse integration.",
color: VelocityTheme.success),
.init(name: "Nebula Ventures", stage: "Discovery", sentiment: "Neutral", score: 45,
insight: "Initial interest detected but hesitation around pricing model tier.",
color: VelocityTheme.warning),
.init(name: "Apex Industries", stage: "Negotiation", sentiment: "Mixed", score: 68,
insight: "Confusion markers during contract terms review. Legal team is the blocker.",
color: VelocityTheme.danger),
.init(name: "Starlight Systems", stage: "Initial Contact", sentiment: "Positive", score: 78,
insight: "Strong ESG engagement. Align pitch with sustainability goals to accelerate close.",
color: VelocityTheme.accent),
]
var body: some View {
VStack(alignment: .leading, spacing: 12) {
insightHeader
insightGrid
}
.padding(16)
.background(RoundedRectangle(cornerRadius: 14)
.fill(Color(red: 0.031, green: 0.039, blue: 0.071))
.overlay(RoundedRectangle(cornerRadius: 14)
.stroke(VelocityTheme.borderAccent, lineWidth: 1)))
}
private var insightHeader: some View {
HStack(spacing: 6) {
Image(systemName: "sparkles").font(.system(size: 12)).foregroundStyle(VelocityTheme.accent)
Text("AI Strategic Insights")
.font(.system(size: 13, weight: .semibold)).foregroundStyle(VelocityTheme.foreground)
Spacer()
Text("LIVE ANALYSIS").font(.system(size: 8, weight: .bold)).tracking(1)
.foregroundStyle(VelocityTheme.accent)
.padding(.horizontal, 6).padding(.vertical, 3)
.background(RoundedRectangle(cornerRadius: 4).fill(VelocityTheme.accent.opacity(0.12))
.overlay(RoundedRectangle(cornerRadius: 4)
.stroke(VelocityTheme.accent.opacity(0.2), lineWidth: 1)))
}
}
private var insightGrid: some View {
let cols = [GridItem(.flexible(), spacing: 10), GridItem(.flexible(), spacing: 10)]
return LazyVGrid(columns: cols, alignment: .leading, spacing: 10) {
ForEach(insights, id: \.name) { item in
InsightCard(
name: item.name, stage: item.stage, sentiment: item.sentiment,
score: item.score, insight: item.insight, color: item.color,
icon: item.icon, scoreColor: item.scoreColor
)
}
}
}
}
private struct InsightCard: View {
struct Item {
let name: String; let stage: String; let sentiment: String
let score: Int; let insight: String; let color: Color
var icon: String { score >= 80 ? "arrow.up.right" : score >= 50 ? "minus" : "exclamationmark.triangle" }
var scoreColor: Color { score >= 80 ? VelocityTheme.success : score >= 50 ? VelocityTheme.warning : VelocityTheme.danger }
}
// Accept ClientInsightsPanel.Insight via protocol duck-typing workaround:
let name: String; let stage: String; let sentiment: String
let score: Int; let insight: String; let color: Color
let icon: String; let scoreColor: Color
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
ZStack {
RoundedRectangle(cornerRadius: 6).fill(color.opacity(0.18)).frame(width: 28, height: 28)
Image(systemName: icon).font(.system(size: 11)).foregroundStyle(color)
}
Spacer()
Text("\(score)").font(.system(size: 11, weight: .bold))
.foregroundStyle(scoreColor)
.padding(.horizontal, 6).padding(.vertical, 2)
.background(RoundedRectangle(cornerRadius: 4).fill(scoreColor.opacity(0.15)))
}
Text(name).font(.system(size: 12, weight: .semibold))
.foregroundStyle(VelocityTheme.foreground).lineLimit(1)
Text(insight).font(.system(size: 10))
.foregroundStyle(VelocityTheme.mutedFg).lineLimit(3)
HStack {
Text(stage).font(.system(size: 9)).foregroundStyle(VelocityTheme.subtleFg)
Spacer()
Text(sentiment).font(.system(size: 9, weight: .semibold)).foregroundStyle(color)
}
}
.padding(10)
.background(RoundedRectangle(cornerRadius: 10).fill(Color.white.opacity(0.04))
.overlay(RoundedRectangle(cornerRadius: 10).stroke(color.opacity(0.15), lineWidth: 1)))
}
}
// MARK: Sentiment Distribution
private struct SentimentDistributionPanel: View {
let visitors: [Visitor]
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 6) {
Image(systemName: "chart.bar").font(.system(size: 12)).foregroundStyle(VelocityTheme.accent)
Text("Sentiment").font(.system(size: 13, weight: .semibold)).foregroundStyle(VelocityTheme.foreground)
}
ForEach(SentimentType.allCases, id: \.self) { type in
let count = visitors.filter { $0.sentiment == type }.count
let fraction = visitors.isEmpty ? 0.0 : Double(count) / Double(visitors.count)
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(type.emoji).font(.system(size: 14))
Text(type.rawValue.capitalized).font(.system(size: 11)).foregroundStyle(VelocityTheme.mutedFg)
Spacer()
Text("\(count)").font(.system(size: 11, weight: .semibold)).foregroundStyle(VelocityTheme.foreground)
}
GeometryReader { geo in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 3).fill(Color.white.opacity(0.06)).frame(height: 5)
RoundedRectangle(cornerRadius: 3).fill(type.color)
.frame(width: geo.size.width * fraction, height: 5)
.animation(.easeOut(duration: 0.6), value: fraction)
}
}
.frame(height: 5)
}
}
}
.padding(16)
.background(RoundedRectangle(cornerRadius: 14).fill(Color(red: 0.031, green: 0.039, blue: 0.071))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(VelocityTheme.borderAccent, lineWidth: 1)))
}
}
// MARK: Dwell Time Panel
private struct DwellTimePanel: View {
private let data: [(range: String, count: Int, trend: String)] = [
("< 5 min", 3, "down"),
("515 min", 5, "up"),
("1530 min", 8, "up"),
("> 30 min", 4, "stable"),
]
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 6) {
Image(systemName: "timer").font(.system(size: 12)).foregroundStyle(VelocityTheme.accent)
Text("Dwell Time").font(.system(size: 13, weight: .semibold)).foregroundStyle(VelocityTheme.foreground)
}
let cols = [GridItem(.flexible(), spacing: 8), GridItem(.flexible(), spacing: 8)]
LazyVGrid(columns: cols, spacing: 8) {
ForEach(data, id: \.range) { item in
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(item.range).font(.system(size: 9)).foregroundStyle(VelocityTheme.mutedFg)
Spacer()
Image(systemName: item.trend == "up" ? "arrow.up.right" :
item.trend == "down" ? "arrow.down.right" : "minus")
.font(.system(size: 9))
.foregroundStyle(item.trend == "up" ? VelocityTheme.success :
item.trend == "down" ? VelocityTheme.danger : VelocityTheme.mutedFg)
}
Text("\(item.count)").font(.system(size: 22, weight: .semibold)).foregroundStyle(VelocityTheme.foreground)
Text("visitors").font(.system(size: 9)).foregroundStyle(VelocityTheme.subtleFg)
}
.padding(10)
.background(RoundedRectangle(cornerRadius: 10).fill(Color.white.opacity(0.04))
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.white.opacity(0.06), lineWidth: 1)))
}
}
}
.padding(16)
.background(RoundedRectangle(cornerRadius: 14).fill(Color(red: 0.031, green: 0.039, blue: 0.071))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(VelocityTheme.borderAccent, lineWidth: 1)))
}
}
// MARK: Alert Panel
private struct AlertPanel: View {
let isActive: Bool
let message: String
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 6) {
Image(systemName: "bell.badge").font(.system(size: 12)).foregroundStyle(VelocityTheme.warning)
Text("Alerts").font(.system(size: 13, weight: .semibold)).foregroundStyle(VelocityTheme.foreground)
Spacer()
Text(isActive ? "Active" : "Clear")
.font(.system(size: 9, weight: .semibold))
.foregroundStyle(isActive ? VelocityTheme.warning : VelocityTheme.success)
.padding(.horizontal, 7).padding(.vertical, 3)
.background(Capsule().fill((isActive ? VelocityTheme.warning : VelocityTheme.success).opacity(0.15))
.overlay(Capsule().stroke((isActive ? VelocityTheme.warning : VelocityTheme.success).opacity(0.25), lineWidth: 1)))
}
if isActive {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 8) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 14)).foregroundStyle(VelocityTheme.warning)
Text("Sentiment Alert").font(.system(size: 12, weight: .semibold)).foregroundStyle(VelocityTheme.warning)
}
Text(message).font(.system(size: 12)).foregroundStyle(VelocityTheme.foreground).lineLimit(3)
Text("Just now").font(.system(size: 10)).foregroundStyle(VelocityTheme.mutedFg)
}
.padding(12)
.background(RoundedRectangle(cornerRadius: 10).fill(VelocityTheme.warning.opacity(0.08))
.overlay(RoundedRectangle(cornerRadius: 10).stroke(VelocityTheme.warning.opacity(0.3), lineWidth: 1)))
.transition(.asymmetric(insertion: .scale(scale: 0.95).combined(with: .opacity),
removal: .scale(scale: 0.95).combined(with: .opacity)))
} else {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 8) {
Image(systemName: "checkmark.shield.fill")
.font(.system(size: 14)).foregroundStyle(VelocityTheme.success)
Text("All Clear").font(.system(size: 12, weight: .semibold)).foregroundStyle(VelocityTheme.success)
}
Text("No alerts at this time").font(.system(size: 12)).foregroundStyle(VelocityTheme.mutedFg)
Text("System nominal").font(.system(size: 10)).foregroundStyle(VelocityTheme.subtleFg)
}
.padding(12)
.background(RoundedRectangle(cornerRadius: 10).fill(VelocityTheme.success.opacity(0.08))
.overlay(RoundedRectangle(cornerRadius: 10).stroke(VelocityTheme.success.opacity(0.3), lineWidth: 1)))
.transition(.asymmetric(insertion: .scale(scale: 0.95).combined(with: .opacity),
removal: .scale(scale: 0.95).combined(with: .opacity)))
}
}
.padding(16)
.animation(.easeInOut(duration: 0.3), value: isActive)
.background(RoundedRectangle(cornerRadius: 14).fill(Color(red: 0.031, green: 0.039, blue: 0.071))
.overlay(RoundedRectangle(cornerRadius: 14).stroke(VelocityTheme.borderAccent, lineWidth: 1)))
}
}

View File

@@ -0,0 +1,141 @@
import SwiftUI
struct SettingsView: View {
var body: some View {
VStack(alignment: .leading, spacing: 24) {
// Page header
VStack(alignment: .leading, spacing: 4) {
Text("Settings")
.font(.system(size: 28, weight: .bold))
.foregroundStyle(VelocityTheme.foreground)
Text("Configuration")
.font(.system(size: 12))
.foregroundStyle(VelocityTheme.mutedFg)
}
// System (live) section
SettingsSection(title: "System") {
HStack(spacing: 14) {
ZStack {
RoundedRectangle(cornerRadius: 7)
.fill(VelocityTheme.success.opacity(0.12)).frame(width: 30, height: 30)
Image(systemName: "bolt.fill")
.font(.system(size: 13, weight: .medium)).foregroundStyle(VelocityTheme.success)
}
Text("Connection Status").font(.system(size: 14)).foregroundStyle(VelocityTheme.foreground)
Spacer()
HStack(spacing: 5) {
Circle().fill(VelocityTheme.success).frame(width: 6, height: 6)
Text("Online").font(.system(size: 12, weight: .medium)).foregroundStyle(VelocityTheme.success)
}
}
.padding(.horizontal, 16).padding(.vertical, 12)
}
// Backend section
SettingsSection(title: "Backend") {
SettingsRow(label: "ComfyUI Endpoint",
value: "http://192.168.x.x:8000",
icon: "server.rack",
accentColor: VelocityTheme.accent)
Divider().background(VelocityTheme.borderSubtle)
SettingsRow(label: "Dream Weaver Path",
value: "/dream-weaver",
icon: "arrow.triangle.branch",
accentColor: VelocityTheme.accent)
}
// Display section
SettingsSection(title: "Display") {
SettingsRow(label: "Orientation",
value: "Landscape Only",
icon: "rectangle.landscape.rotate",
accentColor: VelocityTheme.mutedFg)
Divider().background(VelocityTheme.borderSubtle)
SettingsRow(label: "Theme",
value: "Dark",
icon: "moon.fill",
accentColor: Color(red: 0.60, green: 0.57, blue: 0.99))
}
// App info section
SettingsSection(title: "About") {
SettingsRow(label: "Version",
value: "1.1.0",
icon: "info.circle",
accentColor: VelocityTheme.mutedFg)
Divider().background(VelocityTheme.borderSubtle)
SettingsRow(label: "Build",
value: "SwiftUI · iOS 17+",
icon: "hammer",
accentColor: VelocityTheme.mutedFg)
}
Spacer()
}
.padding(24)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.background(VelocityTheme.background)
}
}
private struct SettingsSection<Content: View>: View {
let title: String
@ViewBuilder let content: Content
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Text(title.uppercased())
.font(.system(size: 10, weight: .semibold))
.tracking(1.2)
.foregroundStyle(VelocityTheme.mutedFg)
.padding(.bottom, 8)
.padding(.horizontal, 4)
VStack(spacing: 0) {
content
}
.padding(.vertical, 4)
.background(
RoundedRectangle(cornerRadius: 14)
.fill(Color(red: 0.031, green: 0.039, blue: 0.071))
.overlay(
RoundedRectangle(cornerRadius: 14)
.stroke(VelocityTheme.borderSubtle, lineWidth: 1)
)
)
}
}
}
private struct SettingsRow: View {
let label: String
let value: String
let icon: String
let accentColor: Color
var body: some View {
HStack(spacing: 14) {
ZStack {
RoundedRectangle(cornerRadius: 7)
.fill(accentColor.opacity(0.12))
.frame(width: 30, height: 30)
Image(systemName: icon)
.font(.system(size: 13, weight: .medium))
.foregroundStyle(accentColor)
}
Text(label)
.font(.system(size: 14))
.foregroundStyle(VelocityTheme.foreground)
Spacer()
Text(value)
.font(.system(size: 13))
.foregroundStyle(VelocityTheme.mutedFg)
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
}