Files
Project_Velocity/iOS/Features/Dashboard/DashboardView.swift
sayan cb6c752c8e 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: sagnik/Project_Velocity#2
Co-authored-by: sayan <sayan@desineuron.in>
Co-committed-by: sayan <sayan@desineuron.in>
2026-03-07 18:46:02 +05:30

443 lines
19 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
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
}
}
}
}