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

@@ -0,0 +1,457 @@
# Project Velocity — Gitea Feature Contribution Guide
**Version:** 1.1
**Last Updated:** 2026-03-07
**Upstream Repository (Sagnik's):** `https://git.desineuron.in/sagnik/Project_Velocity`
**Your Fork:** `https://git.desineuron.in/<YOUR_USERNAME>/Project_Velocity`
> This document is the **canonical guide** for every team member contributing to Project Velocity.
> The team uses a **Fork-based workflow**: each contributor has their own fork of Sagnik's repository. All features are developed in your fork and merged into the upstream via Pull Requests.
> It covers the full lifecycle: Issue → Branch → Commit → Push → Pull Request — both via the **CLI** and via **Antigravity Source Control UI**.
### How the Fork Model Works
```text
[upstream] sagnik/Project_Velocity ← the source of truth
↑ Pull Requests
[your fork] <you>/Project_Velocity ← where you develop
↑ git push
[local] /your/machine/Project_Velocity
```
- `upstream` = Sagnik's repo — you **pull** from here to stay in sync.
- `origin` = Your fork — you **push** your branches here.
- PRs go **from your fork → upstream**.
---
## Table of Contents
1. [Naming Conventions](#naming-conventions)
2. [One-Time Setup — Fork & Remotes](#one-time-setup--fork--remotes)
3. [Step 0 — Pre-flight: Populate `.gitignore` & `.gitkeep`](#step-0--pre-flight)
4. [Step 1 — Create a Gitea Issue](#step-1--create-a-gitea-issue)
5. [Step 2 — Sync with Upstream Before Branching](#step-2--sync-with-upstream-before-branching)
6. [Step 3 — Create a Feature Branch](#step-3--create-a-feature-branch)
7. [Step 4 — Make Your Changes & Commit](#step-4--make-your-changes--commit)
8. [Step 5 — Push the Branch to Your Fork](#step-5--push-the-branch-to-your-fork)
9. [Step 6 — Create a Pull Request (Fork → Upstream)](#step-6--create-a-pull-request-fork--upstream)
10. [Quick Reference — CLI Cheat Sheet](#quick-reference--cli-cheat-sheet)
---
## Naming Conventions
Consistent naming keeps the repository clean and makes history easy to read.
| Type | Format | Example |
|------|--------|---------|
| **Issue Title** | `feat : <Short description>` | `feat : Build the native SwiftUI app shell` |
| **Branch Name** | `feat/#<issue-number>` | `feat/#12` |
| **Commit Message** | `feat(scope): <what changed>` or `fix(scope): <what was fixed>` | `feat(ios): add Dashboard, Oracle and Sentinel tab views` |
> Use `fix`, `chore`, `docs`, `refactor`, or `test` instead of `feat` where appropriate.
---
## One-Time Setup — Fork & Remotes
Do this **once** when you first clone your fork onto a new machine. You only need to do this once per machine.
### 1. Clone your fork
```bash
git clone https://git.desineuron.in/<YOUR_USERNAME>/Project_Velocity.git
cd Project_Velocity
```
### 2. Add the upstream remote (Sagnik's repo)
```bash
git remote add upstream https://git.desineuron.in/sagnik/Project_Velocity.git
```
### 3. Verify your remotes
```bash
git remote -v
```
You should see **two** remotes:
```text
origin https://git.desineuron.in/<YOUR_USERNAME>/Project_Velocity.git (fetch)
origin https://git.desineuron.in/<YOUR_USERNAME>/Project_Velocity.git (push)
upstream https://git.desineuron.in/sagnik/Project_Velocity.git (fetch)
upstream https://git.desineuron.in/sagnik/Project_Velocity.git (push)
```
> ⚠️ If you only see `origin`, you haven't added `upstream` yet. Run step 2 above.
### Via Antigravity Source Control UI
Antigravity does not have a direct UI button to add a new remote. Do this once via the terminal:
1. Open a terminal inside Antigravity (`Ctrl + `` ` `` ` `).
2. Run: `git remote add upstream https://git.desineuron.in/sagnik/Project_Velocity.git`
3. The upstream remote will now appear in the **Graph** section of Source Control.
---
## Step 0 — Pre-flight
Before pushing anything, ensure these files are properly configured at the **root of the repository**.
### `.gitignore`
A root `.gitignore` already exists at the repository root. It covers:
- **macOS** system files (`DS_Store`, etc.)
- **Node / Vite / React** (`app/node_modules/`, `app/dist/`)
- **Xcode / Swift** (derived data, `.xcuserdata`, build artifacts)
- **Python / ComfyUI** (`__pycache__`, `.venv`, model weights)
- **Infrastructure** (Terraform state, secrets)
**You should never commit:**
- API keys or secrets (`.env` files are ignored)
- `node_modules/` directory
- Xcode `DerivedData/` or `.xcuserdata/`
- Python virtual environments (`.venv/`, `venv/`)
### `.gitkeep`
A `.gitkeep` file is used to **track an otherwise empty directory** in Git (Git does not track empty folders).
**When to add one:** If you create a new directory that should be committed but has no files yet, add an empty `.gitkeep` file inside it:
```bash
touch path/to/your/new-empty-folder/.gitkeep
```
---
## Step 1 — Create a Gitea Issue
Every feature or bug fix must start with a **Gitea Issue**. Issues must be created in **Sagnik's upstream repository** — not your fork. The upstream is the source of truth for the project, and `Closes #<N>` in your PR description will only auto-close the issue if it lives in the same repo your PR targets.
### Via Browser (Gitea Web UI)
1. Go to **Sagnik's upstream repo**: `https://git.desineuron.in/sagnik/Project_Velocity`.
2. Click on the **Issues** tab.
3. Click the green **New Issue** button.
4. Fill in the title using the format:
```
feat : <Short description of your feature>
```
**Example:**
```
feat : Build the native SwiftUI app shell mirroring the WebOS interface
```
5. Add a description explaining:
- **What** you are building.
- **Why** it is needed.
- **Acceptance criteria** (what "done" looks like).
6. Assign the issue to yourself.
7. Click **Submit**. Note down the **Issue Number** (e.g., `#12`) — you will need it for the branch name.
### Via CLI (using `curl` or Gitea API)
```bash
# Replace placeholders before running
GITEA_URL="https://git.desineuron.in"
GITEA_TOKEN="<YOUR_PERSONAL_ACCESS_TOKEN>" # Generate at: Profile → Settings → Applications
REPO_OWNER="sagnik" # Issues go in the UPSTREAM repo
REPO_NAME="Project_Velocity"
ISSUE_TITLE="feat : <Short description of your feature>"
ISSUE_BODY="<Detailed description of what you are building and why>"
curl -s -X POST "$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/issues" \
-H "Content-Type: application/json" \
-H "Authorization: token $GITEA_TOKEN" \
-d "{
\"title\": \"$ISSUE_TITLE\",
\"body\": \"$ISSUE_BODY\",
\"assignees\": [\"$REPO_OWNER\"]
}"
```
> The response JSON will contain the `number` field — **this is your issue number**.
---
## Step 2 — Sync with Upstream Before Branching
Before creating a feature branch, **always** pull the latest changes from **`upstream`** (Sagnik's repo) — not from your fork (`origin`). This ensures your work starts from the most up-to-date version of the project.
### Via CLI
```bash
# Navigate to your project root
cd "/path/to/Project_Velocity"
# Switch to main
git checkout main
# Fetch all branches from upstream
git fetch upstream
# Merge upstream/main into your local main
git merge upstream/main
# (Optional) Push the synced main to your own fork to keep it up to date
git push origin main
```
> **Why `upstream` and not `origin`?**
> `origin` is your personal fork — it only knows about changes you've pushed.
> `upstream` is Sagnik's repository — the actual source of truth for the project.
### Via Antigravity Source Control UI
Antigravity's sync button pulls from `origin` by default. To sync from upstream, use the terminal:
1. Open a terminal in Antigravity (`Ctrl + `` ` `` ` `).
2. Run:
```bash
git fetch upstream && git merge upstream/main
```
3. Then click the **↺ (Refresh)** icon in the Source Control panel to refresh the view.
4. Confirm the **Graph** section shows your local `main` is aligned with `upstream/main`.
---
## Step 3 — Create a Feature Branch
Branch name format: **`feat/#<ISSUE_NUMBER>`**
**IMPORTANT:** Replace `<ISSUE_NUMBER>` with the actual number from Step 1.
### Via CLI
```bash
# Make sure you are on main first
git checkout main
# Create and switch to the new branch
git checkout -b feat/#<ISSUE_NUMBER>
# Example:
# git checkout -b feat/#12
```
### Via Antigravity Source Control UI
1. In the **Source Control** panel, look at the bottom **Graph** section.
2. Click on the **branch name** shown next to the current `HEAD` commit (e.g., `main`).
3. A branch picker will appear at the top of the screen.
4. Type the new branch name exactly: `feat/#<ISSUE_NUMBER>`
5. Select **"Create new branch: feat/#<ISSUE_NUMBER>"** from the dropdown.
6. Confirm. Your working branch is now set to the new feature branch.
---
## Step 4 — Make Your Changes & Commit
### Stage Your Files
#### Via CLI
```bash
# Stage specific files
git add path/to/changed/file.swift
# Stage all changed/new files at once (use with care)
git add .
# Review what is staged before committing
git status
```
#### Via Antigravity Source Control UI
1. In the **Source Control** panel, you will see two sections:
- **Staged Changes** — files ready to be committed.
- **Changes** — files that are modified but not yet staged.
2. To **stage a file**: Hover over it in the **Changes** section and click the **`+`** (plus) icon that appears on the right.
3. To **unstage a file**: Hover over it in **Staged Changes** and click the **`-`** (minus) icon.
4. To **stage all changes at once**: Click the **`+`** (plus) icon next to the **Changes** section header.
### Write Your Commit Message
Use the **Conventional Commits** format:
```
<type>(scope): <short summary>
```
| Type | When to Use |
|------|-------------|
| `feat` | A new feature |
| `fix` | A bug fix |
| `docs` | Documentation changes only |
| `refactor` | Code restructured, no behavior change |
| `chore` | Config, build scripts, dependencies |
| `test` | Adding or fixing tests |
**Example commit messages:**
```
feat(ios): add Dashboard, Oracle, Sentinel, and Inventory tab views
feat(ios): implement AppStore with lead, visitor, and metrics models
fix(ios): correct ARSunOverlayView coordinate calculations
docs(agent): add velocity_ios_bible.md technical reference
```
### Commit
#### Via CLI
```bash
git commit -m "<type>(scope): <short summary of your change>"
# Example:
# git commit -m "feat(ios): add Dashboard, Oracle, Sentinel tab views mirroring WebOS"
```
#### Via Antigravity Source Control UI
1. Ensure your files are in the **Staged Changes** section.
2. Click the **Message** text field at the top of the Source Control panel (placeholder: `Message (⌘Enter to com...)`).
3. Type your commit message following the Conventional Commits format above.
4. *(Optional)* Click **Generate ✦** to let Antigravity AI suggest a commit message based on your staged diffs — review it before accepting.
5. Click the blue **✓ Commit** button (or press `⌘ + Enter`) to commit.
---
## Step 5 — Push the Branch to Gitea
### Via CLI
```bash
# Push your feature branch to the remote (origin)
git push origin feat/#<ISSUE_NUMBER>
# Example:
# git push origin feat/#12
```
If this is the **first push** of a new branch, Git may ask you to set the upstream:
```bash
git push --set-upstream origin feat/#<ISSUE_NUMBER>
```
### Via Antigravity Source Control UI
1. After committing, look at the **Changes** header in the Source Control panel.
2. Click the **`...` (More Actions)** menu (top-right of the Changes section header).
3. Select **Push** from the dropdown.
4. Alternatively, click the **↑ (Publish/Push)** cloud icon if visible next to the branch name in the status bar or Graph section.
5. Antigravity will push your local feature branch to `origin/feat/#<ISSUE_NUMBER>` on Gitea.
---
## Step 6 — Create a Pull Request (Fork → Upstream)
Since you are working on a fork, your PR goes **from your fork's feature branch → Sagnik's upstream `main`**. This is a cross-repository PR.
### Via Browser (Gitea Web UI)
1. Go to **Sagnik's upstream repo**: `https://git.desineuron.in/sagnik/Project_Velocity`.
2. Click the **Pull Requests** tab → **New Pull Request**.
3. Click **"compare across forks"** (Gitea shows this option when creating a cross-fork PR).
4. Set up the PR direction:
- **Base repository:** `sagnik/Project_Velocity` — **Base branch:** `main`
- **Head repository:** `<YOUR_USERNAME>/Project_Velocity` — **Compare branch:** `feat/#<ISSUE_NUMBER>`
5. Fill in the PR form:
- **Title:** `feat(scope): <Short description>`
- **Description:**
```
## Summary
<What does this PR do?>
## Changes
- <List the main changes>
## Related Issue
Closes #<ISSUE_NUMBER>
```
6. Assign **Sagnik** as reviewer.
7. Click **Create Pull Request**.
> 💡 **Alternatively**, after you push your branch to your fork (`origin`), Gitea may show a yellow banner inside Sagnik's upstream repo prompting you to create a cross-fork PR automatically. Click that if it appears.
### Via CLI (using Gitea API)
For a cross-fork PR, the `head` must include your fork username as a prefix.
```bash
GITEA_URL="https://git.desineuron.in"
GITEA_TOKEN="<YOUR_PERSONAL_ACCESS_TOKEN>"
UPSTREAM_OWNER="sagnik" # Sagnik's username
UPSTREAM_REPO="Project_Velocity"
YOUR_USERNAME="<YOUR_USERNAME>" # Your Gitea username
FEATURE_BRANCH="feat/#<ISSUE_NUMBER>"
PR_TITLE="feat(scope): <Short description>"
PR_BODY="## Summary\n<What does this PR do?>\n\n## Changes\n- <List main changes>\n\n## Related Issue\nCloses #<ISSUE_NUMBER>"
curl -s -X POST "$GITEA_URL/api/v1/repos/$UPSTREAM_OWNER/$UPSTREAM_REPO/pulls" \
-H "Content-Type: application/json" \
-H "Authorization: token $GITEA_TOKEN" \
-d "{
\"title\": \"$PR_TITLE\",
\"body\": \"$PR_BODY\",
\"head\": \"$YOUR_USERNAME:$FEATURE_BRANCH\",
\"base\": \"main\"
}"
```
> Note the `head` format: `"<YOUR_USERNAME>:feat/#<ISSUE_NUMBER>"` — this tells Gitea the branch lives in your fork, not the upstream.
---
## Quick Reference — CLI Cheat Sheet
Copy and fill in the placeholders (`<...>`) before running.
```bash
# ── ONE-TIME SETUP (do this once per machine) ─────────────────
cd "/path/to/Project_Velocity"
git remote add upstream https://git.desineuron.in/sagnik/Project_Velocity.git
git remote -v # verify: you should see both origin and upstream
# ── 1. Create the Gitea Issue ──────────────────────────────────
# Go to Gitea web UI or use the curl command in Step 1
# Title format: feat : <Short description>
# Note the issue number: <ISSUE_NUMBER>
# ── 2. Sync from upstream main ────────────────────────────────
git checkout main
git fetch upstream
git merge upstream/main
git push origin main # keep your fork in sync too
# ── 3. Create & switch to feature branch ─────────────────────
git checkout -b feat/#<ISSUE_NUMBER>
# ── 4. Stage & Commit ─────────────────────────────────────────
git add .
git status # double-check what is staged
git commit -m "<type>(scope): <short summary>"
# ── 5. Push branch to YOUR FORK (origin) ──────────────────────
git push origin feat/#<ISSUE_NUMBER>
# ── 6. Create Pull Request (fork → upstream) ──────────────────
# Go to: https://git.desineuron.in/sagnik/Project_Velocity
# Pull Requests → New Pull Request → Compare across forks
# head: <YOUR_USERNAME>/Project_Velocity : feat/#<ISSUE_NUMBER>
# base: sagnik/Project_Velocity : main
```
---
## Tips & Best Practices
- **One issue = one branch = one PR.** Keep your changes focused.
- **Always sync from `upstream`, not `origin`.** Your fork doesn't automatically know about changes Sagnik pushes. Run `git fetch upstream && git merge upstream/main` regularly.
- **Commit often, push when ready.** Small commits make code review easier.
- **Never commit directly to `main`.** Always work on a feature branch.
- **Push to `origin`, PR to `upstream`.** Push goes to your fork. The PR targets Sagnik's repo.
- **Reference issues in PRs.** Using `Closes #<ISSUE_NUMBER>` auto-closes the issue on merge.
- **Personal Access Token:** Generate it once at `https://git.desineuron.in` → Profile → Settings → Applications → Generate Token. Store it securely (e.g., in your password manager or macOS Keychain).
- **Keep your fork's `main` clean.** Never push feature work directly to `origin/main` — only merge `upstream/main` into it to stay current.

View File

@@ -0,0 +1,214 @@
# Velocity iOS App - Technical Bible
**Document Version:** 1.0
**Last Updated:** 2026-03-07
**Application Version:** 1.1.0
---
## Table of Contents
1. [Executive Summary](#executive-summary)
2. [Application Overview](#application-overview)
3. [Technology Stack](#technology-stack)
4. [Architecture](#architecture)
5. [Project Structure](#project-structure)
6. [Core Modules](#core-modules)
7. [State Management](#state-management)
8. [Design System](#design-system)
9. [Development Guidelines](#development-guidelines)
10. [API Integration Points](#api-integration-points)
---
## Executive Summary
**Velocity iOS** is the native mobile counterpart of the Velocity premium real estate sales enablement platform. Built natively for Apple devices using SwiftUI, it brings the luxury property showcase and experience center ecosystems directly into the hands of sales teams on the showroom floor. The application features seamless integrations with AI-powered lead management (Oracle), biometric visitor tracking (Sentinel), and real-time inventory management.
**Purpose:** Provide sales teams with portable, native, intelligent insights and immersive property presentations with uncompromising performance and battery efficiency.
**Target Users:** Sales Directors, Real Estate Agents, Property Managers on the floor.
---
## Application Overview
### Key Features
1. **Dashboard** - Real-time metrics (Visitors, Revenue, AI Jobs, Listings), sentiment analysis gauge, system health monitoring, and a quick live AI Chat Widget.
2. **The Oracle** - AI-powered lead management interface to handle WhatsApp, web, and walk-in leads.
3. **The Sentinel** - Biometric visitor tracking interface, visualizing Live visitor IDs, emotion, confidence, dwell time, and zones.
4. **Inventory** - Real-time unit availability tracking, including augmented reality overlays (`ARSunOverlayView`).
5. **Settings** - App configurations and connectivity testing.
### User Flow
```
Launch (VelocityApp) -> ContentView (Entry Hub) -> Feature Tabs/Navigation -> Native Views (Dashboard, Oracle, Sentinel, etc.)
```
---
## Technology Stack
### Core Frameworks
- **Swift** - Primary programming language.
- **SwiftUI** - Declarative UI framework for building the entire interface.
- **Combine / Observation** (`@Observable`) - Reactive programming for robust state management.
### Networking & Data
- **Alamofire** (via `ComfyClient.swift`) - Elegant HTTP networking for handling requests (e.g. Dream Weaver AI image generation).
### UI & Animations
- **SwiftUI Animation** - Native transitions (`.easeInOut`, `.spring()`, `.numericText()`).
- **Glassmorphism natively** - via custom modifiers and blurs (`GlassCard`, `GlassBlurView`).
---
## Architecture
### Application Architecture
```text
┌─────────────────────────────────────────────────────────────┐
│ VelocityApp.swift │
│ (App Entry & WindowGroup Setup) │
└─────────────────────────────────┬───────────────────────────┘
┌─────────────────────────────────┴───────────────────────────┐
│ ContentView.swift │
│ (Main App Structure / Tab View) │
└──────┬────────────────────┬──────────────────────┬──────────┘
│ │ │
┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐
│ Dashboard │ │ Oracle │ │ Sentinel │
│ Module │ │ Module │ │ Module │
└─────────────┘ └─────────────┘ └─────────────┘
```
### State Architecture
A centralized `AppStore` leverages the `@Observable` macro to power the state of the app across different modules asynchronously.
```text
┌────────────────────────────────────────────────┐
│ AppStore.swift (@Observable) │
├────────────────────────────────────────────────┤
│ Metrics (DashboardMetrics) │
│ Visitors (Sentinel array) │
│ Alerts (isAlertActive, alertMessage) │
│ Leads & Messages (Oracle data) │
└──────────────────────┬─────────────────────────┘
┌─────────┴──────────┐
│ UI Subscriptions │
│ (Environment/State)│
└────────────────────┘
```
---
## Project Structure
### Directory Layout
```text
iOS/
├── App/ # App Lifecycle
│ ├── ContentView.swift # Main App Tab/Navigation Hub
│ └── VelocityApp.swift # @main Entry Point
├── Core/ # Shared infrastructure
│ ├── Math/
│ │ └── SunMath.swift # Calculation utilities for AR/Environment
│ ├── Networking/
│ │ └── ComfyClient.swift# Alamofire client (AI integrations)
│ ├── State/
│ │ └── AppStore.swift # Single source of truth state manager
│ └── UI/
│ │ ├── GlassBlurView.swift # Native blur implementations
│ │ └── VelocityTheme.swift # Design system tokens
└── Features/ # Feature-specific modules
├── Dashboard/
│ └── DashboardView.swift # Analytics, KPIs, Sentiment Gauge
├── Inventory/
│ ├── ARSunOverlayView.swift # AR overlay for property sun paths
│ └── InventoryView.swift
├── Oracle/
│ └── OracleView.swift # Lead progression and AI chat interface
├── Sentinel/
│ └── SentinelView.swift # Visitor facial & emotion analysis
└── Settings/
└── SettingsView.swift # App configuration
```
---
## Core Modules
### 1. Dashboard (`DashboardView.swift`)
**Purpose:** Executive overview matching the WebOS counterpart with native fluidity.
**Components:**
- **LiveKPICard**: Displays `Visitors`, `Revenue`, `AI Jobs`, and `Listings` with pulse animations.
- **Sentiment Gauge**: A dynamic, animated thermometer evaluating the overall showroom vibe.
- **System Health Panel**: Monitors underlying CPU/GPU/Memory configurations.
- **AI Chat Widget**: Embedded, fast-access AI query terminal with typing indicators and conversational memory.
### 2. Apps & AI Integration (`ComfyClient.swift` & Oracles)
The iOS app natively implements calls to standard HTTP backends and advanced AI generation services using an `Alamofire` `Session` directly connecting to high-GPU instances for rapid image synthesis (`DreamWeaverRequest`).
---
## State Management
### Zustand to AppStore Mapping
The iOS app natively replicates the WebOS `Zustand` state mechanism heavily through `AppStore.swift`.
- **Live Ticker:** Contains a background publisher running every 5 seconds that jitter-updates visitors, sentiment, and raises mock backend alerts.
- **Models:**
- `Visitor`: Contains `id`, `faceId`, `sentiment`, `confidence`, `zone`.
- `Lead`: Categorizes lead paths (Walk-in, Website, WhatsApp), Qualification (Whale, Potential), Budget.
- `ChatMessage` & `DashboardMetrics`: Shared data schemas for AI interactions and KPIs.
---
## Design System
**Location:** `Core/UI/VelocityTheme.swift`
### Color Palette
Precisely mirrors the web dashboard's high-contrast, premium dark mode:
- **Backgrounds**: True black `(0, 0, 0)` with elevated surfaces (`#131418`, `#22262e`).
- **Foregrounds**: Off-white for max readability, subtle grays for muted elements.
- **Accent**: `VelocityTheme.accent` `(0.231, 0.510, 0.965)` (Blue)
- **Semantic Status**: Matches SwiftUI `Color` blocks (`success`, `warning`, `danger$).
### Native Glassmorphism
Using native `VisualEffectView` or styled semi-transparent blocks wrapped in `.modifier(GlassCard())`.
```swift
// Implementation
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color(red: 0.031, green: 0.039, blue: 0.071).opacity(0.82))
.overlay(RoundedRectangle(cornerRadius: 16).stroke(VelocityTheme.borderAccent, lineWidth: 1))
.shadow(color: .black.opacity(0.55), radius: 16, y: 4)
)
```
---
## Development Guidelines
**UI Paradigms:**
- Leverage `ZStack` and `GeometryReader` for scale-dependent UI components like gauges and progress bars.
- Add subtle animations using `.animation(.easeInOut(duration: 0.4), value: ...)` to make data updates feel organic.
- Restrict logic calculations out of the `View` body, moving them to extensions, external math engines (`SunMath.swift`), or the `AppStore`.
---
## API Integration Points
**ComfyClient / Dream Weaver:**
- Core generation client utilizing `.post` to `http://192.168.x.x:8000/dream-weaver`.
- Automates formatting to Base64 formats for sending payloads to AI models and decoding the respective `output_base64` image back to a `UIImage`.
*(Note: Data sources like leads and visitors are currently heavily maintained via the native `AppStore` state but conform to general structures mapping to `/api/leads` and `/api/visitors` on the production backends).*

View File

@@ -0,0 +1,6 @@
# This file exists to track the `.Team Context` directory in Git.
# Git does not track empty directories. Once this folder contains
# team-facing documents (briefs, transcripts, personas), this file
# should be replaced or removed.
#
# Do NOT commit sensitive credentials or personal data here.

161
.gitignore vendored Normal file
View File

@@ -0,0 +1,161 @@
# ================================================
# Project Velocity - Root .gitignore
# Covers: Vite/React (app/), SwiftUI/Xcode (iOS/),
# Python (comfy_engine/), Infra, IDE, OS
# ================================================
# ────────────────────────────────────────────────
# OS & System
# ────────────────────────────────────────────────
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
desktop.ini
# ────────────────────────────────────────────────
# IDE & Editor
# ────────────────────────────────────────────────
.vscode/
.idea/
*.swp
*.swo
*~
.cursor/
.gemini/
# ────────────────────────────────────────────────
# Environment & Secrets
# ────────────────────────────────────────────────
.env
.env.local
.env.*.local
.env.development
.env.production
*.pem
*.key
*.cert
secrets.json
# ────────────────────────────────────────────────
# React / Vite / Node (app/)
# ────────────────────────────────────────────────
app/node_modules/
app/dist/
app/.vite/
app/coverage/
app/.cache/
# NPM / Yarn / PNPM
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
.pnpm-store/
package-lock.json
yarn.lock
# Optional: TypeScript build info
app/tsconfig.tsbuildinfo
*.tsbuildinfo
# ────────────────────────────────────────────────
# Xcode / Swift / iOS (iOS/)
# ────────────────────────────────────────────────
# Build output
iOS/**/build/
iOS/**/*.xcarchive
iOS/**/*.ipa
iOS/**/*.dSYM.zip
iOS/**/*.dSYM/
# Xcode derived data & caches
iOS/**/DerivedData/
iOS/**/*.pbxuser
!default.pbxuser
iOS/**/*.mode1v3
!default.mode1v3
iOS/**/*.mode2v3
!default.mode2v3
iOS/**/*.perspectivev3
!default.perspectivev3
iOS/**/*.xcworkspace/xcuserdata/
iOS/**/*.xcodeproj/xcuserdata/
iOS/**/xcshareddata/xctestrun/
# Package.resolved (Swift Package Manager keep if you want reproducible builds)
# iOS/**/*.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
# CocoaPods (if used)
iOS/**/Pods/
iOS/**/Podfile.lock
# Swift Package Manager build artifacts
iOS/**/.build/
iOS/**/.swiftpm/
# Playgrounds
iOS/**/*.playground/timeline.xctimeline
# Simulator logs
iOS/**/xcuserstated/
# ────────────────────────────────────────────────
# Python / ComfyUI Engine (comfy_engine/)
# ────────────────────────────────────────────────
comfy_engine/__pycache__/
comfy_engine/**/__pycache__/
comfy_engine/**/*.pyc
comfy_engine/**/*.pyo
comfy_engine/**/*.pyd
comfy_engine/.venv/
comfy_engine/venv/
comfy_engine/env/
comfy_engine/*.egg-info/
comfy_engine/dist/
comfy_engine/build/
comfy_engine/.pytest_cache/
comfy_engine/.mypy_cache/
comfy_engine/.ruff_cache/
# ComfyUI specific outputs
comfy_engine/output/
comfy_engine/input/
comfy_engine/models/
comfy_engine/custom_nodes/*/node_modules/
# ────────────────────────────────────────────────
# Infrastructure / Deployment
# ────────────────────────────────────────────────
infrastructure/**/*.tfstate
infrastructure/**/*.tfstate.backup
infrastructure/**/.terraform/
infrastructure/**/.terraform.lock.hcl
infrastructure/**/terraform.tfvars
*.tfvars
# Docker
*.log
docker-compose.override.yml
# ────────────────────────────────────────────────
# Agent / AI Context (do NOT commit sensitive internals)
# ────────────────────────────────────────────────
.Agent Context/transcript.json
.Agent Context/transcript.pdf
.Agent Context/*.docx
# ────────────────────────────────────────────────
# Misc
# ────────────────────────────────────────────────
*.tmp
*.bak
*.orig
*.log
*.pid
*.seed
*.pid.lock

View File

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

View File

@@ -5,6 +5,7 @@ struct VelocityApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.preferredColorScheme(.dark)
}
}
}

View File

@@ -1,6 +1,6 @@
import Foundation
import UIKit
import Alamofire
@preconcurrency import Alamofire
final class ComfyClient {
static let shared = ComfyClient()
@@ -45,12 +45,12 @@ final class ComfyClient {
}
}
private struct DreamWeaverRequest: Encodable {
private struct DreamWeaverRequest: Encodable, Sendable {
let imageBase64: String
let prompt: String
}
private struct DreamWeaverResponse: Decodable {
private struct DreamWeaverResponse: Decodable, Sendable {
let outputBase64: String
enum CodingKeys: String, CodingKey {

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,369 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
A27B23462F58DAF100A74A49 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = A27B23452F58DAF100A74A49 /* Alamofire */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
A27B23152F58D9C300A74A49 /* velocity.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = velocity.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
A27B23172F58D9C300A74A49 /* velocity */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = velocity;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
A27B23122F58D9C300A74A49 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A27B23462F58DAF100A74A49 /* Alamofire in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
A27B230C2F58D9C300A74A49 = {
isa = PBXGroup;
children = (
A27B23172F58D9C300A74A49 /* velocity */,
A27B23162F58D9C300A74A49 /* Products */,
);
sourceTree = "<group>";
};
A27B23162F58D9C300A74A49 /* Products */ = {
isa = PBXGroup;
children = (
A27B23152F58D9C300A74A49 /* velocity.app */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
A27B23142F58D9C300A74A49 /* velocity */ = {
isa = PBXNativeTarget;
buildConfigurationList = A27B23202F58D9C400A74A49 /* Build configuration list for PBXNativeTarget "velocity" */;
buildPhases = (
A27B23112F58D9C300A74A49 /* Sources */,
A27B23122F58D9C300A74A49 /* Frameworks */,
A27B23132F58D9C300A74A49 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
A27B23172F58D9C300A74A49 /* velocity */,
);
name = velocity;
packageProductDependencies = (
A27B23452F58DAF100A74A49 /* Alamofire */,
);
productName = velocity;
productReference = A27B23152F58D9C300A74A49 /* velocity.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
A27B230D2F58D9C300A74A49 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 2630;
LastUpgradeCheck = 2630;
TargetAttributes = {
A27B23142F58D9C300A74A49 = {
CreatedOnToolsVersion = 26.3;
};
};
};
buildConfigurationList = A27B23102F58D9C300A74A49 /* Build configuration list for PBXProject "velocity" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = A27B230C2F58D9C300A74A49;
minimizedProjectReferenceProxies = 1;
packageReferences = (
A27B23442F58DAF100A74A49 /* XCRemoteSwiftPackageReference "Alamofire" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = A27B23162F58D9C300A74A49 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
A27B23142F58D9C300A74A49 /* velocity */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
A27B23132F58D9C300A74A49 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
A27B23112F58D9C300A74A49 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
A27B231E2F58D9C400A74A49 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
A27B231F2F58D9C400A74A49 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.2;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
A27B23212F58D9C400A74A49 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = L29922NHD9;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSCameraUsageDescription = "\"Used to capture rooms for the DreamWeaver AI redesign feature.\"";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "\"Used to calculate the sun's path for the Sunseeker AR overlay.\"\n\"Used to calculate the sun's path for the Sunseeker AR overlay.\"\n\"Used to calculate the sun's path for the Sunseeker AR overlay.\"\n";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.desineouron.velocity;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
A27B23222F58D9C400A74A49 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = L29922NHD9;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSCameraUsageDescription = "\"Used to capture rooms for the DreamWeaver AI redesign feature.\"";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "\"Used to calculate the sun's path for the Sunseeker AR overlay.\"\n\"Used to calculate the sun's path for the Sunseeker AR overlay.\"\n\"Used to calculate the sun's path for the Sunseeker AR overlay.\"\n";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.desineouron.velocity;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
A27B23102F58D9C300A74A49 /* Build configuration list for PBXProject "velocity" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A27B231E2F58D9C400A74A49 /* Debug */,
A27B231F2F58D9C400A74A49 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
A27B23202F58D9C400A74A49 /* Build configuration list for PBXNativeTarget "velocity" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A27B23212F58D9C400A74A49 /* Debug */,
A27B23222F58D9C400A74A49 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
A27B23442F58DAF100A74A49 /* XCRemoteSwiftPackageReference "Alamofire" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Alamofire/Alamofire";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 5.0.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
A27B23452F58DAF100A74A49 /* Alamofire */ = {
isa = XCSwiftPackageProductDependency;
package = A27B23442F58DAF100A74A49 /* XCRemoteSwiftPackageReference "Alamofire" */;
productName = Alamofire;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = A27B230D2F58D9C300A74A49 /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,15 @@
{
"originHash" : "11b78eba97192d19796cff581fdf69b3e65b441188b1448a1b67e5d7b825a354",
"pins" : [
{
"identity" : "alamofire",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Alamofire/Alamofire",
"state" : {
"revision" : "3f99050e75bbc6fe71fc323adabb039756680016",
"version" : "5.11.1"
}
}
],
"version" : 3
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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