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 } } } }