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

This commit is contained in:
Sayan Datta
2026-03-07 17:04:53 +05:30
parent 8fe2344e71
commit cfa340cb5d
33 changed files with 6930 additions and 67 deletions

View File

@@ -1,38 +1,442 @@
import SwiftUI
struct DashboardView: View {
private let columns = [GridItem(.adaptive(minimum: 220), spacing: 16)]
private var store: AppStore { AppStore.shared }
@State private var chatInput = ""
private let columns = [GridItem(.adaptive(minimum: 220), spacing: 14)]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 16) {
WidgetCard(title: "Listings", value: "128", subtitle: "Active units")
WidgetCard(title: "Revenue", value: "$3.2M", subtitle: "30-day forecast")
WidgetCard(title: "AI Jobs", value: "24", subtitle: "Queue depth")
WidgetCard(title: "Visitors", value: "17", subtitle: "Today")
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)
}
.navigationTitle("Dashboard")
.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?"
}
}
private struct WidgetCard: View {
// 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: 8) {
Text(title)
.font(.headline)
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(.largeTitle.bold())
Text(subtitle)
.foregroundStyle(.secondary)
.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
}
}
.frame(maxWidth: .infinity, minHeight: 140, alignment: .leading)
.padding()
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 18))
}
}

View File

@@ -32,7 +32,19 @@ struct InventoryView: View {
private let haptics = UIImpactFeedbackGenerator(style: .light)
var body: some View {
VStack(spacing: 16) {
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)
@@ -45,7 +57,34 @@ struct InventoryView: View {
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,
@@ -63,8 +102,18 @@ struct InventoryView: View {
.padding(.bottom, 20)
.animation(.easeInOut(duration: 0.25), value: store.mode)
}
.navigationTitle("Inventory")
.background(Color(uiColor: .systemGroupedBackground))
.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)
}

View File

@@ -1,16 +1,960 @@
import SwiftUI
struct OracleView: View {
var body: some View {
VStack(spacing: 16) {
Image(systemName: "message.and.waveform")
.font(.system(size: 48))
Text("Oracle Chat")
.font(.title2.bold())
Text("Connect this view to your backend assistant pipeline.")
.foregroundStyle(.secondary)
// 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"
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.navigationTitle("Oracle")
}
}
// 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

@@ -1,16 +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 {
VStack(spacing: 16) {
Image(systemName: "faceid")
.font(.system(size: 48))
Text("Sentinel")
.font(.title2.bold())
Text("FaceID and visitor event logs surface here.")
.foregroundStyle(.secondary)
ScrollView {
VStack(alignment: .leading, spacing: 20) {
pageHeader
kpiGrid
analyticsRow
bottomRow
}
.padding(24)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.navigationTitle("Sentinel")
.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

@@ -2,14 +2,140 @@ import SwiftUI
struct SettingsView: View {
var body: some View {
Form {
Section("Backend") {
LabeledContent("ComfyUI Endpoint", value: "http://192.168.x.x:8000")
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)
}
Section("Display") {
LabeledContent("Orientation", value: "Landscape Only")
// 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()
}
.navigationTitle("Settings")
.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)
}
}