Compare commits
12 Commits
main
...
9e981e79ea
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e981e79ea | |||
| 7d54f9a233 | |||
| 184bfa77f8 | |||
| 7993d1fd0f | |||
| 52d9eef712 | |||
| 7e3764a8a4 | |||
| 333f321077 | |||
| 62ee3dd678 | |||
| 29d8e1ec16 | |||
| e746bdb718 | |||
| 4c0819d9dc | |||
| a4cb815338 |
@@ -1,265 +0,0 @@
|
||||
# Velocity Comms Integration — Handoff Document
|
||||
|
||||
## 1. Architecture Recommendation
|
||||
|
||||
### Goal
|
||||
Add a native **Conversations** module to Project Velocity so brokers/agents can manage WhatsApp (and future SMS/call) threads without leaving the WebOS.
|
||||
|
||||
### Design Philosophy
|
||||
- **Native Velocity UI**: Dark glass panels, compact density, blue accent, no iframe embeds.
|
||||
- **Provider-agnostic backend**: Abstract `CommsProvider` class with adapter pattern.
|
||||
- **CRM-first**: Every thread attempts to link to `crm_people` by `primary_phone`. Unresolved numbers are surfaced for manual linking.
|
||||
- **Mock-first development**: The module renders fully without real credentials.
|
||||
|
||||
### Provider Comparison
|
||||
|
||||
| Provider | Best For | Velocity Fit | 72-Hour Viability |
|
||||
|----------|----------|--------------|-------------------|
|
||||
| **Chatwoot** | Full support suite (email, SMS, WA) | Too heavy to embed; good UX reference | Low — would require stripping UI |
|
||||
| **WAHA** | Lightweight WhatsApp Web gateway | Good adapter candidate | High — simple REST, easy webhooks |
|
||||
| **Evolution API** | Modern WA gateway with groups, status, typing | Best adapter candidate | **High** — active community, clean webhooks |
|
||||
| **Meta Cloud API** | Official WABA; template-based outbound | Required for production scale at large builders | Medium — needs Meta Business verification |
|
||||
|
||||
**Recommended 72-hour route:**
|
||||
1. **Day 1**: Merge schema + backend routes + mock provider. Frontend compiles with mock data.
|
||||
2. **Day 2**: Connect Evolution API or WAHA in a staging environment. Test inbound webhook → thread creation.
|
||||
3. **Day 3**: CRM linking, settings UI, call-log upload placeholder, and smoke tests.
|
||||
|
||||
For production, plan a **dual-provider** setup:
|
||||
- **Evolution/WAHA** for quick conversational messaging (no Meta approval needed).
|
||||
- **Meta Cloud API** for template-based broadcast/re-engagement once Business Manager is verified.
|
||||
|
||||
---
|
||||
|
||||
## 2. Exact Files Created
|
||||
|
||||
```
|
||||
app/src/types/commsTypes.ts
|
||||
app/src/lib/commsApi.ts
|
||||
app/src/components/modules/Comms.tsx
|
||||
backend/db/schema_comms.sql
|
||||
backend/services/comms_provider.py
|
||||
backend/services/comms_waha_provider.py
|
||||
backend/services/comms_evolution_provider.py
|
||||
backend/services/comms_ingest.py
|
||||
backend/api/routes_comms.py
|
||||
COMMS_INTEGRATION_HANDOFF.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Patch Instructions for Existing Files
|
||||
|
||||
### A. `app/src/types/index.ts`
|
||||
Add `'comms'` to the `ModuleId` union:
|
||||
```typescript
|
||||
export type ModuleId = 'dashboard' | 'oracle' | 'sentinel' | 'inventory' | 'settings' | 'catalyst' | 'admin' | 'crm' | 'comms';
|
||||
```
|
||||
|
||||
### B. `app/src/App.tsx`
|
||||
1. Import the new component:
|
||||
```typescript
|
||||
import { Comms } from '@/components/modules/Comms';
|
||||
```
|
||||
2. Insert the route into `MODULE_ROUTES` **just before** `settings`:
|
||||
```typescript
|
||||
{ id: 'comms', path: '/comms', title: 'Conversations', component: Comms },
|
||||
```
|
||||
|
||||
### C. `app/src/components/layout/Sidebar.tsx`
|
||||
1. Import a new icon:
|
||||
```typescript
|
||||
import { MessageCircle } from 'lucide-react';
|
||||
```
|
||||
2. Add to `NAV_ICONS`:
|
||||
```typescript
|
||||
const NAV_ICONS: Record<string, LucideIcon> = {
|
||||
'/dashboard': LayoutGrid,
|
||||
'/oracle': MessageSquarePlus,
|
||||
'/sentinel': ScanFace,
|
||||
'/inventory': Building2,
|
||||
'/catalyst': Megaphone,
|
||||
'/comms': MessageCircle, // ← NEW
|
||||
'/settings': Sliders,
|
||||
'/admin': Shield,
|
||||
'/crm': Users,
|
||||
};
|
||||
```
|
||||
|
||||
### D. `backend/main.py`
|
||||
1. Import the router near the other imports:
|
||||
```python
|
||||
from backend.api.routes_comms import router as comms_router
|
||||
```
|
||||
2. Include it after the other routers:
|
||||
```python
|
||||
app.include_router(comms_router, prefix="/api/comms", tags=["Comms"])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Environment Variables
|
||||
|
||||
Add these to your `.env` or systemd environment:
|
||||
|
||||
```bash
|
||||
# Provider selection: mock | waha | evolution | meta_cloud
|
||||
COMMS_PROVIDER=mock
|
||||
|
||||
# Provider connectivity
|
||||
COMMS_PROVIDER_BASE_URL=
|
||||
COMMS_PROVIDER_API_KEY=
|
||||
COMMS_INSTANCE_ID=default
|
||||
|
||||
# Webhook security
|
||||
COMMS_WEBHOOK_SECRET=
|
||||
|
||||
# Phone normalization
|
||||
COMMS_DEFAULT_COUNTRY_CODE=91
|
||||
|
||||
# Media storage
|
||||
COMMS_MEDIA_STORAGE_DIR=/opt/dlami/nvme/assets/comms
|
||||
|
||||
# Transcription (none | openai | local)
|
||||
COMMS_TRANSCRIPTION_PROVIDER=none
|
||||
```
|
||||
|
||||
**No secrets are hardcoded in source.**
|
||||
|
||||
---
|
||||
|
||||
## 5. Database Migration
|
||||
|
||||
Run the SQL file against your Postgres database:
|
||||
|
||||
```bash
|
||||
psql $DATABASE_URL -f backend/db/schema_comms.sql
|
||||
```
|
||||
|
||||
Tables created:
|
||||
- `comms_threads` — conversation headers with CRM link
|
||||
- `comms_messages` — individual messages (inbound/outbound/system)
|
||||
- `comms_call_logs` — call records with optional transcript
|
||||
- `comms_settings` — key-value config store
|
||||
|
||||
---
|
||||
|
||||
## 6. API Routes
|
||||
|
||||
| Method | Path | Purpose |
|
||||
|--------|------|---------|
|
||||
| GET | `/api/comms/threads` | List threads (search, status, pagination) |
|
||||
| GET | `/api/comms/threads/{id}` | Get single thread with CRM enrichment |
|
||||
| GET | `/api/comms/threads/{id}/messages` | Chronological messages |
|
||||
| POST | `/api/comms/threads/{id}/messages` | Send outbound message via provider |
|
||||
| POST | `/api/comms/threads/{id}/link-person` | Link thread to `crm_people.id` |
|
||||
| POST | `/api/comms/threads/{id}/notes` | Add system note |
|
||||
| POST | `/api/comms/threads/{id}/tasks` | Add system task |
|
||||
| POST | `/api/comms/webhooks/{provider}` | Public webhook endpoint |
|
||||
| GET | `/api/comms/settings` | Get comms configuration |
|
||||
| PATCH | `/api/comms/settings` | Update configuration |
|
||||
| POST | `/api/comms/provider/test` | Test provider connectivity |
|
||||
| POST | `/api/comms/recordings/transcribe` | Queue transcription job |
|
||||
|
||||
---
|
||||
|
||||
## 7. Frontend Route Changes
|
||||
|
||||
- New sidebar item: **Conversations** (icon: `MessageCircle`)
|
||||
- Position: directly above **Settings**
|
||||
- Route: `/comms`
|
||||
- Component: `Comms.tsx` with three-pane layout (Inbox | Chat | CRM Rail)
|
||||
|
||||
---
|
||||
|
||||
## 8. Settings Changes
|
||||
|
||||
A new **Communications** subsection should be added inside your existing Settings module (or as a standalone card). Fields:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| Provider | select | mock / waha / evolution / meta_cloud |
|
||||
| Provider Base URL | text | e.g. `http://localhost:3000` |
|
||||
| API Key | password | masked after save |
|
||||
| Instance ID | text | WA/Evolution session name |
|
||||
| Phone Number ID | text | Meta Cloud API only |
|
||||
| Webhook Callback URL | text | Auto-populated or custom |
|
||||
| Webhook Secret | password | Sets `webhook_secret_set` flag |
|
||||
| Default Assignment User | select | User dropdown from `/api/auth/users` |
|
||||
| Auto-link by Phone | toggle | Match `crm_people.primary_phone` automatically |
|
||||
| Create CRM Interaction on Inbound | toggle | Write to `intel_interactions` if table exists |
|
||||
| Default Country Code | text | e.g. `91` for India |
|
||||
| Transcription Provider | select | none / openai / local |
|
||||
| Connection Test | button | Calls `POST /api/comms/provider/test` |
|
||||
|
||||
---
|
||||
|
||||
## 9. Smoke Test Steps
|
||||
|
||||
1. **DB**: Run `schema_comms.sql`. Verify tables exist.
|
||||
2. **Backend**: Start FastAPI. Confirm `/health` returns `db_pool: connected`.
|
||||
3. **Backend**: `curl -X POST http://localhost:8000/api/comms/provider/test` → should return mock success.
|
||||
4. **Frontend**: Load Velocity. Sidebar should show **Conversations**.
|
||||
5. **Frontend**: Click Conversations. Mock mode should render 3 threads and messages.
|
||||
6. **Frontend**: Send a message in mock thread. Optimistic update → mock delivery checkmark.
|
||||
7. **Backend**: Post sample webhook:
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/comms/webhooks/evolution -H "Content-Type: application/json" -d '{"event":"messages.upsert","instance":"default","data":{"key":{"remoteJid":"919876543210@s.whatsapp.net","fromMe":false,"id":"test-1"},"message":{"conversation":"Hello from webhook"},"messageTimestamp":1710000000}}'
|
||||
```
|
||||
8. **Backend**: Verify thread + message inserted. Check `comms_threads` for new row.
|
||||
9. **Frontend**: Refresh inbox. New thread should appear.
|
||||
10. **CRM Link**: Click "Link to Contact" (or call `POST /api/comms/threads/{id}/link-person`) and verify `person_id` is set.
|
||||
|
||||
---
|
||||
|
||||
## 10. Known Limitations
|
||||
|
||||
- **Call recording via WhatsApp API**: Neither WAHA nor Evolution supports native WhatsApp call recording. Call logs are designed for **external telephony intake** (manual upload or webhook from a PBX/VoIP system). Recording file + transcript workflow is scaffolded but needs a real transcription provider (OpenAI Whisper, AWS Transcribe, or faster-whisper) wired in.
|
||||
- **Media downloads**: `get_media()` is stubbed for WAHA/Evolution. Production needs signed URL handling or local file download.
|
||||
- **Meta Cloud API adapter**: Not yet implemented. Add `comms_meta_provider.py` when Meta Business verification is complete.
|
||||
- **Template messages**: Only placeholder methods exist. Template approval flow (Meta) or local template storage must be built for outbound campaigns.
|
||||
- **Webhook auth**: Currently accepts any payload. Add HMAC/signature verification per provider before production.
|
||||
- **Rate limiting**: Not implemented. Add FastAPI rate-limit middleware on `/api/comms/webhooks/{provider}`.
|
||||
- **phonenumbers library**: `comms_ingest.py` gracefully degrades to regex if `phonenumbers` is not installed. Install it for robust E.164 normalization:
|
||||
```bash
|
||||
pip install phonenumbers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. What Still Needs Real Credentials
|
||||
|
||||
| Item | What You Need |
|
||||
|------|---------------|
|
||||
| **Evolution API** | A running Evolution instance (Docker), API key, and a paired WhatsApp number. |
|
||||
| **WAHA** | A running WAHA container, session QR-scan, and API key. |
|
||||
| **Meta Cloud API** | Meta Business Manager, verified business, WhatsApp Business Account, permanent access token, phone number ID. |
|
||||
| **Transcription** | OpenAI API key (for Whisper) or local faster-whisper model path. |
|
||||
| **CRM enrichment** | Ensure `crm_people` table exists with `primary_phone` indexed. |
|
||||
|
||||
---
|
||||
|
||||
## 12. What to Verify Before Production
|
||||
|
||||
- [ ] Webhook endpoint is exposed via HTTPS (ngrok/cloudflare tunnel for local dev).
|
||||
- [ ] `COMMS_WEBHOOK_SECRET` is set and signature verification is enabled in `routes_comms.py`.
|
||||
- [ ] Database has indexes on `comms_threads(phone_e164)` and `comms_messages(thread_id, created_at)`.
|
||||
- [ ] `crm_people.primary_phone` is normalized to E.164 before comms matching.
|
||||
- [ ] Media storage directory exists and is writable (`COMMS_MEDIA_STORAGE_DIR`).
|
||||
- [ ] Outbound message queue / retry logic is added (currently synchronous).
|
||||
- [ ] GDPR/opt-out handling is implemented if targeting EU markets.
|
||||
- [ ] Backup strategy for `comms_messages` (contains legal conversation records).
|
||||
|
||||
---
|
||||
|
||||
## 13. Next Iteration Ideas
|
||||
|
||||
- **Bulk broadcast**: Template-based outbound to filtered CRM segments.
|
||||
- **AI reply suggestions**: Integrate Oracle / local LLM to draft replies based on CRM context.
|
||||
- **Voice notes**: Upload `.ogg` audio, transcribe, store transcript as message.
|
||||
- **Read receipts**: Poll provider for delivery/read status and update `comms_messages`.
|
||||
- **Assignment rules**: Round-robin or load-based auto-assignment to agents.
|
||||
|
||||
---
|
||||
|
||||
*Document generated for Project Velocity v1.1 — Comms Module Integration*
|
||||
@@ -1,565 +0,0 @@
|
||||
# Codebase Analysis v1.1.md
|
||||
|
||||
## Table of Contents / Chapters
|
||||
|
||||
### 1. Overview
|
||||
- Introduction to Project Velocity and its purpose
|
||||
- Core principles and approach
|
||||
|
||||
### 2. Architectural Mapping
|
||||
- Overall System Architecture (Mermaid diagram)
|
||||
- File Dependency Graph (Mermaid diagram)
|
||||
- Data Flow Architecture (Mermaid diagram)
|
||||
|
||||
### 3. Logic Decomposition
|
||||
- Authentication & Authorization
|
||||
- CRM Data Model
|
||||
- Sentinel Biometric Intelligence
|
||||
- Oracle Natural Language Intelligence
|
||||
- Catalyst Marketing Orchestration
|
||||
- Infrastructure & Deployment
|
||||
|
||||
### 4. Connectivity Matrix
|
||||
- Component interconnections and data flow
|
||||
- Interconnection rationale
|
||||
|
||||
### 5. First-Principles Guide
|
||||
- Core Concept: AI-Augmented Sales Intelligence
|
||||
- Why Real Estate Specifically?
|
||||
- Principle 1: Data Sovereignty First
|
||||
- Principle 2: Real-Time Perception Matters
|
||||
- Principle 3: Intelligence Through Conversation
|
||||
- Principle 4: Visual Storytelling Drives Sales
|
||||
- Principle 5: Revision Control for Business Logic
|
||||
- Design Philosophy: Production-Ready Craft
|
||||
- Why This Architecture Succeeds
|
||||
|
||||
### 6. API Endpoints Reference
|
||||
- Authentication Endpoints
|
||||
- CRM Endpoints
|
||||
- Analytics Endpoints
|
||||
- Oracle AI Intelligence Endpoints
|
||||
- Oracle Canvas Management (v1)
|
||||
- Oracle Template Management
|
||||
- Sentinel Biometric Intelligence Endpoints
|
||||
- Catalyst Marketing Orchestration Endpoints
|
||||
- Vault Trackable Links Endpoints
|
||||
- CCTV Surveillance Integration Endpoints
|
||||
- Video Scene Mapping Endpoints
|
||||
- Marketing Videos Endpoints
|
||||
- Mobile Edge Communication Endpoints
|
||||
- Inventory Management Endpoints
|
||||
- Admin Surface Control Endpoints
|
||||
- CRM Canonical Data Endpoints
|
||||
- Runtime LLM Endpoints
|
||||
- Infrastructure Notes
|
||||
|
||||
## Overview
|
||||
|
||||
Project Velocity is an on-prem real estate operating system designed for high-value property sales. It combines a premium WebOS, an iPad field app, a FastAPI neural core, ComfyUI-based media generation, and biometric/sentiment-assisted sales intelligence. The system enables brokers to operate at the speed of AI while preserving control, provenance, and safety for customer and revenue-critical data.
|
||||
|
||||
This analysis provides a comprehensive understanding of the codebase from first principles, applying the Feynman Technique to distill complex implementations into intuitive concepts.
|
||||
|
||||
## Architectural Mapping
|
||||
|
||||
### Overall System Architecture
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "User Interfaces"
|
||||
WebOS[Velocity WebOS<br/>React + TypeScript]
|
||||
iPad[iPad App<br/>Swift + MediaPipe]
|
||||
end
|
||||
|
||||
subgraph "Core Backend"
|
||||
FastAPI[FastAPI Neural Core<br/>PostgreSQL + JWT Auth]
|
||||
end
|
||||
|
||||
subgraph "AI Services"
|
||||
Oracle[The Oracle<br/>Natural Language Intelligence]
|
||||
Sentinel[The Sentinel<br/>Biometric Perception Engine]
|
||||
Catalyst[The Catalyst<br/>Marketing Campaign Orchestration]
|
||||
Comfy[ComfyUI / Dream Weaver<br/>Media Generation]
|
||||
end
|
||||
|
||||
subgraph "Infrastructure"
|
||||
AWS[AWS GPU Workers<br/>NVIDIA GPUs]
|
||||
S3[S3 Asset Store<br/>Models + Media]
|
||||
Linux[Linux Control Surface<br/>On-prem Deployment]
|
||||
end
|
||||
|
||||
WebOS --> FastAPI
|
||||
iPad --> FastAPI
|
||||
FastAPI --> Oracle
|
||||
FastAPI --> Sentinel
|
||||
FastAPI --> Catalyst
|
||||
Catalyst --> Comfy
|
||||
Comfy --> AWS
|
||||
Comfy --> S3
|
||||
FastAPI --> Linux
|
||||
|
||||
style FastAPI fill:#e1f5fe
|
||||
style Oracle fill:#f3e5f5
|
||||
style Sentinel fill:#e8f5e8
|
||||
style Catalyst fill:#fff3e0
|
||||
```
|
||||
|
||||
### File Dependency Graph
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "Frontend (React/Vite)"
|
||||
App[App.tsx<br/>Routing & Auth]
|
||||
Store[useStore.ts<br/>Zustand State]
|
||||
Components[Components/<br/>Modules & UI]
|
||||
API[api.ts<br/>HTTP Client]
|
||||
end
|
||||
|
||||
subgraph "Backend (FastAPI)"
|
||||
Main[main.py<br/>App Entry]
|
||||
Routers[routers/<br/>API Endpoints]
|
||||
Services[services/<br/>Business Logic]
|
||||
DB[db/<br/>Schema & Pool]
|
||||
Auth[auth/<br/>JWT & Users]
|
||||
Oracle[oracle/<br/>AI Intelligence]
|
||||
end
|
||||
|
||||
subgraph "AI Infrastructure"
|
||||
Comfy[comfy_engine/<br/>Media Generation]
|
||||
Models[models/<br/>AI Models]
|
||||
Prompts[nemoclaw_prompts/<br/>LLM Templates]
|
||||
end
|
||||
|
||||
subgraph "Deployment"
|
||||
Infra[infrastructure/<br/>Linux + AWS]
|
||||
Agents[agents/<br/>Orchestration]
|
||||
end
|
||||
|
||||
App --> API
|
||||
API --> Main
|
||||
Main --> Routers
|
||||
Routers --> Services
|
||||
Services --> DB
|
||||
Services --> Auth
|
||||
Services --> Oracle
|
||||
Oracle --> Prompts
|
||||
Comfy --> Infra
|
||||
Infra --> Agents
|
||||
|
||||
style Main fill:#e3f2fd
|
||||
style Oracle fill:#fce4ec
|
||||
```
|
||||
|
||||
### Data Flow Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
User[User Input] --> UI[WebOS/iPad UI]
|
||||
UI --> API[FastAPI Endpoints]
|
||||
API --> Auth[JWT Authentication]
|
||||
API --> Policy[Policy Engine<br/>Authorization]
|
||||
API --> LLM[Nemoclaw LLM<br/>Reasoning & Planning]
|
||||
LLM --> Query[SQL Generation<br/>Safe Queries]
|
||||
Query --> DB[(PostgreSQL<br/>CRM + Intelligence)]
|
||||
DB --> Results[Query Results]
|
||||
Results --> Viz[Visualization<br/>Components]
|
||||
Viz --> Canvas[Oracle Canvas<br/>Persistent Views]
|
||||
Canvas --> UI
|
||||
|
||||
Sentinel[Sentinel Biometric] --> WS[WebSocket<br/>Real-time]
|
||||
WS --> Perception[Face Analysis<br/>MediaPipe]
|
||||
Perception --> QD[QD Scoring<br/>NemoClaw]
|
||||
QD --> DB
|
||||
|
||||
Catalyst[Catalyst Marketing] --> Comfy[ComfyUI<br/>Media Generation]
|
||||
Comfy --> S3[S3 Assets]
|
||||
Comfy --> GPU[AWS GPUs]
|
||||
|
||||
style DB fill:#fff9c4
|
||||
style LLM fill:#e8f5e8
|
||||
style Comfy fill:#fce4ec
|
||||
```
|
||||
|
||||
## Logic Decomposition
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
**What:** Secure access control for all system functions
|
||||
**How:** JWT tokens with role-based permissions (ADMIN, SALES_DIRECTOR, SENIOR_BROKER, JUNIOR_BROKER)
|
||||
**Why:** Real estate involves sensitive client data; strict access prevents unauthorized sales interference
|
||||
|
||||
**Key Implementation:**
|
||||
- `backend/auth/dependencies.py`: JWT validation, user extraction, role enforcement
|
||||
- `main.py`: Login endpoint with password hashing, user profile management
|
||||
- Role hierarchy prevents junior brokers from approving high-value deals
|
||||
|
||||
### CRM Data Model
|
||||
|
||||
**What:** Canonical client and interaction records
|
||||
**How:** PostgreSQL schema with leads, contacts, opportunities, interactions
|
||||
**Why:** Real estate sales require accurate pipeline tracking and relationship history
|
||||
|
||||
**Key Tables:**
|
||||
- `leads_intelligence`: Core lead data with QD scores and tags
|
||||
- `omnichannel_logs`: All interactions (calls, emails, visits) with timestamps
|
||||
- `crm_people/crm_leads`: Structured client relationships and deal stages
|
||||
|
||||
### Sentinel Biometric Intelligence
|
||||
|
||||
**What:** Real-time visitor sentiment analysis for showroom engagement
|
||||
**How:** Browser webcam + MediaPipe face landmarking → blend shapes → QD scoring
|
||||
**Why:** Human emotion drives buying decisions; AI detects subtle cues brokers miss
|
||||
|
||||
**Execution Flow:**
|
||||
1. Browser captures video stream
|
||||
2. MediaPipe extracts facial landmarks (68 points)
|
||||
3. Blend shapes calculated (brow furrow, smile intensity, etc.)
|
||||
4. NemoClaw LLM scores 1-100 QD (Qualification Desire)
|
||||
5. Real-time dashboard updates for brokers
|
||||
|
||||
**Key Decision:** Browser-side processing preserves privacy (no video leaves client device)
|
||||
|
||||
### Oracle Natural Language Intelligence
|
||||
|
||||
**What:** AI-powered data analysis through conversational queries
|
||||
**How:** Natural language → SQL planning → visualization components → canvas
|
||||
**Why:** Brokers shouldn't need SQL knowledge; AI translates business questions to insights
|
||||
|
||||
**Architecture Layers:**
|
||||
- **Prompt Orchestrator:** Decomposes complex requests into sub-queries
|
||||
- **Natural DB Agent:** Schema introspection + safe SQL generation
|
||||
- **Canvas Service:** Persistent visual workspaces with revision history
|
||||
- **Collaboration:** Fork/merge workflows for team coordination
|
||||
|
||||
**Why Canvas?** Unlike transient chat responses, canvases persist as living documents
|
||||
|
||||
### Catalyst Marketing Orchestration
|
||||
|
||||
**What:** Automated campaign creation and asset generation
|
||||
**How:** Meta Ads API integration + ComfyUI media workflows
|
||||
**Why:** Luxury real estate needs high-quality visuals; manual creation is too slow
|
||||
|
||||
**Workflow:**
|
||||
1. Campaign parameters → Meta API calls
|
||||
2. Creative assets → ComfyUI (Dream Weaver, Wan 2.2, Qwen poster generation)
|
||||
3. GPU processing on AWS → S3 storage
|
||||
4. Performance tracking back to CRM
|
||||
|
||||
### Infrastructure & Deployment
|
||||
|
||||
**What:** Production-ready on-prem + cloud hybrid deployment
|
||||
**How:** Linux control surface + AWS GPU workers + stable ingress
|
||||
**Why:** Real estate firms demand data sovereignty; cloud-only solutions unacceptable
|
||||
|
||||
**Key Components:**
|
||||
- **Ingress:** Caddy on EC2 t4g.micro with TLS termination
|
||||
- **Backend:** Linux systemd services for FastAPI, ComfyUI, LLM runtime
|
||||
- **GPU:** AWS managed instances for media generation
|
||||
- **Assets:** S3 for model storage, NVMe for fast access
|
||||
|
||||
## Connectivity Matrix
|
||||
|
||||
| Component | Inputs | Outputs | Dependencies | Protocols |
|
||||
|-----------|--------|---------|--------------|-----------|
|
||||
| WebOS Frontend | User actions, API responses | UI renders, API calls | FastAPI backend | HTTP/WS, JWT |
|
||||
| iPad App | Camera feeds, user input | Biometric data, inventory scans | FastAPI backend | HTTP/WS |
|
||||
| FastAPI Core | API requests, WS connections | DB queries, AI responses | PostgreSQL, Redis (future) | SQL, HTTP |
|
||||
| Oracle Engine | Natural language prompts | Canvas components | NemoClaw LLM, PostgreSQL | Internal API |
|
||||
| Sentinel Engine | Webcam streams | QD scores, alerts | MediaPipe, NemoClaw | WS real-time |
|
||||
| Catalyst Engine | Campaign specs | Ad creatives, assets | Meta API, ComfyUI | HTTP, S3 |
|
||||
| ComfyUI | Generation requests | Images/videos | GPU workers, S3 | Internal queue |
|
||||
| PostgreSQL | SQL queries | Structured data | - | SQL |
|
||||
| AWS GPUs | Media jobs | Generated assets | ComfyUI workflows | SSH/tunnel |
|
||||
|
||||
**Interconnection Rationale:**
|
||||
- **WebOS ↔ Backend:** Thin client architecture; all business logic server-side for security
|
||||
- **Backend ↔ AI Services:** Modular design; each AI component (Oracle, Sentinel, Catalyst) operates independently but shares auth/policy
|
||||
- **AI Services ↔ DB:** Direct SQL access with row-level security; no ORM abstraction to maintain performance
|
||||
- **Infrastructure:** Hybrid on-prem/cloud; sensitive data stays on-prem, compute-intensive tasks use cloud GPUs
|
||||
|
||||
## First-Principles Guide
|
||||
|
||||
### Core Concept: AI-Augmented Sales Intelligence
|
||||
|
||||
At its foundation, Project Velocity operates on the principle that human sales professionals excel at relationship-building and deal-closing, while AI excels at pattern recognition, data synthesis, and repetitive analysis. The system doesn't replace brokers—it amplifies their capabilities by providing real-time insights they couldn't otherwise access.
|
||||
|
||||
**Why Real Estate Specifically?**
|
||||
- High-value transactions ($M+ deals) with long sales cycles (months)
|
||||
- Emotional decision-making influenced by subtle cues
|
||||
- Complex data relationships (properties, buyers, markets, timing)
|
||||
- Regulatory compliance requirements
|
||||
- Need for visual storytelling (luxury properties)
|
||||
|
||||
### Principle 1: Data Sovereignty First
|
||||
|
||||
**Fundamental Truth:** Real estate firms own their client relationships. Project Velocity runs on-premise or in tenant-controlled cloud to maintain this ownership.
|
||||
|
||||
**Implementation:** Linux-based deployment with optional AWS GPU extensions. All client data remains within tenant boundaries; only anonymous model requests leave for AI processing.
|
||||
|
||||
### Principle 2: Real-Time Perception Matters
|
||||
|
||||
**Fundamental Truth:** Buying decisions happen in moments of emotional connection. Project Velocity captures these moments through biometric analysis.
|
||||
|
||||
**Implementation:** Sentinel uses facial expression analysis to score "Qualification Desire" (QD) on a 1-100 scale, alerting brokers to engagement spikes during property tours.
|
||||
|
||||
### Principle 3: Intelligence Through Conversation
|
||||
|
||||
**Fundamental Truth:** Sales professionals think in business terms, not database queries. The Oracle translates natural language into structured analytics.
|
||||
|
||||
**Implementation:** Users ask "Show me whale leads from Dubai this quarter" and receive visual dashboards. The system plans safe SQL queries, executes them, and renders results as persistent canvas components.
|
||||
|
||||
### Principle 4: Visual Storytelling Drives Sales
|
||||
|
||||
**Fundamental Truth:** Luxury properties sell through aspiration and emotion. AI-generated media must be photorealistic and brand-consistent.
|
||||
|
||||
**Implementation:** ComfyUI workflows (Dream Weaver, Wan 2.2) create property visualizations. Catalyst orchestrates campaigns with generated assets automatically uploaded to Meta Ads.
|
||||
|
||||
### Principle 5: Revision Control for Business Logic
|
||||
|
||||
**Fundamental Truth:** Sales strategies evolve through collaboration and iteration. Oracle canvases use Git-like branching for analytical workflows.
|
||||
|
||||
**Implementation:** Canvas pages support forks, merge requests, and revision history. Brokers can experiment with analysis approaches without breaking production views.
|
||||
|
||||
### Design Philosophy: Production-Ready Craft
|
||||
|
||||
Project Velocity follows "experienced engineer" principles:
|
||||
- **Error Handling:** No silent failures; all errors surface useful messages
|
||||
- **Type Safety:** TypeScript frontend, typed Python backend
|
||||
- **Performance:** Async everywhere, connection pooling, efficient queries
|
||||
- **Security:** JWT auth, role-based access, input validation
|
||||
- **Observability:** Structured logging, health checks, WebSocket monitoring
|
||||
- **Maintainability:** Clear separation of concerns, comprehensive documentation
|
||||
|
||||
### Why This Architecture Succeeds
|
||||
|
||||
1. **Modular AI Services:** Each intelligence component (Oracle, Sentinel, Catalyst) operates independently, allowing incremental improvement and specialized optimization.
|
||||
|
||||
2. **Hybrid Infrastructure:** Combines on-prem reliability with cloud scalability. Sensitive CRM data stays local; compute-intensive media generation uses managed GPUs.
|
||||
|
||||
3. **Real-Time Integration:** WebSockets enable live updates across all surfaces. Brokers see lead scoring changes instantly, dashboard metrics update in real-time.
|
||||
|
||||
4. **Business Logic in Code:** Revenue-critical workflows (lead qualification, campaign approval, deal closing) are explicit code paths, not AI hallucinations.
|
||||
|
||||
5. **User-Centric Design:** The WebOS feels like a native application, not a bolted-on AI interface. Familiar patterns (canvases, dashboards, forms) reduce training time.
|
||||
|
||||
This architecture transforms real estate sales from intuition-driven processes into data-augmented, AI-accelerated operations while preserving the human elements that drive luxury transactions.
|
||||
|
||||
## API Endpoints Reference
|
||||
|
||||
This section provides a comprehensive catalog of all API endpoints exposed by the Project Velocity backend, organized by functional module. Each endpoint includes the HTTP method, URI path, absolute HTTPS URL, and a brief description of its functionality. This serves as the definitive source of truth for the project's routing and interface architecture.
|
||||
|
||||
### Authentication Endpoints
|
||||
|
||||
| Method | Path | Absolute URL | Description |
|
||||
|--------|------|--------------|-------------|
|
||||
| POST | /api/auth/login | https://velocity.desineuron.in/api/auth/login | Authenticate a user with email/password and return JWT token |
|
||||
| GET | /api/auth/me | https://velocity.desineuron.in/api/auth/me | Get current authenticated user's profile information |
|
||||
| GET | /api/auth/users | https://velocity.desineuron.in/api/auth/users | List all active users in the system |
|
||||
| POST | /api/auth/profile/avatar | https://velocity.desineuron.in/api/auth/profile/avatar | Upload and update user's profile avatar image |
|
||||
|
||||
### CRM Endpoints
|
||||
|
||||
| Method | Path | Absolute URL | Description |
|
||||
|--------|------|--------------|-------------|
|
||||
| GET | /api/leads | https://velocity.desineuron.in/api/leads | List leads with pagination and filtering |
|
||||
| GET | /api/leads/{lead_id} | https://velocity.desineuron.in/api/leads/{lead_id} | Get detailed information for a specific lead |
|
||||
| GET | /api/kanban/board | https://velocity.desineuron.in/api/kanban/board | Retrieve the kanban board view of leads by stage |
|
||||
| GET | /api/chat-logs | https://velocity.desineuron.in/api/chat-logs | List chat logs for leads with optional lead filtering |
|
||||
| GET | /api/leads/demographics | https://velocity.desineuron.in/api/leads/demographics | Get demographic analytics for leads (source, qualification) |
|
||||
|
||||
### Analytics Endpoints
|
||||
|
||||
| Method | Path | Absolute URL | Description |
|
||||
|--------|------|--------------|-------------|
|
||||
| GET | /api/analytics/sentiment-scatter | https://velocity.desineuron.in/api/analytics/sentiment-scatter | Get scatter plot data for sentiment analysis |
|
||||
|
||||
### Oracle AI Intelligence Endpoints
|
||||
|
||||
| Method | Path | Absolute URL | Description |
|
||||
|--------|------|--------------|-------------|
|
||||
| GET | /api/oracle/health | https://velocity.desineuron.in/api/oracle/health | Check Oracle system health and MCP tool availability |
|
||||
| GET | /api/oracle/data-health | https://velocity.desineuron.in/api/oracle/data-health | Get data health metrics for database tables |
|
||||
| GET | /api/oracle/schema-catalog | https://velocity.desineuron.in/api/oracle/schema-catalog | Retrieve schema catalog for database introspection |
|
||||
| POST | /api/oracle/query | https://velocity.desineuron.in/api/oracle/query | Execute natural language query against database |
|
||||
| GET | /api/oracle/mcp/tools | https://velocity.desineuron.in/api/oracle/mcp/tools | List available MCP tools for execution |
|
||||
| POST | /api/oracle/mcp/execute | https://velocity.desineuron.in/api/oracle/mcp/execute | Execute an MCP tool with given query |
|
||||
| POST | /api/oracle/workflow/preview | https://velocity.desineuron.in/api/oracle/workflow/preview | Preview workflow plan for a natural language prompt |
|
||||
|
||||
#### Oracle Canvas Management (v1)
|
||||
|
||||
| Method | Path | Absolute URL | Description |
|
||||
|--------|------|--------------|-------------|
|
||||
| POST | /api/oracle/v1/canvas/pages | https://velocity.desineuron.in/api/oracle/v1/canvas/pages | Create a new Oracle canvas page |
|
||||
| GET | /api/oracle/v1/canvas/pages | https://velocity.desineuron.in/api/oracle/v1/canvas/pages | List user's Oracle canvas pages |
|
||||
| GET | /api/oracle/v1/canvas/pages/{page_id} | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id} | Get a specific canvas page |
|
||||
| PUT | /api/oracle/v1/canvas/pages/{page_id} | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id} | Update a canvas page |
|
||||
| DELETE | /api/oracle/v1/canvas/pages/{page_id} | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id} | Delete a canvas page |
|
||||
| POST | /api/oracle/v1/canvas/pages/{page_id}/fork | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/fork | Create a fork of a canvas page |
|
||||
| POST | /api/oracle/v1/canvas/pages/{page_id}/merge | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/merge | Merge changes into a canvas page |
|
||||
| GET | /api/oracle/v1/canvas/pages/{page_id}/revisions | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/revisions | List revisions for a canvas page |
|
||||
| POST | /api/oracle/v1/canvas/pages/{page_id}/revisions | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/revisions | Create new revision for a canvas page |
|
||||
| GET | /api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id} | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id} | Get specific revision |
|
||||
| PUT | /api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id} | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id} | Update a revision |
|
||||
| DELETE | /api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id} | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id} | Delete a revision |
|
||||
| GET | /api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id}/components | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id}/components | List components in a revision |
|
||||
| POST | /api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id}/components | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id}/components | Add component to revision |
|
||||
| PUT | /api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id}/components/{component_id} | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id}/components/{component_id} | Update component |
|
||||
| DELETE | /api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id}/components/{component_id} | https://velocity.desineuron.in/api/oracle/v1/canvas/pages/{page_id}/revisions/{revision_id}/components/{component_id} | Delete component |
|
||||
|
||||
#### Oracle Template Management
|
||||
|
||||
| Method | Path | Absolute URL | Description |
|
||||
|--------|------|--------------|-------------|
|
||||
| GET | /api/oracle/template-chapters | https://velocity.desineuron.in/api/oracle/template-chapters | List Oracle template chapters |
|
||||
| POST | /api/oracle/template-chapters | https://velocity.desineuron.in/api/oracle/template-chapters | Create new template chapter |
|
||||
| GET | /api/oracle/template-subchapters | https://velocity.desineuron.in/api/oracle/template-subchapters | List Oracle template subchapters |
|
||||
| POST | /api/oracle/template-subchapters | https://velocity.desineuron.in/api/oracle/template-subchapters | Create new template subchapter |
|
||||
| GET | /api/oracle/component-templates | https://velocity.desineuron.in/api/oracle/component-templates | List Oracle component templates |
|
||||
| POST | /api/oracle/component-templates | https://velocity.desineuron.in/api/oracle/component-templates | Create new component template |
|
||||
| GET | /api/oracle/component-templates/{template_id} | https://velocity.desineuron.in/api/oracle/component-templates/{template_id} | Get specific component template |
|
||||
| POST | /api/oracle/component-templates/{template_id}/seed | https://velocity.desineuron.in/api/oracle/component-templates/{template_id}/seed | Add seed example to template |
|
||||
| GET | /api/oracle/component-templates/{template_id}/seed | https://velocity.desineuron.in/api/oracle/component-templates/{template_id}/seed | List seed examples for template |
|
||||
| POST | /api/oracle/component-templates/synthetic-jobs | https://velocity.desineuron.in/api/oracle/component-templates/synthetic-jobs | Submit synthetic template generation job |
|
||||
|
||||
### Sentinel Biometric Intelligence Endpoints
|
||||
|
||||
| Method | Path | Absolute URL | Description |
|
||||
|--------|------|--------------|-------------|
|
||||
| WebSocket | /api/sentinel/ws/notifications | wss://velocity.desineuron.in/api/sentinel/ws/notifications | Real-time notifications WebSocket |
|
||||
| WebSocket | /api/sentinel/ws/perception | wss://velocity.desineuron.in/api/sentinel/ws/perception | Biometric perception data WebSocket |
|
||||
| POST | /api/sentinel/consent | https://velocity.desineuron.in/api/sentinel/consent | Record biometric consent for lead |
|
||||
| POST | /api/sentinel/session/complete | https://velocity.desineuron.in/api/sentinel/session/complete | Close a perception session and finalize QD score |
|
||||
| POST | /api/sentinel/tag-lead | https://velocity.desineuron.in/api/sentinel/tag-lead | Apply NemoClaw lead tagging to CRM lead |
|
||||
| GET | /api/sentinel/qd-score/{lead_id} | https://velocity.desineuron.in/api/sentinel/qd-score/{lead_id} | Get current QD score for a lead |
|
||||
|
||||
### Catalyst Marketing Orchestration Endpoints
|
||||
|
||||
| Method | Path | Absolute URL | Description |
|
||||
|--------|------|--------------|-------------|
|
||||
| POST | /api/catalyst/campaigns/create | https://velocity.desineuron.in/api/catalyst/campaigns/create | Bulk create Meta ad campaigns |
|
||||
| POST | /api/catalyst/creative/sync | https://velocity.desineuron.in/api/catalyst/creative/sync | Upload ComfyUI assets to Meta |
|
||||
| GET | /api/catalyst/insights/realtime | https://velocity.desineuron.in/api/catalyst/insights/realtime | Poll Meta Ads Insights API |
|
||||
| POST | /api/catalyst/audiences/lookalike | https://velocity.desineuron.in/api/catalyst/audiences/lookalike | Push CRM leads to Meta Custom Audience |
|
||||
| POST | /api/catalyst/auth/meta | https://velocity.desineuron.in/api/catalyst/auth/meta | OAuth token acquisition for Meta |
|
||||
|
||||
### Vault Trackable Links Endpoints
|
||||
|
||||
| Method | Path | Absolute URL | Description |
|
||||
|--------|------|--------------|-------------|
|
||||
| POST | /api/vault/generate-link | https://velocity.desineuron.in/api/vault/generate-link | Generate trackable URL for shared asset |
|
||||
| GET | /vault/{tracking_hash} | https://velocity.desineuron.in/vault/{tracking_hash} | Public access to trackable vault link (no auth required) |
|
||||
|
||||
### CCTV Surveillance Integration Endpoints
|
||||
|
||||
| Method | Path | Absolute URL | Description |
|
||||
|--------|------|--------------|-------------|
|
||||
| POST | /api/cctv/event | https://velocity.desineuron.in/api/cctv/event | Ingest CCTV frame event from RTSP/ONVIF bridge |
|
||||
| POST | /api/cctv/finalize-auto-mode | https://velocity.desineuron.in/api/cctv/finalize-auto-mode | Match or create lead after auto-mode session |
|
||||
|
||||
### Video Scene Mapping Endpoints
|
||||
|
||||
| Method | Path | Absolute URL | Description |
|
||||
|--------|------|--------------|-------------|
|
||||
| POST | /api/scenes/upload | https://velocity.desineuron.in/api/scenes/upload | Upload CSV scene map for marketing video |
|
||||
| GET | /api/scenes/{video_asset_id} | https://velocity.desineuron.in/api/scenes/{video_asset_id} | Get scene map for specific video asset |
|
||||
|
||||
### Marketing Videos Endpoints
|
||||
|
||||
| Method | Path | Absolute URL | Description |
|
||||
|--------|------|--------------|-------------|
|
||||
| GET | /api/videos/marketing | https://velocity.desineuron.in/api/videos/marketing | List marketing videos available for Sentinel sessions |
|
||||
|
||||
### Mobile Edge Communication Endpoints
|
||||
|
||||
| Method | Path | Absolute URL | Description |
|
||||
|--------|------|--------------|-------------|
|
||||
| GET | /api/mobile-edge/events | https://velocity.desineuron.in/api/mobile-edge/events | List communication events for a lead |
|
||||
| POST | /api/mobile-edge/events | https://velocity.desineuron.in/api/mobile-edge/events | Log new communication event |
|
||||
| GET | /api/mobile-edge/memory | https://velocity.desineuron.in/api/mobile-edge/memory | List memory facts for a lead |
|
||||
| POST | /api/mobile-edge/imports | https://velocity.desineuron.in/api/mobile-edge/imports | Operator-assisted import of recording/note |
|
||||
| POST | /api/mobile-edge/notes | https://velocity.desineuron.in/api/mobile-edge/notes | Quick note attachment to lead |
|
||||
| GET | /api/mobile-edge/calendar | https://velocity.desineuron.in/api/mobile-edge/calendar | Calendar events for authenticated user |
|
||||
| POST | /api/mobile-edge/calendar | https://velocity.desineuron.in/api/mobile-edge/calendar | Create calendar event |
|
||||
| PATCH | /api/mobile-edge/calendar/{calendar_event_id} | https://velocity.desineuron.in/api/mobile-edge/calendar/{calendar_event_id} | Update calendar event |
|
||||
| DELETE | /api/mobile-edge/calendar/{calendar_event_id} | https://velocity.desineuron.in/api/mobile-edge/calendar/{calendar_event_id} | Cancel calendar event |
|
||||
| GET | /api/mobile-edge/transcripts/{event_id} | https://velocity.desineuron.in/api/mobile-edge/transcripts/{event_id} | Transcript segments for event |
|
||||
| GET | /api/mobile-edge/insights/{lead_id} | https://velocity.desineuron.in/api/mobile-edge/insights/{lead_id} | Insight recommendations for lead |
|
||||
| POST | /api/mobile-edge/insights/{recommendation_id}/act | https://velocity.desineuron.in/api/mobile-edge/insights/{recommendation_id}/act | Act on or dismiss insight |
|
||||
| GET | /api/mobile-edge/alerts | https://velocity.desineuron.in/api/mobile-edge/alerts | Active alerts for authenticated user |
|
||||
| POST | /api/mobile-edge/session | https://velocity.desineuron.in/api/mobile-edge/session | Register surface session heartbeat |
|
||||
|
||||
### Inventory Management Endpoints
|
||||
|
||||
| Method | Path | Absolute URL | Description |
|
||||
|--------|------|--------------|-------------|
|
||||
| POST | /api/inventory/import-batches | https://velocity.desineuron.in/api/inventory/import-batches | Create inventory import batch |
|
||||
| GET | /api/inventory/import-batches | https://velocity.desineuron.in/api/inventory/import-batches | List import batches |
|
||||
| GET | /api/inventory/import-batches/{batch_id} | https://velocity.desineuron.in/api/inventory/import-batches/{batch_id} | Get batch status |
|
||||
| POST | /api/inventory/properties | https://velocity.desineuron.in/api/inventory/properties | Create single property |
|
||||
| GET | /api/inventory/properties | https://velocity.desineuron.in/api/inventory/properties | List inventory properties |
|
||||
| GET | /api/inventory/properties/{property_id} | https://velocity.desineuron.in/api/inventory/properties/{property_id} | Get property details |
|
||||
| PATCH | /api/inventory/properties/{property_id} | https://velocity.desineuron.in/api/inventory/properties/{property_id} | Update property |
|
||||
| DELETE | /api/inventory/properties/{property_id} | https://velocity.desineuron.in/api/inventory/properties/{property_id} | Archive property |
|
||||
| POST | /api/inventory/properties/{property_id}/media | https://velocity.desineuron.in/api/inventory/properties/{property_id}/media | Attach media to property |
|
||||
| GET | /api/inventory/properties/{property_id}/media | https://velocity.desineuron.in/api/inventory/properties/{property_id}/media | List media for property |
|
||||
| DELETE | /api/inventory/media/{media_asset_id} | https://velocity.desineuron.in/api/inventory/media/{media_asset_id} | Remove media asset |
|
||||
|
||||
### Admin Surface Control Endpoints
|
||||
|
||||
| Method | Path | Absolute URL | Description |
|
||||
|--------|------|--------------|-------------|
|
||||
| GET | /api/admin-surface/health | https://velocity.desineuron.in/api/admin-surface/health | System health overview |
|
||||
| GET | /api/admin-surface/queues | https://velocity.desineuron.in/api/admin-surface/queues | Queue depth snapshot |
|
||||
| GET | /api/admin-surface/installs | https://velocity.desineuron.in/api/admin-surface/installs | Surface session/install overview |
|
||||
| POST | /api/admin-surface/actions | https://velocity.desineuron.in/api/admin-surface/actions | Submit admin action |
|
||||
| GET | /api/admin-surface/actions | https://velocity.desineuron.in/api/admin-surface/actions | List admin action history |
|
||||
| GET | /api/admin-surface/actions/{action_event_id} | https://velocity.desineuron.in/api/admin-surface/actions/{action_event_id} | Get specific admin action |
|
||||
| GET | /api/admin-surface/logs | https://velocity.desineuron.in/api/admin-surface/logs | Recent Oracle audit event log |
|
||||
| GET | /api/admin-surface/templates | https://velocity.desineuron.in/api/admin-surface/templates | Template catalog admin view |
|
||||
| POST | /api/admin-surface/templates/{template_id}/publish | https://velocity.desineuron.in/api/admin-surface/templates/{template_id}/publish | Publish template |
|
||||
| POST | /api/admin-surface/templates/{template_id}/archive | https://velocity.desineuron.in/api/admin-surface/templates/{template_id}/archive | Archive template |
|
||||
| GET | /api/admin-surface/template-chapters | https://velocity.desineuron.in/api/admin-surface/template-chapters | List template chapters (admin) |
|
||||
| GET | /api/admin-surface/synthetic-jobs | https://velocity.desineuron.in/api/admin-surface/synthetic-jobs | List synthetic generation jobs |
|
||||
| POST | /api/admin-surface/synthetic-jobs/{job_id}/cancel | https://velocity.desineuron.in/api/admin-surface/synthetic-jobs/{job_id}/cancel | Cancel synthetic job |
|
||||
|
||||
### CRM Canonical Data Endpoints
|
||||
|
||||
| Method | Path | Absolute URL | Description |
|
||||
|--------|------|--------------|-------------|
|
||||
| POST | /api/crm/imports | https://velocity.desineuron.in/api/crm/imports | Upload CSV batch for import |
|
||||
| GET | /api/crm/imports | https://velocity.desineuron.in/api/crm/imports | List import batches |
|
||||
| GET | /api/crm/imports/{batch_id} | https://velocity.desineuron.in/api/crm/imports/{batch_id} | Get batch detail and proposals |
|
||||
| PUT | /api/crm/imports/{batch_id}/review-proposal | https://velocity.desineuron.in/api/crm/imports/{batch_id}/review-proposal | Review import proposal |
|
||||
| POST | /api/crm/imports/{batch_id}/commit | https://velocity.desineuron.in/api/crm/imports/{batch_id}/commit | Commit approved proposals |
|
||||
| GET | /api/crm/contacts | https://velocity.desineuron.in/api/crm/contacts | Canonical contact list with QD summary |
|
||||
| POST | /api/crm/contacts | https://velocity.desineuron.in/api/crm/contacts | Create new contact |
|
||||
| GET | /api/crm/contacts/{person_id} | https://velocity.desineuron.in/api/crm/contacts/{person_id} | Canonical contact detail |
|
||||
| GET | /api/crm/client-360/{person_id} | https://velocity.desineuron.in/api/crm/client-360/{person_id} | Client 360 aggregated snapshot |
|
||||
| GET | /api/crm/opportunities | https://velocity.desineuron.in/api/crm/opportunities | Opportunity pipeline list |
|
||||
| GET | /api/crm/tasks | https://velocity.desineuron.in/api/crm/tasks | Reminder/task list |
|
||||
| POST | /api/crm/tasks | https://velocity.desineuron.in/api/crm/tasks | Create new task |
|
||||
| GET | /api/crm/kanban | https://velocity.desineuron.in/api/crm/kanban | Kanban board (canonical leads) |
|
||||
| GET | /api/crm/qd/{person_id} | https://velocity.desineuron.in/api/crm/qd/{person_id} | QD score history for person |
|
||||
| GET | /api/crm/client-data | https://velocity.desineuron.in/api/crm/client-data | List client data records |
|
||||
| GET | /api/crm/client-data/{person_id} | https://velocity.desineuron.in/api/crm/client-data/{person_id} | Get client data for person |
|
||||
| PATCH | /api/crm/client-data/{person_id} | https://velocity.desineuron.in/api/crm/client-data/{person_id} | Update client data |
|
||||
| GET | /api/crm/client-data/{person_id}/timeline | https://velocity.desineuron.in/api/crm/client-data/{person_id}/timeline | Client data timeline |
|
||||
| POST | /api/crm/client-data/{person_id}/tasks | https://velocity.desineuron.in/api/crm/client-data/{person_id}/tasks | Create task for client |
|
||||
|
||||
### Runtime LLM Endpoints
|
||||
Method Path Absolute URL Description
|
||||
GET /api/runtime/llm/providers https://velocity.desineuron.in/api/runtime/llm/providers List configured LLM providers and models
|
||||
POST /api/runtime/llm/chat https://velocity.desineuron.in/api/runtime/llm/chat Execute single LLM chat completion
|
||||
POST /api/runtime/llm/batch https://velocity.desineuron.in/api/runtime/llm/batch Submit persisted LLM batch job
|
||||
GET /api/runtime/llm/jobs/{job_id} https://velocity.desineuron.in/api/runtime/llm/jobs/{job_id} Get batch job status
|
||||
GET /api/runtime/llm/jobs/{job_id}/results https://velocity.desineuron.in/api/runtime/llm/jobs/{job_id}/results Get batch job results
|
||||
|
||||
### Infrastructure Notes
|
||||
|
||||
- **Caddy Reverse Proxy**: All endpoints are served through Caddy on port 443, proxying to FastAPI on localhost:8443 with TLS termination
|
||||
- **Authentication**: JWT-based auth required for most endpoints (except public vault links)
|
||||
- **WebSockets**: Real-time features use WebSocket connections for live updates
|
||||
- **Role-Based Access**: Endpoints enforce role permissions (SENIOR_BROKER, ADMIN, etc.)
|
||||
- **Tenant Isolation**: Multi-tenant architecture with tenant_id scoping
|
||||
- **Audit Logging**: All mutations create immutable audit records
|
||||
- **Health Checks**: System provides comprehensive health and queue monitoring endpoints
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,98 +0,0 @@
|
||||
I audited iOS/velocity-ipad/velocity for mock/demo/static/fallback data paths. Good news: I did not find hard-coded fake CRM people, fake properties, fake opportunities, fake calendar events, or fake comms threads being rendered as normal production data. The app is mostly live-backed.
|
||||
|
||||
That said, there are still several hard-coded or locally synthesized data paths you should decide whether to move fully behind backend/database contracts.
|
||||
|
||||
Production-Risk Mock Or Synthetic Data
|
||||
|
||||
SimulatorSunOverlayView.swift (line 8)
|
||||
|
||||
Uses fake simulator-only location: San Francisco 37.7749, -122.4194.
|
||||
Uses mock heading: 0.
|
||||
Wrapped in #if targetEnvironment(simulator), so it should not run on physical iPad, but it is still mock data in app code.
|
||||
InventoryView.swift (line 941)
|
||||
|
||||
If Building.usdz / Building.scn fails to load, Dollhouse falls back to a procedural synthetic building.
|
||||
The fallback creates hard-coded rooms, walls, colors, and dimensions at InventoryView.swift (line 975).
|
||||
For production, this should probably fail closed or fetch a real model reference from backend inventory metadata.
|
||||
Building.usda (line 1) and Building.usdz
|
||||
|
||||
Bundled local 3D building asset with hard-coded cube geometry: Podium, TowerA, TowerB, AmenityDeck, Courtyard.
|
||||
This is not backend/database-backed property inventory. It is a static app asset.
|
||||
|
||||
Frontend-Derived Fallback Data
|
||||
VelocityAPIClient.swift (line 1538)
|
||||
|
||||
VelocityClient360DTO.minimal(from:) fabricates a Client 360 snapshot from a contact when richer Client 360 data is unavailable.
|
||||
It creates local QD overview fields, empty opportunities/interactions/tasks/interests, and note "Derived from the CRM client-data endpoint."
|
||||
VelocityAPIClient.swift (line 2725)
|
||||
|
||||
If Client 360 decode fails with invalidResponse, the app fetches contacts and builds the minimal local snapshot instead of failing.
|
||||
VelocityAPIClient.swift (line 1905)
|
||||
|
||||
If backend gives QD scores but no recommended actions, the app generates: Review {scoreType} score at {displayScore}.
|
||||
This is local advisory text, not backend intelligence.
|
||||
AppStore.swift (line 873)
|
||||
|
||||
Dashboard metrics fall back to locally computed canonicalDashboardMetrics(...) if /api/dashboard/metrics fails.
|
||||
The app computes lead count, whale count, property count, today calendar count, pending insights, etc. locally at AppStore.swift (line 902).
|
||||
AppStore.swift (line 857)
|
||||
|
||||
If contact fetch returns 404, app reuses cached contacts instead of treating backend as source-of-truth unavailable.
|
||||
AppStore.swift (line 869)
|
||||
|
||||
Several failed backend calls silently fall back to empty arrays: kanban, opportunities, properties.
|
||||
This can make missing backend data look like “zero production data.”
|
||||
Local Offline Data That Becomes Temporary UI Truth
|
||||
10. AppStore.swift (line 607)
|
||||
|
||||
Offline calendar create generates local IDs like local-{UUID} and local createdAt.
|
||||
The event is merged into UI before backend confirmation.
|
||||
AppStore.swift (line 436)
|
||||
If task mutation happens offline and the task cannot be resolved, app fabricates a local task title: "Queued CRM task" and default priority "normal".
|
||||
OfflineReplayStore.swift (line 16)
|
||||
App stores offline replay mutations in local Core Data OfflineReplay.sqlite.
|
||||
This is valid offline architecture, but if you require strict backend-only truth, this should be treated as a write queue only and clearly marked as “pending sync.”
|
||||
Hard-Coded Business Vocabularies
|
||||
13. ClientsView.swift (line 35)
|
||||
|
||||
Hard-coded lead statuses: new, contacted, qualified, site_visit_scheduled, etc.
|
||||
ClientsView.swift (line 48)
|
||||
Hard-coded urgency values: low, medium, high, critical.
|
||||
ClientsView.swift (line 49)
|
||||
Hard-coded buyer types: end_user, hni_end_user, nri_investor, family_office, etc.
|
||||
ClientsView.swift (line 59)
|
||||
Hard-coded task priorities: low, normal, high, urgent.
|
||||
OracleView.swift (line 1001)
|
||||
Hard-coded canonical lead stages.
|
||||
OracleView.swift (line 1016)
|
||||
Hard-coded opportunity stages: prospect, qualified, proposal, site_visit, etc.
|
||||
ImportsView.swift (line 26)
|
||||
Hard-coded duplicate policies: create_new, update_existing, skip_duplicate.
|
||||
InventoryView.swift (line 501)
|
||||
Hard-coded Dream Weaver room types: bedroom, living room, bathroom, kitchen, etc.
|
||||
These are sent to the backend as room_type.
|
||||
Hard-Coded Defaults Affecting Created Backend Data
|
||||
21. VelocityAPIClient.swift (line 2579)
|
||||
|
||||
Communications task creation defaults priority to "normal".
|
||||
VelocityAPIClient.swift (line 2969)
|
||||
CSV import upload defaults source_system to "ipad_csv_upload".
|
||||
CalendarView.swift (line 1118)
|
||||
Calendar create adds local metadata: created_from = ipad_calendar, surface = velocity_ipad.
|
||||
Config And Environment Defaults
|
||||
24. SessionConfiguration.swift (line 16)
|
||||
|
||||
Default endpoint prompt/value: https://velocity.desineuron.in/api.
|
||||
SessionConfiguration.swift (line 17)
|
||||
Default Dream Weaver endpoint: https://dreamweaver.desineuron.in.
|
||||
SessionConfigurationPanel.swift (line 74)
|
||||
Placeholder operator email: operator@desineuron.in.
|
||||
Verdict
|
||||
The highest-priority removals for true production purity are:
|
||||
|
||||
Remove Dollhouse procedural fallback or move model selection to backend inventory metadata.
|
||||
Replace hard-coded CRM vocabularies/stages with /api/crm/vocabularies or similar.
|
||||
Remove Client 360 minimal fallback so malformed/missing backend data fails visibly.
|
||||
Stop silently converting failed backend reads into empty arrays.
|
||||
Keep offline replay only as a pending-write queue, not as “truth” without clear pending-sync labeling.
|
||||
Replace simulator fake Sunseeker data with unavailable-state only, if simulator mock paths must be totally absent.
|
||||
File diff suppressed because it is too large
Load Diff
63
.github/workflows/production-readiness.yml
vendored
63
.github/workflows/production-readiness.yml
vendored
@@ -1,63 +0,0 @@
|
||||
name: Production Readiness
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
|
||||
jobs:
|
||||
backend-contracts:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- name: Install backend dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r backend/requirements.txt
|
||||
pip install pytest
|
||||
- name: Run backend contract tests
|
||||
run: |
|
||||
PYTHONPATH="$PWD" python -m pytest \
|
||||
backend/tests/test_auth_tenant_contract.py \
|
||||
backend/tests/test_canonical_crm_auth.py \
|
||||
backend/tests/test_canonical_crm_tenant_scoping.py \
|
||||
backend/tests/test_dream_weaver_gateway_auth.py \
|
||||
backend/tests/test_migrations_and_observability.py \
|
||||
backend/tests/test_surface_route_tenant_scoping.py
|
||||
|
||||
webos-typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: app
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
cache: npm
|
||||
cache-dependency-path: app/package-lock.json
|
||||
- name: Install WebOS dependencies
|
||||
run: npm ci
|
||||
- name: Typecheck WebOS
|
||||
run: npx tsc --noEmit
|
||||
|
||||
ipad-parse:
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Parse active iPad Swift sources
|
||||
run: |
|
||||
swiftc -frontend -parse \
|
||||
iOS/velocity-ipad/velocity/App/ContentView.swift \
|
||||
iOS/velocity-ipad/velocity/Features/Clients/ClientsView.swift \
|
||||
iOS/velocity-ipad/velocity/Features/Imports/ImportsView.swift \
|
||||
iOS/velocity-ipad/velocity/Core/Networking/VelocityAPIClient.swift \
|
||||
iOS/velocity-ipad/velocity/Core/State/AppStore.swift \
|
||||
iOS/velocity-ipad/velocityTests/VelocitySmokeTests.swift
|
||||
|
||||
26
app/dist/index.html
vendored
26
app/dist/index.html
vendored
@@ -1,13 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Velocity WebOS</title>
|
||||
<script type="module" crossorigin src="./assets/index-DqpQquIA.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-80rkGqUG.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Velocity WebOS</title>
|
||||
<script type="module" crossorigin src="./assets/index-BbE_azx6.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-CILgAuxv.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
</body>
|
||||
|
||||
124
app/node_modules/.tmp/tsconfig.app.tsbuildinfo
generated
vendored
124
app/node_modules/.tmp/tsconfig.app.tsbuildinfo
generated
vendored
@@ -1,123 +1 @@
|
||||
{
|
||||
"root": [
|
||||
"../../src/app.tsx",
|
||||
"../../src/main.tsx",
|
||||
"../../src/app/admin/page.tsx",
|
||||
"../../src/app/oracle/page.tsx",
|
||||
"../../src/components/layout/loginscreen.tsx",
|
||||
"../../src/components/layout/notificationcenter.tsx",
|
||||
"../../src/components/layout/sidebar.tsx",
|
||||
"../../src/components/modules/crm.tsx",
|
||||
"../../src/components/modules/catalyst.tsx",
|
||||
"../../src/components/modules/catalystdreamweavertab.tsx",
|
||||
"../../src/components/modules/catalystmarketingtab.tsx",
|
||||
"../../src/components/modules/dashboard.tsx",
|
||||
"../../src/components/modules/groundtruthpicker.tsx",
|
||||
"../../src/components/modules/inventory.tsx",
|
||||
"../../src/components/modules/oracle.tsx",
|
||||
"../../src/components/modules/sentinel.tsx",
|
||||
"../../src/components/modules/settings.tsx",
|
||||
"../../src/components/modules/sentinel/perceptionplayer.tsx",
|
||||
"../../src/components/modules/sentinel/sentinellivesession.tsx",
|
||||
"../../src/components/oracle/leadinspector.tsx",
|
||||
"../../src/components/oracle/pipelineview.tsx",
|
||||
"../../src/components/sentinel/journeyriver/inspectorpanel.tsx",
|
||||
"../../src/components/sentinel/journeyriver/riverpath.tsx",
|
||||
"../../src/components/sentinel/journeyriver/index.tsx",
|
||||
"../../src/components/ui/accordion.tsx",
|
||||
"../../src/components/ui/alert-dialog.tsx",
|
||||
"../../src/components/ui/alert.tsx",
|
||||
"../../src/components/ui/aspect-ratio.tsx",
|
||||
"../../src/components/ui/avatar.tsx",
|
||||
"../../src/components/ui/badge.tsx",
|
||||
"../../src/components/ui/breadcrumb.tsx",
|
||||
"../../src/components/ui/button-group.tsx",
|
||||
"../../src/components/ui/button.tsx",
|
||||
"../../src/components/ui/calendar.tsx",
|
||||
"../../src/components/ui/card.tsx",
|
||||
"../../src/components/ui/carousel.tsx",
|
||||
"../../src/components/ui/chart.tsx",
|
||||
"../../src/components/ui/checkbox.tsx",
|
||||
"../../src/components/ui/collapsible.tsx",
|
||||
"../../src/components/ui/command.tsx",
|
||||
"../../src/components/ui/context-menu.tsx",
|
||||
"../../src/components/ui/dialog.tsx",
|
||||
"../../src/components/ui/drawer.tsx",
|
||||
"../../src/components/ui/dropdown-menu.tsx",
|
||||
"../../src/components/ui/empty.tsx",
|
||||
"../../src/components/ui/field.tsx",
|
||||
"../../src/components/ui/form.tsx",
|
||||
"../../src/components/ui/hover-card.tsx",
|
||||
"../../src/components/ui/input-group.tsx",
|
||||
"../../src/components/ui/input-otp.tsx",
|
||||
"../../src/components/ui/input.tsx",
|
||||
"../../src/components/ui/item.tsx",
|
||||
"../../src/components/ui/kbd.tsx",
|
||||
"../../src/components/ui/label.tsx",
|
||||
"../../src/components/ui/menubar.tsx",
|
||||
"../../src/components/ui/navigation-menu.tsx",
|
||||
"../../src/components/ui/pagination.tsx",
|
||||
"../../src/components/ui/popover.tsx",
|
||||
"../../src/components/ui/progress.tsx",
|
||||
"../../src/components/ui/radio-group.tsx",
|
||||
"../../src/components/ui/resizable.tsx",
|
||||
"../../src/components/ui/scroll-area.tsx",
|
||||
"../../src/components/ui/select.tsx",
|
||||
"../../src/components/ui/separator.tsx",
|
||||
"../../src/components/ui/sheet.tsx",
|
||||
"../../src/components/ui/sidebar.tsx",
|
||||
"../../src/components/ui/skeleton.tsx",
|
||||
"../../src/components/ui/slider.tsx",
|
||||
"../../src/components/ui/sonner.tsx",
|
||||
"../../src/components/ui/spinner.tsx",
|
||||
"../../src/components/ui/switch.tsx",
|
||||
"../../src/components/ui/table.tsx",
|
||||
"../../src/components/ui/tabs.tsx",
|
||||
"../../src/components/ui/textarea.tsx",
|
||||
"../../src/components/ui/toggle-group.tsx",
|
||||
"../../src/components/ui/toggle.tsx",
|
||||
"../../src/components/ui/tooltip.tsx",
|
||||
"../../src/hooks/use-mobile.ts",
|
||||
"../../src/hooks/usecrmbootstrap.ts",
|
||||
"../../src/hooks/usemediapipefacelandmarker.ts",
|
||||
"../../src/hooks/usevelocitysocket.ts",
|
||||
"../../src/lib/api.ts",
|
||||
"../../src/lib/crmapi.ts",
|
||||
"../../src/lib/crmmappers.ts",
|
||||
"../../src/lib/dreamweaverapi.ts",
|
||||
"../../src/lib/platformmappers.ts",
|
||||
"../../src/lib/utils.ts",
|
||||
"../../src/lib/velocityplatformclient.ts",
|
||||
"../../src/lib/velocitysession.ts",
|
||||
"../../src/oracle/components/branchbar.tsx",
|
||||
"../../src/oracle/components/canvasviewport.tsx",
|
||||
"../../src/oracle/components/componentregistry.tsx",
|
||||
"../../src/oracle/components/promptrail.tsx",
|
||||
"../../src/oracle/components/rollbackconfirmmodal.tsx",
|
||||
"../../src/oracle/components/sharemodal.tsx",
|
||||
"../../src/oracle/components/renderers/activitystreamrenderer.tsx",
|
||||
"../../src/oracle/components/renderers/barchartrenderer.tsx",
|
||||
"../../src/oracle/components/renderers/errornoticerenderer.tsx",
|
||||
"../../src/oracle/components/renderers/geomaprenderer.tsx",
|
||||
"../../src/oracle/components/renderers/kpitilerenderer.tsx",
|
||||
"../../src/oracle/components/renderers/linechartrenderer.tsx",
|
||||
"../../src/oracle/components/renderers/pipelineboardrenderer.tsx",
|
||||
"../../src/oracle/components/renderers/rendererwrapper.tsx",
|
||||
"../../src/oracle/components/renderers/tablerenderer.tsx",
|
||||
"../../src/oracle/components/renderers/timelinerenderer.tsx",
|
||||
"../../src/oracle/components/review/mergereviewdrawer.tsx",
|
||||
"../../src/oracle/hooks/useoracleexecution.ts",
|
||||
"../../src/oracle/hooks/useoraclepage.ts",
|
||||
"../../src/oracle/lib/oracleapiclient.ts",
|
||||
"../../src/oracle/types/canvas.ts",
|
||||
"../../src/store/usecurrencystore.ts",
|
||||
"../../src/store/usemarketingstore.ts",
|
||||
"../../src/store/usestore.ts",
|
||||
"../../src/types/crm.ts",
|
||||
"../../src/types/crmtypes.ts",
|
||||
"../../src/types/index.ts",
|
||||
"../../src/utils/curvegenerator.ts",
|
||||
"../../src/utils/landmarkpacketencoder.ts"
|
||||
],
|
||||
"version": "5.9.3"
|
||||
}
|
||||
{"root":["../../src/app.tsx","../../src/main.tsx","../../src/app/admin/page.tsx","../../src/app/oracle/page.tsx","../../src/components/layout/loginscreen.tsx","../../src/components/layout/notificationcenter.tsx","../../src/components/layout/sidebar.tsx","../../src/components/modules/crm.tsx","../../src/components/modules/catalyst.tsx","../../src/components/modules/catalystmarketingtab.tsx","../../src/components/modules/dashboard.tsx","../../src/components/modules/groundtruthpicker.tsx","../../src/components/modules/inventory.tsx","../../src/components/modules/oracle.tsx","../../src/components/modules/sentinel.tsx","../../src/components/modules/settings.tsx","../../src/components/modules/sentinel/perceptionplayer.tsx","../../src/components/modules/sentinel/sentinellivesession.tsx","../../src/components/oracle/leadinspector.tsx","../../src/components/oracle/pipelineview.tsx","../../src/components/sentinel/journeyriver/inspectorpanel.tsx","../../src/components/sentinel/journeyriver/riverpath.tsx","../../src/components/sentinel/journeyriver/index.tsx","../../src/components/ui/accordion.tsx","../../src/components/ui/alert-dialog.tsx","../../src/components/ui/alert.tsx","../../src/components/ui/aspect-ratio.tsx","../../src/components/ui/avatar.tsx","../../src/components/ui/badge.tsx","../../src/components/ui/breadcrumb.tsx","../../src/components/ui/button-group.tsx","../../src/components/ui/button.tsx","../../src/components/ui/calendar.tsx","../../src/components/ui/card.tsx","../../src/components/ui/carousel.tsx","../../src/components/ui/chart.tsx","../../src/components/ui/checkbox.tsx","../../src/components/ui/collapsible.tsx","../../src/components/ui/command.tsx","../../src/components/ui/context-menu.tsx","../../src/components/ui/dialog.tsx","../../src/components/ui/drawer.tsx","../../src/components/ui/dropdown-menu.tsx","../../src/components/ui/empty.tsx","../../src/components/ui/field.tsx","../../src/components/ui/form.tsx","../../src/components/ui/hover-card.tsx","../../src/components/ui/input-group.tsx","../../src/components/ui/input-otp.tsx","../../src/components/ui/input.tsx","../../src/components/ui/item.tsx","../../src/components/ui/kbd.tsx","../../src/components/ui/label.tsx","../../src/components/ui/menubar.tsx","../../src/components/ui/navigation-menu.tsx","../../src/components/ui/pagination.tsx","../../src/components/ui/popover.tsx","../../src/components/ui/progress.tsx","../../src/components/ui/radio-group.tsx","../../src/components/ui/resizable.tsx","../../src/components/ui/scroll-area.tsx","../../src/components/ui/select.tsx","../../src/components/ui/separator.tsx","../../src/components/ui/sheet.tsx","../../src/components/ui/sidebar.tsx","../../src/components/ui/skeleton.tsx","../../src/components/ui/slider.tsx","../../src/components/ui/sonner.tsx","../../src/components/ui/spinner.tsx","../../src/components/ui/switch.tsx","../../src/components/ui/table.tsx","../../src/components/ui/tabs.tsx","../../src/components/ui/textarea.tsx","../../src/components/ui/toggle-group.tsx","../../src/components/ui/toggle.tsx","../../src/components/ui/tooltip.tsx","../../src/hooks/use-mobile.ts","../../src/hooks/usecrmbootstrap.ts","../../src/hooks/usemediapipefacelandmarker.ts","../../src/hooks/usevelocitysocket.ts","../../src/lib/api.ts","../../src/lib/crmapi.ts","../../src/lib/crmmappers.ts","../../src/lib/platformmappers.ts","../../src/lib/utils.ts","../../src/lib/velocityplatformclient.ts","../../src/oracle/components/branchbar.tsx","../../src/oracle/components/canvasviewport.tsx","../../src/oracle/components/componentregistry.tsx","../../src/oracle/components/promptrail.tsx","../../src/oracle/components/rollbackconfirmmodal.tsx","../../src/oracle/components/sharemodal.tsx","../../src/oracle/components/renderers/activitystreamrenderer.tsx","../../src/oracle/components/renderers/barchartrenderer.tsx","../../src/oracle/components/renderers/errornoticerenderer.tsx","../../src/oracle/components/renderers/geomaprenderer.tsx","../../src/oracle/components/renderers/kpitilerenderer.tsx","../../src/oracle/components/renderers/linechartrenderer.tsx","../../src/oracle/components/renderers/pipelineboardrenderer.tsx","../../src/oracle/components/renderers/rendererwrapper.tsx","../../src/oracle/components/renderers/tablerenderer.tsx","../../src/oracle/components/renderers/textcanvasrenderer.tsx","../../src/oracle/components/renderers/timelinerenderer.tsx","../../src/oracle/components/review/mergereviewdrawer.tsx","../../src/oracle/hooks/useoracleexecution.ts","../../src/oracle/hooks/useoraclepage.ts","../../src/oracle/lib/oracleapiclient.ts","../../src/oracle/types/canvas.ts","../../src/store/usecurrencystore.ts","../../src/store/usemarketingstore.ts","../../src/store/usestore.ts","../../src/types/crm.ts","../../src/types/crmtypes.ts","../../src/types/index.ts","../../src/utils/curvegenerator.ts","../../src/utils/landmarkpacketencoder.ts"],"version":"5.9.3"}
|
||||
14
app/node_modules/.vite/deps/@radix-ui_react-avatar.js
generated
vendored
14
app/node_modules/.vite/deps/@radix-ui_react-avatar.js
generated
vendored
@@ -1,18 +1,18 @@
|
||||
"use client";
|
||||
import {
|
||||
require_shim
|
||||
} from "./chunk-TXHHHGR3.js";
|
||||
import {
|
||||
useCallbackRef,
|
||||
useLayoutEffect2
|
||||
} from "./chunk-J4JAFMOP.js";
|
||||
} from "./chunk-GRXJTWBV.js";
|
||||
import {
|
||||
require_react_dom
|
||||
} from "./chunk-YF4B4G2L.js";
|
||||
} from "./chunk-YLZ34CCM.js";
|
||||
import {
|
||||
require_shim
|
||||
} from "./chunk-642Z5WD3.js";
|
||||
import {
|
||||
createSlot
|
||||
} from "./chunk-YWBEB5PG.js";
|
||||
import "./chunk-2VUH7NEY.js";
|
||||
} from "./chunk-5HUACAZ7.js";
|
||||
import "./chunk-HPBHRBIF.js";
|
||||
import {
|
||||
require_jsx_runtime
|
||||
} from "./chunk-USXRE7Q2.js";
|
||||
|
||||
7
app/node_modules/.vite/deps/@radix-ui_react-avatar.js.map
generated
vendored
Normal file
7
app/node_modules/.vite/deps/@radix-ui_react-avatar.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
910
app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js
generated
vendored
910
app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js
generated
vendored
File diff suppressed because it is too large
Load Diff
7
app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js.map
generated
vendored
Normal file
7
app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
17
app/node_modules/.vite/deps/@radix-ui_react-slot.js
generated
vendored
Normal file
17
app/node_modules/.vite/deps/@radix-ui_react-slot.js
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
Slot,
|
||||
Slottable,
|
||||
createSlot,
|
||||
createSlottable
|
||||
} from "./chunk-5HUACAZ7.js";
|
||||
import "./chunk-HPBHRBIF.js";
|
||||
import "./chunk-USXRE7Q2.js";
|
||||
import "./chunk-ZNKPWGXJ.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export {
|
||||
Slot as Root,
|
||||
Slot,
|
||||
Slottable,
|
||||
createSlot,
|
||||
createSlottable
|
||||
};
|
||||
7
app/node_modules/.vite/deps/@radix-ui_react-slot.js.map
generated
vendored
Normal file
7
app/node_modules/.vite/deps/@radix-ui_react-slot.js.map
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
3702
app/node_modules/.vite/deps/@react-three_drei.js
generated
vendored
3702
app/node_modules/.vite/deps/@react-three_drei.js
generated
vendored
File diff suppressed because it is too large
Load Diff
7
app/node_modules/.vite/deps/@react-three_drei.js.map
generated
vendored
Normal file
7
app/node_modules/.vite/deps/@react-three_drei.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
69
app/node_modules/.vite/deps/@react-three_fiber.js
generated
vendored
Normal file
69
app/node_modules/.vite/deps/@react-three_fiber.js
generated
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
Canvas,
|
||||
_roots,
|
||||
act,
|
||||
addAfterEffect,
|
||||
addEffect,
|
||||
addTail,
|
||||
advance,
|
||||
applyProps,
|
||||
buildGraph,
|
||||
context,
|
||||
createEvents,
|
||||
createPointerEvents,
|
||||
createPortal,
|
||||
createRoot,
|
||||
dispose,
|
||||
extend,
|
||||
flushGlobalEffects,
|
||||
flushSync,
|
||||
getRootState,
|
||||
invalidate,
|
||||
reconciler,
|
||||
threeTypes,
|
||||
unmountComponentAtNode,
|
||||
useFrame,
|
||||
useGraph,
|
||||
useInstanceHandle,
|
||||
useLoader,
|
||||
useStore,
|
||||
useThree
|
||||
} from "./chunk-CSHY5MMV.js";
|
||||
import "./chunk-LTNRPUSL.js";
|
||||
import "./chunk-INS7YHTD.js";
|
||||
import "./chunk-QURGMCZB.js";
|
||||
import "./chunk-642Z5WD3.js";
|
||||
import "./chunk-USXRE7Q2.js";
|
||||
import "./chunk-ZNKPWGXJ.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export {
|
||||
Canvas,
|
||||
threeTypes as ReactThreeFiber,
|
||||
_roots,
|
||||
act,
|
||||
addAfterEffect,
|
||||
addEffect,
|
||||
addTail,
|
||||
advance,
|
||||
applyProps,
|
||||
buildGraph,
|
||||
context,
|
||||
createEvents,
|
||||
createPortal,
|
||||
createRoot,
|
||||
dispose,
|
||||
createPointerEvents as events,
|
||||
extend,
|
||||
flushGlobalEffects,
|
||||
flushSync,
|
||||
getRootState,
|
||||
invalidate,
|
||||
reconciler,
|
||||
unmountComponentAtNode,
|
||||
useFrame,
|
||||
useGraph,
|
||||
useInstanceHandle,
|
||||
useLoader,
|
||||
useStore,
|
||||
useThree
|
||||
};
|
||||
7
app/node_modules/.vite/deps/@react-three_fiber.js.map
generated
vendored
Normal file
7
app/node_modules/.vite/deps/@react-three_fiber.js.map
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
385
app/node_modules/.vite/deps/_metadata.json
generated
vendored
385
app/node_modules/.vite/deps/_metadata.json
generated
vendored
@@ -1,196 +1,193 @@
|
||||
{
|
||||
"hash": "d63ca5ca",
|
||||
"configHash": "1dd3b956",
|
||||
"lockfileHash": "db47663b",
|
||||
"browserHash": "b8dcfecc",
|
||||
"optimized": {
|
||||
"react": {
|
||||
"src": "../../react/index.js",
|
||||
"file": "react.js",
|
||||
"fileHash": "0c4ff044",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-dom": {
|
||||
"src": "../../react-dom/index.js",
|
||||
"file": "react-dom.js",
|
||||
"fileHash": "d9b3477a",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react/jsx-dev-runtime": {
|
||||
"src": "../../react/jsx-dev-runtime.js",
|
||||
"file": "react_jsx-dev-runtime.js",
|
||||
"fileHash": "60584ffa",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react/jsx-runtime": {
|
||||
"src": "../../react/jsx-runtime.js",
|
||||
"file": "react_jsx-runtime.js",
|
||||
"fileHash": "0909256b",
|
||||
"needsInterop": true
|
||||
},
|
||||
"@radix-ui/react-avatar": {
|
||||
"src": "../../@radix-ui/react-avatar/dist/index.mjs",
|
||||
"file": "@radix-ui_react-avatar.js",
|
||||
"fileHash": "3fc2fdda",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@radix-ui/react-dropdown-menu": {
|
||||
"src": "../../@radix-ui/react-dropdown-menu/dist/index.mjs",
|
||||
"file": "@radix-ui_react-dropdown-menu.js",
|
||||
"fileHash": "eef7ef00",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@radix-ui/react-slot": {
|
||||
"src": "../../@radix-ui/react-slot/dist/index.mjs",
|
||||
"file": "@radix-ui_react-slot.js",
|
||||
"fileHash": "6745f8b7",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@react-three/drei": {
|
||||
"src": "../../@react-three/drei/index.js",
|
||||
"file": "@react-three_drei.js",
|
||||
"fileHash": "62f4e280",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@react-three/fiber": {
|
||||
"src": "../../@react-three/fiber/dist/react-three-fiber.esm.js",
|
||||
"file": "@react-three_fiber.js",
|
||||
"fileHash": "c4b868b0",
|
||||
"needsInterop": false
|
||||
},
|
||||
"class-variance-authority": {
|
||||
"src": "../../class-variance-authority/dist/index.mjs",
|
||||
"file": "class-variance-authority.js",
|
||||
"fileHash": "db4ee666",
|
||||
"needsInterop": false
|
||||
},
|
||||
"clsx": {
|
||||
"src": "../../clsx/dist/clsx.mjs",
|
||||
"file": "clsx.js",
|
||||
"fileHash": "0a67ca45",
|
||||
"needsInterop": false
|
||||
},
|
||||
"framer-motion": {
|
||||
"src": "../../framer-motion/dist/es/index.mjs",
|
||||
"file": "framer-motion.js",
|
||||
"fileHash": "9694d550",
|
||||
"needsInterop": false
|
||||
},
|
||||
"lucide-react": {
|
||||
"src": "../../lucide-react/dist/esm/lucide-react.js",
|
||||
"file": "lucide-react.js",
|
||||
"fileHash": "15d2dc31",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-dom/client": {
|
||||
"src": "../../react-dom/client.js",
|
||||
"file": "react-dom_client.js",
|
||||
"fileHash": "a8f9db58",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-router-dom": {
|
||||
"src": "../../react-router-dom/dist/index.mjs",
|
||||
"file": "react-router-dom.js",
|
||||
"fileHash": "3a519f93",
|
||||
"needsInterop": false
|
||||
},
|
||||
"recharts": {
|
||||
"src": "../../recharts/es6/index.js",
|
||||
"file": "recharts.js",
|
||||
"fileHash": "1cac0e9f",
|
||||
"needsInterop": false
|
||||
},
|
||||
"sonner": {
|
||||
"src": "../../sonner/dist/index.mjs",
|
||||
"file": "sonner.js",
|
||||
"fileHash": "1ad92981",
|
||||
"needsInterop": false
|
||||
},
|
||||
"tailwind-merge": {
|
||||
"src": "../../tailwind-merge/dist/bundle-mjs.mjs",
|
||||
"file": "tailwind-merge.js",
|
||||
"fileHash": "e2d07b44",
|
||||
"needsInterop": false
|
||||
},
|
||||
"three": {
|
||||
"src": "../../three/build/three.module.js",
|
||||
"file": "three.js",
|
||||
"fileHash": "09fb4882",
|
||||
"needsInterop": false
|
||||
},
|
||||
"zustand": {
|
||||
"src": "../../zustand/esm/index.mjs",
|
||||
"file": "zustand.js",
|
||||
"fileHash": "4607d0bf",
|
||||
"needsInterop": false
|
||||
},
|
||||
"zustand/middleware": {
|
||||
"src": "../../zustand/esm/middleware.mjs",
|
||||
"file": "zustand_middleware.js",
|
||||
"fileHash": "e4fd4342",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
"chunks": {
|
||||
"hls-Q6LDPZPT": {
|
||||
"file": "hls-Q6LDPZPT.js"
|
||||
},
|
||||
"chunk-EQCCHGRT": {
|
||||
"file": "chunk-EQCCHGRT.js"
|
||||
},
|
||||
"chunk-7GZ4CI6Q": {
|
||||
"file": "chunk-7GZ4CI6Q.js"
|
||||
},
|
||||
"chunk-5ESDTKMP": {
|
||||
"file": "chunk-5ESDTKMP.js"
|
||||
},
|
||||
"chunk-U7P2NEEE": {
|
||||
"file": "chunk-U7P2NEEE.js"
|
||||
},
|
||||
"chunk-GRXJTWBV": {
|
||||
"file": "chunk-GRXJTWBV.js"
|
||||
},
|
||||
"chunk-O4L7C4YS": {
|
||||
"file": "chunk-O4L7C4YS.js"
|
||||
},
|
||||
"chunk-L3Z576C2": {
|
||||
"file": "chunk-L3Z576C2.js"
|
||||
},
|
||||
"chunk-OAEA5FZL": {
|
||||
"file": "chunk-OAEA5FZL.js"
|
||||
},
|
||||
"chunk-6MXH2QM6": {
|
||||
"file": "chunk-6MXH2QM6.js"
|
||||
},
|
||||
"chunk-LTNRPUSL": {
|
||||
"file": "chunk-LTNRPUSL.js"
|
||||
},
|
||||
"chunk-TXHHHGR3": {
|
||||
"file": "chunk-TXHHHGR3.js"
|
||||
},
|
||||
"chunk-J4JAFMOP": {
|
||||
"file": "chunk-J4JAFMOP.js"
|
||||
},
|
||||
"chunk-YF4B4G2L": {
|
||||
"file": "chunk-YF4B4G2L.js"
|
||||
},
|
||||
"chunk-YWBEB5PG": {
|
||||
"file": "chunk-YWBEB5PG.js"
|
||||
},
|
||||
"chunk-2VUH7NEY": {
|
||||
"file": "chunk-2VUH7NEY.js"
|
||||
},
|
||||
"chunk-2YVA4HRZ": {
|
||||
"file": "chunk-2YVA4HRZ.js"
|
||||
},
|
||||
"chunk-WUR7D6NS": {
|
||||
"file": "chunk-WUR7D6NS.js"
|
||||
},
|
||||
"chunk-U7P2NEEE": {
|
||||
"file": "chunk-U7P2NEEE.js"
|
||||
},
|
||||
"chunk-G3PMV62Z": {
|
||||
"file": "chunk-G3PMV62Z.js"
|
||||
}
|
||||
}
|
||||
"hash": "9ed426b5",
|
||||
"configHash": "6a55a817",
|
||||
"lockfileHash": "cbf147e9",
|
||||
"browserHash": "a13f5201",
|
||||
"optimized": {
|
||||
"react": {
|
||||
"src": "../../react/index.js",
|
||||
"file": "react.js",
|
||||
"fileHash": "c178e920",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-dom": {
|
||||
"src": "../../react-dom/index.js",
|
||||
"file": "react-dom.js",
|
||||
"fileHash": "071b9320",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react/jsx-dev-runtime": {
|
||||
"src": "../../react/jsx-dev-runtime.js",
|
||||
"file": "react_jsx-dev-runtime.js",
|
||||
"fileHash": "72ddf78c",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react/jsx-runtime": {
|
||||
"src": "../../react/jsx-runtime.js",
|
||||
"file": "react_jsx-runtime.js",
|
||||
"fileHash": "14b8d385",
|
||||
"needsInterop": true
|
||||
},
|
||||
"@radix-ui/react-avatar": {
|
||||
"src": "../../@radix-ui/react-avatar/dist/index.mjs",
|
||||
"file": "@radix-ui_react-avatar.js",
|
||||
"fileHash": "590b7679",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@radix-ui/react-dropdown-menu": {
|
||||
"src": "../../@radix-ui/react-dropdown-menu/dist/index.mjs",
|
||||
"file": "@radix-ui_react-dropdown-menu.js",
|
||||
"fileHash": "087b631e",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@radix-ui/react-slot": {
|
||||
"src": "../../@radix-ui/react-slot/dist/index.mjs",
|
||||
"file": "@radix-ui_react-slot.js",
|
||||
"fileHash": "4e55412b",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@react-three/drei": {
|
||||
"src": "../../@react-three/drei/index.js",
|
||||
"file": "@react-three_drei.js",
|
||||
"fileHash": "ba800aca",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@react-three/fiber": {
|
||||
"src": "../../@react-three/fiber/dist/react-three-fiber.esm.js",
|
||||
"file": "@react-three_fiber.js",
|
||||
"fileHash": "12f23541",
|
||||
"needsInterop": false
|
||||
},
|
||||
"class-variance-authority": {
|
||||
"src": "../../class-variance-authority/dist/index.mjs",
|
||||
"file": "class-variance-authority.js",
|
||||
"fileHash": "0153428f",
|
||||
"needsInterop": false
|
||||
},
|
||||
"clsx": {
|
||||
"src": "../../clsx/dist/clsx.mjs",
|
||||
"file": "clsx.js",
|
||||
"fileHash": "99f068f1",
|
||||
"needsInterop": false
|
||||
},
|
||||
"framer-motion": {
|
||||
"src": "../../framer-motion/dist/es/index.mjs",
|
||||
"file": "framer-motion.js",
|
||||
"fileHash": "c1fc1ac2",
|
||||
"needsInterop": false
|
||||
},
|
||||
"lucide-react": {
|
||||
"src": "../../lucide-react/dist/esm/lucide-react.js",
|
||||
"file": "lucide-react.js",
|
||||
"fileHash": "4418176c",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-dom/client": {
|
||||
"src": "../../react-dom/client.js",
|
||||
"file": "react-dom_client.js",
|
||||
"fileHash": "8029f031",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-router-dom": {
|
||||
"src": "../../react-router-dom/dist/index.mjs",
|
||||
"file": "react-router-dom.js",
|
||||
"fileHash": "c673e5a0",
|
||||
"needsInterop": false
|
||||
},
|
||||
"recharts": {
|
||||
"src": "../../recharts/es6/index.js",
|
||||
"file": "recharts.js",
|
||||
"fileHash": "41235262",
|
||||
"needsInterop": false
|
||||
},
|
||||
"sonner": {
|
||||
"src": "../../sonner/dist/index.mjs",
|
||||
"file": "sonner.js",
|
||||
"fileHash": "c99e6320",
|
||||
"needsInterop": false
|
||||
},
|
||||
"tailwind-merge": {
|
||||
"src": "../../tailwind-merge/dist/bundle-mjs.mjs",
|
||||
"file": "tailwind-merge.js",
|
||||
"fileHash": "017ed736",
|
||||
"needsInterop": false
|
||||
},
|
||||
"three": {
|
||||
"src": "../../three/build/three.module.js",
|
||||
"file": "three.js",
|
||||
"fileHash": "8d6b5e64",
|
||||
"needsInterop": false
|
||||
},
|
||||
"zustand": {
|
||||
"src": "../../zustand/esm/index.mjs",
|
||||
"file": "zustand.js",
|
||||
"fileHash": "bcef7203",
|
||||
"needsInterop": false
|
||||
},
|
||||
"zustand/middleware": {
|
||||
"src": "../../zustand/esm/middleware.mjs",
|
||||
"file": "zustand_middleware.js",
|
||||
"fileHash": "1afe1817",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
"chunks": {
|
||||
"hls-Q6LDPZPT": {
|
||||
"file": "hls-Q6LDPZPT.js"
|
||||
},
|
||||
"chunk-QJTQF54Q": {
|
||||
"file": "chunk-QJTQF54Q.js"
|
||||
},
|
||||
"chunk-XGWIEMTH": {
|
||||
"file": "chunk-XGWIEMTH.js"
|
||||
},
|
||||
"chunk-OAEA5FZL": {
|
||||
"file": "chunk-OAEA5FZL.js"
|
||||
},
|
||||
"chunk-2NWYL6R2": {
|
||||
"file": "chunk-2NWYL6R2.js"
|
||||
},
|
||||
"chunk-H4GSM2WL": {
|
||||
"file": "chunk-H4GSM2WL.js"
|
||||
},
|
||||
"chunk-U7P2NEEE": {
|
||||
"file": "chunk-U7P2NEEE.js"
|
||||
},
|
||||
"chunk-GRXJTWBV": {
|
||||
"file": "chunk-GRXJTWBV.js"
|
||||
},
|
||||
"chunk-YLZ34CCM": {
|
||||
"file": "chunk-YLZ34CCM.js"
|
||||
},
|
||||
"chunk-CSHY5MMV": {
|
||||
"file": "chunk-CSHY5MMV.js"
|
||||
},
|
||||
"chunk-LTNRPUSL": {
|
||||
"file": "chunk-LTNRPUSL.js"
|
||||
},
|
||||
"chunk-INS7YHTD": {
|
||||
"file": "chunk-INS7YHTD.js"
|
||||
},
|
||||
"chunk-QURGMCZB": {
|
||||
"file": "chunk-QURGMCZB.js"
|
||||
},
|
||||
"chunk-642Z5WD3": {
|
||||
"file": "chunk-642Z5WD3.js"
|
||||
},
|
||||
"chunk-5HUACAZ7": {
|
||||
"file": "chunk-5HUACAZ7.js"
|
||||
},
|
||||
"chunk-HPBHRBIF": {
|
||||
"file": "chunk-HPBHRBIF.js"
|
||||
},
|
||||
"chunk-USXRE7Q2": {
|
||||
"file": "chunk-USXRE7Q2.js"
|
||||
},
|
||||
"chunk-ZNKPWGXJ": {
|
||||
"file": "chunk-ZNKPWGXJ.js"
|
||||
},
|
||||
"chunk-G3PMV62Z": {
|
||||
"file": "chunk-G3PMV62Z.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
35
app/node_modules/.vite/deps/chunk-G3PMV62Z.js
generated
vendored
Normal file
35
app/node_modules/.vite/deps/chunk-G3PMV62Z.js
generated
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __commonJS = (cb, mod) => function __require() {
|
||||
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
||||
};
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
|
||||
export {
|
||||
__commonJS,
|
||||
__export,
|
||||
__toESM
|
||||
};
|
||||
7
app/node_modules/.vite/deps/chunk-G3PMV62Z.js.map
generated
vendored
Normal file
7
app/node_modules/.vite/deps/chunk-G3PMV62Z.js.map
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
32999
app/node_modules/.vite/deps/chunk-OAEA5FZL.js
generated
vendored
Normal file
32999
app/node_modules/.vite/deps/chunk-OAEA5FZL.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
app/node_modules/.vite/deps/chunk-OAEA5FZL.js.map
generated
vendored
Normal file
7
app/node_modules/.vite/deps/chunk-OAEA5FZL.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
21
app/node_modules/.vite/deps/chunk-U7P2NEEE.js
generated
vendored
Normal file
21
app/node_modules/.vite/deps/chunk-U7P2NEEE.js
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
// node_modules/clsx/dist/clsx.mjs
|
||||
function r(e) {
|
||||
var t, f, n = "";
|
||||
if ("string" == typeof e || "number" == typeof e) n += e;
|
||||
else if ("object" == typeof e) if (Array.isArray(e)) {
|
||||
var o = e.length;
|
||||
for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
|
||||
} else for (f in e) e[f] && (n && (n += " "), n += f);
|
||||
return n;
|
||||
}
|
||||
function clsx() {
|
||||
for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
|
||||
return n;
|
||||
}
|
||||
var clsx_default = clsx;
|
||||
|
||||
export {
|
||||
clsx,
|
||||
clsx_default
|
||||
};
|
||||
//# sourceMappingURL=chunk-U7P2NEEE.js.map
|
||||
7
app/node_modules/.vite/deps/chunk-U7P2NEEE.js.map
generated
vendored
Normal file
7
app/node_modules/.vite/deps/chunk-U7P2NEEE.js.map
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../clsx/dist/clsx.mjs"],
|
||||
"sourcesContent": ["function r(e){var t,f,n=\"\";if(\"string\"==typeof e||\"number\"==typeof e)n+=e;else if(\"object\"==typeof e)if(Array.isArray(e)){var o=e.length;for(t=0;t<o;t++)e[t]&&(f=r(e[t]))&&(n&&(n+=\" \"),n+=f)}else for(f in e)e[f]&&(n&&(n+=\" \"),n+=f);return n}export function clsx(){for(var e,t,f=0,n=\"\",o=arguments.length;f<o;f++)(e=arguments[f])&&(t=r(e))&&(n&&(n+=\" \"),n+=t);return n}export default clsx;"],
|
||||
"mappings": ";AAAA,SAAS,EAAE,GAAE;AAAC,MAAI,GAAE,GAAE,IAAE;AAAG,MAAG,YAAU,OAAO,KAAG,YAAU,OAAO,EAAE,MAAG;AAAA,WAAU,YAAU,OAAO,EAAE,KAAG,MAAM,QAAQ,CAAC,GAAE;AAAC,QAAI,IAAE,EAAE;AAAO,SAAI,IAAE,GAAE,IAAE,GAAE,IAAI,GAAE,CAAC,MAAI,IAAE,EAAE,EAAE,CAAC,CAAC,OAAK,MAAI,KAAG,MAAK,KAAG;AAAA,EAAE,MAAM,MAAI,KAAK,EAAE,GAAE,CAAC,MAAI,MAAI,KAAG,MAAK,KAAG;AAAG,SAAO;AAAC;AAAQ,SAAS,OAAM;AAAC,WAAQ,GAAE,GAAE,IAAE,GAAE,IAAE,IAAG,IAAE,UAAU,QAAO,IAAE,GAAE,IAAI,EAAC,IAAE,UAAU,CAAC,OAAK,IAAE,EAAE,CAAC,OAAK,MAAI,KAAG,MAAK,KAAG;AAAG,SAAO;AAAC;AAAC,IAAO,eAAQ;",
|
||||
"names": []
|
||||
}
|
||||
51
app/node_modules/.vite/deps/class-variance-authority.js
generated
vendored
Normal file
51
app/node_modules/.vite/deps/class-variance-authority.js
generated
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
clsx
|
||||
} from "./chunk-U7P2NEEE.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
|
||||
// node_modules/class-variance-authority/dist/index.mjs
|
||||
var falsyToString = (value) => typeof value === "boolean" ? `${value}` : value === 0 ? "0" : value;
|
||||
var cx = clsx;
|
||||
var cva = (base, config) => (props) => {
|
||||
var _config_compoundVariants;
|
||||
if ((config === null || config === void 0 ? void 0 : config.variants) == null) return cx(base, props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);
|
||||
const { variants, defaultVariants } = config;
|
||||
const getVariantClassNames = Object.keys(variants).map((variant) => {
|
||||
const variantProp = props === null || props === void 0 ? void 0 : props[variant];
|
||||
const defaultVariantProp = defaultVariants === null || defaultVariants === void 0 ? void 0 : defaultVariants[variant];
|
||||
if (variantProp === null) return null;
|
||||
const variantKey = falsyToString(variantProp) || falsyToString(defaultVariantProp);
|
||||
return variants[variant][variantKey];
|
||||
});
|
||||
const propsWithoutUndefined = props && Object.entries(props).reduce((acc, param) => {
|
||||
let [key, value] = param;
|
||||
if (value === void 0) {
|
||||
return acc;
|
||||
}
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
}, {});
|
||||
const getCompoundVariantClassNames = config === null || config === void 0 ? void 0 : (_config_compoundVariants = config.compoundVariants) === null || _config_compoundVariants === void 0 ? void 0 : _config_compoundVariants.reduce((acc, param) => {
|
||||
let { class: cvClass, className: cvClassName, ...compoundVariantOptions } = param;
|
||||
return Object.entries(compoundVariantOptions).every((param2) => {
|
||||
let [key, value] = param2;
|
||||
return Array.isArray(value) ? value.includes({
|
||||
...defaultVariants,
|
||||
...propsWithoutUndefined
|
||||
}[key]) : {
|
||||
...defaultVariants,
|
||||
...propsWithoutUndefined
|
||||
}[key] === value;
|
||||
}) ? [
|
||||
...acc,
|
||||
cvClass,
|
||||
cvClassName
|
||||
] : acc;
|
||||
}, []);
|
||||
return cx(base, getVariantClassNames, getCompoundVariantClassNames, props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);
|
||||
};
|
||||
export {
|
||||
cva,
|
||||
cx
|
||||
};
|
||||
//# sourceMappingURL=class-variance-authority.js.map
|
||||
7
app/node_modules/.vite/deps/class-variance-authority.js.map
generated
vendored
Normal file
7
app/node_modules/.vite/deps/class-variance-authority.js.map
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../class-variance-authority/dist/index.mjs"],
|
||||
"sourcesContent": ["/**\r\n * Copyright 2022 Joe Bell. All rights reserved.\r\n *\r\n * This file is licensed to you under the Apache License, Version 2.0\r\n * (the \"License\"); you may not use this file except in compliance with the\r\n * License. You may obtain a copy of the License at\r\n *\r\n * http://www.apache.org/licenses/LICENSE-2.0\r\n *\r\n * Unless required by applicable law or agreed to in writing, software\r\n * distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT\r\n * WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or implied. See the\r\n * License for the specific language governing permissions and limitations under\r\n * the License.\r\n */ import { clsx } from \"clsx\";\r\nconst falsyToString = (value)=>typeof value === \"boolean\" ? `${value}` : value === 0 ? \"0\" : value;\r\nexport const cx = clsx;\r\nexport const cva = (base, config)=>(props)=>{\r\n var _config_compoundVariants;\r\n if ((config === null || config === void 0 ? void 0 : config.variants) == null) return cx(base, props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);\r\n const { variants, defaultVariants } = config;\r\n const getVariantClassNames = Object.keys(variants).map((variant)=>{\r\n const variantProp = props === null || props === void 0 ? void 0 : props[variant];\r\n const defaultVariantProp = defaultVariants === null || defaultVariants === void 0 ? void 0 : defaultVariants[variant];\r\n if (variantProp === null) return null;\r\n const variantKey = falsyToString(variantProp) || falsyToString(defaultVariantProp);\r\n return variants[variant][variantKey];\r\n });\r\n const propsWithoutUndefined = props && Object.entries(props).reduce((acc, param)=>{\r\n let [key, value] = param;\r\n if (value === undefined) {\r\n return acc;\r\n }\r\n acc[key] = value;\r\n return acc;\r\n }, {});\r\n const getCompoundVariantClassNames = config === null || config === void 0 ? void 0 : (_config_compoundVariants = config.compoundVariants) === null || _config_compoundVariants === void 0 ? void 0 : _config_compoundVariants.reduce((acc, param)=>{\r\n let { class: cvClass, className: cvClassName, ...compoundVariantOptions } = param;\r\n return Object.entries(compoundVariantOptions).every((param)=>{\r\n let [key, value] = param;\r\n return Array.isArray(value) ? value.includes({\r\n ...defaultVariants,\r\n ...propsWithoutUndefined\r\n }[key]) : ({\r\n ...defaultVariants,\r\n ...propsWithoutUndefined\r\n })[key] === value;\r\n }) ? [\r\n ...acc,\r\n cvClass,\r\n cvClassName\r\n ] : acc;\r\n }, []);\r\n return cx(base, getVariantClassNames, getCompoundVariantClassNames, props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);\r\n };\r\n\r\n"],
|
||||
"mappings": ";;;;;;AAeA,IAAM,gBAAgB,CAAC,UAAQ,OAAO,UAAU,YAAY,GAAG,KAAK,KAAK,UAAU,IAAI,MAAM;AACtF,IAAM,KAAK;AACX,IAAM,MAAM,CAAC,MAAM,WAAS,CAAC,UAAQ;AACpC,MAAI;AACJ,OAAK,WAAW,QAAQ,WAAW,SAAS,SAAS,OAAO,aAAa,KAAM,QAAO,GAAG,MAAM,UAAU,QAAQ,UAAU,SAAS,SAAS,MAAM,OAAO,UAAU,QAAQ,UAAU,SAAS,SAAS,MAAM,SAAS;AACvN,QAAM,EAAE,UAAU,gBAAgB,IAAI;AACtC,QAAM,uBAAuB,OAAO,KAAK,QAAQ,EAAE,IAAI,CAAC,YAAU;AAC9D,UAAM,cAAc,UAAU,QAAQ,UAAU,SAAS,SAAS,MAAM,OAAO;AAC/E,UAAM,qBAAqB,oBAAoB,QAAQ,oBAAoB,SAAS,SAAS,gBAAgB,OAAO;AACpH,QAAI,gBAAgB,KAAM,QAAO;AACjC,UAAM,aAAa,cAAc,WAAW,KAAK,cAAc,kBAAkB;AACjF,WAAO,SAAS,OAAO,EAAE,UAAU;AAAA,EACvC,CAAC;AACD,QAAM,wBAAwB,SAAS,OAAO,QAAQ,KAAK,EAAE,OAAO,CAAC,KAAK,UAAQ;AAC9E,QAAI,CAAC,KAAK,KAAK,IAAI;AACnB,QAAI,UAAU,QAAW;AACrB,aAAO;AAAA,IACX;AACA,QAAI,GAAG,IAAI;AACX,WAAO;AAAA,EACX,GAAG,CAAC,CAAC;AACL,QAAM,+BAA+B,WAAW,QAAQ,WAAW,SAAS,UAAU,2BAA2B,OAAO,sBAAsB,QAAQ,6BAA6B,SAAS,SAAS,yBAAyB,OAAO,CAAC,KAAK,UAAQ;AAC/O,QAAI,EAAE,OAAO,SAAS,WAAW,aAAa,GAAG,uBAAuB,IAAI;AAC5E,WAAO,OAAO,QAAQ,sBAAsB,EAAE,MAAM,CAACA,WAAQ;AACzD,UAAI,CAAC,KAAK,KAAK,IAAIA;AACnB,aAAO,MAAM,QAAQ,KAAK,IAAI,MAAM,SAAS;AAAA,QACzC,GAAG;AAAA,QACH,GAAG;AAAA,MACP,EAAE,GAAG,CAAC,IAAK;AAAA,QACP,GAAG;AAAA,QACH,GAAG;AAAA,MACP,EAAG,GAAG,MAAM;AAAA,IAChB,CAAC,IAAI;AAAA,MACD,GAAG;AAAA,MACH;AAAA,MACA;AAAA,IACJ,IAAI;AAAA,EACR,GAAG,CAAC,CAAC;AACL,SAAO,GAAG,MAAM,sBAAsB,8BAA8B,UAAU,QAAQ,UAAU,SAAS,SAAS,MAAM,OAAO,UAAU,QAAQ,UAAU,SAAS,SAAS,MAAM,SAAS;AAChM;",
|
||||
"names": ["param"]
|
||||
}
|
||||
9
app/node_modules/.vite/deps/clsx.js
generated
vendored
Normal file
9
app/node_modules/.vite/deps/clsx.js
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import {
|
||||
clsx,
|
||||
clsx_default
|
||||
} from "./chunk-U7P2NEEE.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export {
|
||||
clsx,
|
||||
clsx_default as default
|
||||
};
|
||||
7
app/node_modules/.vite/deps/clsx.js.map
generated
vendored
Normal file
7
app/node_modules/.vite/deps/clsx.js.map
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
12319
app/node_modules/.vite/deps/framer-motion.js
generated
vendored
Normal file
12319
app/node_modules/.vite/deps/framer-motion.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
app/node_modules/.vite/deps/framer-motion.js.map
generated
vendored
Normal file
7
app/node_modules/.vite/deps/framer-motion.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
98
app/node_modules/.vite/deps/hls-Q6LDPZPT.js
generated
vendored
Normal file
98
app/node_modules/.vite/deps/hls-Q6LDPZPT.js
generated
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
AbrController,
|
||||
AttrList,
|
||||
AudioStreamController,
|
||||
AudioTrackController,
|
||||
BasePlaylistController,
|
||||
BaseSegment,
|
||||
BaseStreamController,
|
||||
BufferController,
|
||||
CMCDController,
|
||||
CapLevelController,
|
||||
ChunkMetadata,
|
||||
ContentSteeringController,
|
||||
Cues,
|
||||
DateRange,
|
||||
EMEController,
|
||||
ErrorActionFlags,
|
||||
ErrorController,
|
||||
ErrorDetails,
|
||||
ErrorTypes,
|
||||
Events,
|
||||
FPSController,
|
||||
FetchLoader,
|
||||
Fragment,
|
||||
Hls,
|
||||
HlsSkip,
|
||||
HlsUrlParameters,
|
||||
KeySystemFormats,
|
||||
KeySystems,
|
||||
Level,
|
||||
LevelDetails,
|
||||
LevelKey,
|
||||
LoadStats,
|
||||
M3U8Parser,
|
||||
MetadataSchema,
|
||||
NetworkErrorAction,
|
||||
Part,
|
||||
PlaylistLevelType,
|
||||
SubtitleStreamController,
|
||||
SubtitleTrackController,
|
||||
TimelineController,
|
||||
XhrLoader,
|
||||
fetchSupported,
|
||||
getMediaSource,
|
||||
isMSESupported,
|
||||
isSupported,
|
||||
requestMediaKeySystemAccess
|
||||
} from "./chunk-OAEA5FZL.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export {
|
||||
AbrController,
|
||||
AttrList,
|
||||
AudioStreamController,
|
||||
AudioTrackController,
|
||||
BasePlaylistController,
|
||||
BaseSegment,
|
||||
BaseStreamController,
|
||||
BufferController,
|
||||
CMCDController,
|
||||
CapLevelController,
|
||||
ChunkMetadata,
|
||||
ContentSteeringController,
|
||||
Cues,
|
||||
DateRange,
|
||||
EMEController,
|
||||
ErrorActionFlags,
|
||||
ErrorController,
|
||||
ErrorDetails,
|
||||
ErrorTypes,
|
||||
Events,
|
||||
FPSController,
|
||||
FetchLoader,
|
||||
Fragment,
|
||||
Hls,
|
||||
HlsSkip,
|
||||
HlsUrlParameters,
|
||||
KeySystemFormats,
|
||||
KeySystems,
|
||||
Level,
|
||||
LevelDetails,
|
||||
LevelKey,
|
||||
LoadStats,
|
||||
M3U8Parser,
|
||||
MetadataSchema,
|
||||
NetworkErrorAction,
|
||||
Part,
|
||||
PlaylistLevelType,
|
||||
SubtitleStreamController,
|
||||
SubtitleTrackController,
|
||||
TimelineController,
|
||||
XhrLoader,
|
||||
Hls as default,
|
||||
fetchSupported,
|
||||
getMediaSource,
|
||||
isMSESupported,
|
||||
isSupported,
|
||||
requestMediaKeySystemAccess
|
||||
};
|
||||
7
app/node_modules/.vite/deps/hls-Q6LDPZPT.js.map
generated
vendored
Normal file
7
app/node_modules/.vite/deps/hls-Q6LDPZPT.js.map
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
27351
app/node_modules/.vite/deps/lucide-react.js
generated
vendored
Normal file
27351
app/node_modules/.vite/deps/lucide-react.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
app/node_modules/.vite/deps/lucide-react.js.map
generated
vendored
Normal file
7
app/node_modules/.vite/deps/lucide-react.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
264
app/node_modules/.vite/deps/react-dom.js
generated
vendored
264
app/node_modules/.vite/deps/react-dom.js
generated
vendored
@@ -1,262 +1,6 @@
|
||||
import {
|
||||
__commonJS,
|
||||
require_react
|
||||
} from "./chunk-E7O7WYRO.js";
|
||||
|
||||
// node_modules/react-dom/cjs/react-dom.development.js
|
||||
var require_react_dom_development = __commonJS({
|
||||
"node_modules/react-dom/cjs/react-dom.development.js"(exports) {
|
||||
"use strict";
|
||||
(function() {
|
||||
function noop() {
|
||||
}
|
||||
function testStringCoercion(value) {
|
||||
return "" + value;
|
||||
}
|
||||
function createPortal$1(children, containerInfo, implementation) {
|
||||
var key = 3 < arguments.length && void 0 !== arguments[3] ? arguments[3] : null;
|
||||
try {
|
||||
testStringCoercion(key);
|
||||
var JSCompiler_inline_result = false;
|
||||
} catch (e) {
|
||||
JSCompiler_inline_result = true;
|
||||
}
|
||||
JSCompiler_inline_result && (console.error(
|
||||
"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.",
|
||||
"function" === typeof Symbol && Symbol.toStringTag && key[Symbol.toStringTag] || key.constructor.name || "Object"
|
||||
), testStringCoercion(key));
|
||||
return {
|
||||
$$typeof: REACT_PORTAL_TYPE,
|
||||
key: null == key ? null : "" + key,
|
||||
children,
|
||||
containerInfo,
|
||||
implementation
|
||||
};
|
||||
}
|
||||
function getCrossOriginStringAs(as, input) {
|
||||
if ("font" === as) return "";
|
||||
if ("string" === typeof input)
|
||||
return "use-credentials" === input ? input : "";
|
||||
}
|
||||
function getValueDescriptorExpectingObjectForWarning(thing) {
|
||||
return null === thing ? "`null`" : void 0 === thing ? "`undefined`" : "" === thing ? "an empty string" : 'something with type "' + typeof thing + '"';
|
||||
}
|
||||
function getValueDescriptorExpectingEnumForWarning(thing) {
|
||||
return null === thing ? "`null`" : void 0 === thing ? "`undefined`" : "" === thing ? "an empty string" : "string" === typeof thing ? JSON.stringify(thing) : "number" === typeof thing ? "`" + thing + "`" : 'something with type "' + typeof thing + '"';
|
||||
}
|
||||
function resolveDispatcher() {
|
||||
var dispatcher = ReactSharedInternals.H;
|
||||
null === dispatcher && console.error(
|
||||
"Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem."
|
||||
);
|
||||
return dispatcher;
|
||||
}
|
||||
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());
|
||||
var React = require_react(), Internals = {
|
||||
d: {
|
||||
f: noop,
|
||||
r: function() {
|
||||
throw Error(
|
||||
"Invalid form element. requestFormReset must be passed a form that was rendered by React."
|
||||
);
|
||||
},
|
||||
D: noop,
|
||||
C: noop,
|
||||
L: noop,
|
||||
m: noop,
|
||||
X: noop,
|
||||
S: noop,
|
||||
M: noop
|
||||
},
|
||||
p: 0,
|
||||
findDOMNode: null
|
||||
}, REACT_PORTAL_TYPE = /* @__PURE__ */ Symbol.for("react.portal"), ReactSharedInternals = React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
||||
"function" === typeof Map && null != Map.prototype && "function" === typeof Map.prototype.forEach && "function" === typeof Set && null != Set.prototype && "function" === typeof Set.prototype.clear && "function" === typeof Set.prototype.forEach || console.error(
|
||||
"React depends on Map and Set built-in types. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills"
|
||||
);
|
||||
exports.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = Internals;
|
||||
exports.createPortal = function(children, container) {
|
||||
var key = 2 < arguments.length && void 0 !== arguments[2] ? arguments[2] : null;
|
||||
if (!container || 1 !== container.nodeType && 9 !== container.nodeType && 11 !== container.nodeType)
|
||||
throw Error("Target container is not a DOM element.");
|
||||
return createPortal$1(children, container, null, key);
|
||||
};
|
||||
exports.flushSync = function(fn) {
|
||||
var previousTransition = ReactSharedInternals.T, previousUpdatePriority = Internals.p;
|
||||
try {
|
||||
if (ReactSharedInternals.T = null, Internals.p = 2, fn)
|
||||
return fn();
|
||||
} finally {
|
||||
ReactSharedInternals.T = previousTransition, Internals.p = previousUpdatePriority, Internals.d.f() && console.error(
|
||||
"flushSync was called from inside a lifecycle method. React cannot flush when React is already rendering. Consider moving this call to a scheduler task or micro task."
|
||||
);
|
||||
}
|
||||
};
|
||||
exports.preconnect = function(href, options) {
|
||||
"string" === typeof href && href ? null != options && "object" !== typeof options ? console.error(
|
||||
"ReactDOM.preconnect(): Expected the `options` argument (second) to be an object but encountered %s instead. The only supported option at this time is `crossOrigin` which accepts a string.",
|
||||
getValueDescriptorExpectingEnumForWarning(options)
|
||||
) : null != options && "string" !== typeof options.crossOrigin && console.error(
|
||||
"ReactDOM.preconnect(): Expected the `crossOrigin` option (second argument) to be a string but encountered %s instead. Try removing this option or passing a string value instead.",
|
||||
getValueDescriptorExpectingObjectForWarning(options.crossOrigin)
|
||||
) : console.error(
|
||||
"ReactDOM.preconnect(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.",
|
||||
getValueDescriptorExpectingObjectForWarning(href)
|
||||
);
|
||||
"string" === typeof href && (options ? (options = options.crossOrigin, options = "string" === typeof options ? "use-credentials" === options ? options : "" : void 0) : options = null, Internals.d.C(href, options));
|
||||
};
|
||||
exports.prefetchDNS = function(href) {
|
||||
if ("string" !== typeof href || !href)
|
||||
console.error(
|
||||
"ReactDOM.prefetchDNS(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.",
|
||||
getValueDescriptorExpectingObjectForWarning(href)
|
||||
);
|
||||
else if (1 < arguments.length) {
|
||||
var options = arguments[1];
|
||||
"object" === typeof options && options.hasOwnProperty("crossOrigin") ? console.error(
|
||||
"ReactDOM.prefetchDNS(): Expected only one argument, `href`, but encountered %s as a second argument instead. This argument is reserved for future options and is currently disallowed. It looks like the you are attempting to set a crossOrigin property for this DNS lookup hint. Browsers do not perform DNS queries using CORS and setting this attribute on the resource hint has no effect. Try calling ReactDOM.prefetchDNS() with just a single string argument, `href`.",
|
||||
getValueDescriptorExpectingEnumForWarning(options)
|
||||
) : console.error(
|
||||
"ReactDOM.prefetchDNS(): Expected only one argument, `href`, but encountered %s as a second argument instead. This argument is reserved for future options and is currently disallowed. Try calling ReactDOM.prefetchDNS() with just a single string argument, `href`.",
|
||||
getValueDescriptorExpectingEnumForWarning(options)
|
||||
);
|
||||
}
|
||||
"string" === typeof href && Internals.d.D(href);
|
||||
};
|
||||
exports.preinit = function(href, options) {
|
||||
"string" === typeof href && href ? null == options || "object" !== typeof options ? console.error(
|
||||
"ReactDOM.preinit(): Expected the `options` argument (second) to be an object with an `as` property describing the type of resource to be preinitialized but encountered %s instead.",
|
||||
getValueDescriptorExpectingEnumForWarning(options)
|
||||
) : "style" !== options.as && "script" !== options.as && console.error(
|
||||
'ReactDOM.preinit(): Expected the `as` property in the `options` argument (second) to contain a valid value describing the type of resource to be preinitialized but encountered %s instead. Valid values for `as` are "style" and "script".',
|
||||
getValueDescriptorExpectingEnumForWarning(options.as)
|
||||
) : console.error(
|
||||
"ReactDOM.preinit(): Expected the `href` argument (first) to be a non-empty string but encountered %s instead.",
|
||||
getValueDescriptorExpectingObjectForWarning(href)
|
||||
);
|
||||
if ("string" === typeof href && options && "string" === typeof options.as) {
|
||||
var as = options.as, crossOrigin = getCrossOriginStringAs(as, options.crossOrigin), integrity = "string" === typeof options.integrity ? options.integrity : void 0, fetchPriority = "string" === typeof options.fetchPriority ? options.fetchPriority : void 0;
|
||||
"style" === as ? Internals.d.S(
|
||||
href,
|
||||
"string" === typeof options.precedence ? options.precedence : void 0,
|
||||
{
|
||||
crossOrigin,
|
||||
integrity,
|
||||
fetchPriority
|
||||
}
|
||||
) : "script" === as && Internals.d.X(href, {
|
||||
crossOrigin,
|
||||
integrity,
|
||||
fetchPriority,
|
||||
nonce: "string" === typeof options.nonce ? options.nonce : void 0
|
||||
});
|
||||
}
|
||||
};
|
||||
exports.preinitModule = function(href, options) {
|
||||
var encountered = "";
|
||||
"string" === typeof href && href || (encountered += " The `href` argument encountered was " + getValueDescriptorExpectingObjectForWarning(href) + ".");
|
||||
void 0 !== options && "object" !== typeof options ? encountered += " The `options` argument encountered was " + getValueDescriptorExpectingObjectForWarning(options) + "." : options && "as" in options && "script" !== options.as && (encountered += " The `as` option encountered was " + getValueDescriptorExpectingEnumForWarning(options.as) + ".");
|
||||
if (encountered)
|
||||
console.error(
|
||||
"ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property.%s",
|
||||
encountered
|
||||
);
|
||||
else
|
||||
switch (encountered = options && "string" === typeof options.as ? options.as : "script", encountered) {
|
||||
case "script":
|
||||
break;
|
||||
default:
|
||||
encountered = getValueDescriptorExpectingEnumForWarning(encountered), console.error(
|
||||
'ReactDOM.preinitModule(): Currently the only supported "as" type for this function is "script" but received "%s" instead. This warning was generated for `href` "%s". In the future other module types will be supported, aligning with the import-attributes proposal. Learn more here: (https://github.com/tc39/proposal-import-attributes)',
|
||||
encountered,
|
||||
href
|
||||
);
|
||||
}
|
||||
if ("string" === typeof href)
|
||||
if ("object" === typeof options && null !== options) {
|
||||
if (null == options.as || "script" === options.as)
|
||||
encountered = getCrossOriginStringAs(
|
||||
options.as,
|
||||
options.crossOrigin
|
||||
), Internals.d.M(href, {
|
||||
crossOrigin: encountered,
|
||||
integrity: "string" === typeof options.integrity ? options.integrity : void 0,
|
||||
nonce: "string" === typeof options.nonce ? options.nonce : void 0
|
||||
});
|
||||
} else null == options && Internals.d.M(href);
|
||||
};
|
||||
exports.preload = function(href, options) {
|
||||
var encountered = "";
|
||||
"string" === typeof href && href || (encountered += " The `href` argument encountered was " + getValueDescriptorExpectingObjectForWarning(href) + ".");
|
||||
null == options || "object" !== typeof options ? encountered += " The `options` argument encountered was " + getValueDescriptorExpectingObjectForWarning(options) + "." : "string" === typeof options.as && options.as || (encountered += " The `as` option encountered was " + getValueDescriptorExpectingObjectForWarning(options.as) + ".");
|
||||
encountered && console.error(
|
||||
'ReactDOM.preload(): Expected two arguments, a non-empty `href` string and an `options` object with an `as` property valid for a `<link rel="preload" as="..." />` tag.%s',
|
||||
encountered
|
||||
);
|
||||
if ("string" === typeof href && "object" === typeof options && null !== options && "string" === typeof options.as) {
|
||||
encountered = options.as;
|
||||
var crossOrigin = getCrossOriginStringAs(
|
||||
encountered,
|
||||
options.crossOrigin
|
||||
);
|
||||
Internals.d.L(href, encountered, {
|
||||
crossOrigin,
|
||||
integrity: "string" === typeof options.integrity ? options.integrity : void 0,
|
||||
nonce: "string" === typeof options.nonce ? options.nonce : void 0,
|
||||
type: "string" === typeof options.type ? options.type : void 0,
|
||||
fetchPriority: "string" === typeof options.fetchPriority ? options.fetchPriority : void 0,
|
||||
referrerPolicy: "string" === typeof options.referrerPolicy ? options.referrerPolicy : void 0,
|
||||
imageSrcSet: "string" === typeof options.imageSrcSet ? options.imageSrcSet : void 0,
|
||||
imageSizes: "string" === typeof options.imageSizes ? options.imageSizes : void 0,
|
||||
media: "string" === typeof options.media ? options.media : void 0
|
||||
});
|
||||
}
|
||||
};
|
||||
exports.preloadModule = function(href, options) {
|
||||
var encountered = "";
|
||||
"string" === typeof href && href || (encountered += " The `href` argument encountered was " + getValueDescriptorExpectingObjectForWarning(href) + ".");
|
||||
void 0 !== options && "object" !== typeof options ? encountered += " The `options` argument encountered was " + getValueDescriptorExpectingObjectForWarning(options) + "." : options && "as" in options && "string" !== typeof options.as && (encountered += " The `as` option encountered was " + getValueDescriptorExpectingObjectForWarning(options.as) + ".");
|
||||
encountered && console.error(
|
||||
'ReactDOM.preloadModule(): Expected two arguments, a non-empty `href` string and, optionally, an `options` object with an `as` property valid for a `<link rel="modulepreload" as="..." />` tag.%s',
|
||||
encountered
|
||||
);
|
||||
"string" === typeof href && (options ? (encountered = getCrossOriginStringAs(
|
||||
options.as,
|
||||
options.crossOrigin
|
||||
), Internals.d.m(href, {
|
||||
as: "string" === typeof options.as && "script" !== options.as ? options.as : void 0,
|
||||
crossOrigin: encountered,
|
||||
integrity: "string" === typeof options.integrity ? options.integrity : void 0
|
||||
})) : Internals.d.m(href));
|
||||
};
|
||||
exports.requestFormReset = function(form) {
|
||||
Internals.d.r(form);
|
||||
};
|
||||
exports.unstable_batchedUpdates = function(fn, a) {
|
||||
return fn(a);
|
||||
};
|
||||
exports.useFormState = function(action, initialState, permalink) {
|
||||
return resolveDispatcher().useFormState(action, initialState, permalink);
|
||||
};
|
||||
exports.useFormStatus = function() {
|
||||
return resolveDispatcher().useHostTransitionStatus();
|
||||
};
|
||||
exports.version = "19.2.3";
|
||||
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ && "function" === typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop && __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error());
|
||||
})();
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/react-dom/index.js
|
||||
var require_react_dom = __commonJS({
|
||||
"node_modules/react-dom/index.js"(exports, module) {
|
||||
if (false) {
|
||||
checkDCE();
|
||||
module.exports = null;
|
||||
} else {
|
||||
module.exports = require_react_dom_development();
|
||||
}
|
||||
}
|
||||
});
|
||||
require_react_dom
|
||||
} from "./chunk-YLZ34CCM.js";
|
||||
import "./chunk-ZNKPWGXJ.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export default require_react_dom();
|
||||
//# sourceMappingURL=react-dom.js.map
|
||||
|
||||
6
app/node_modules/.vite/deps/react-dom.js.map
generated
vendored
6
app/node_modules/.vite/deps/react-dom.js.map
generated
vendored
File diff suppressed because one or more lines are too long
8
app/node_modules/.vite/deps/react-dom_client.js
generated
vendored
Normal file
8
app/node_modules/.vite/deps/react-dom_client.js
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
import {
|
||||
require_client
|
||||
} from "./chunk-2NWYL6R2.js";
|
||||
import "./chunk-YLZ34CCM.js";
|
||||
import "./chunk-QURGMCZB.js";
|
||||
import "./chunk-ZNKPWGXJ.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export default require_client();
|
||||
7
app/node_modules/.vite/deps/react-dom_client.js.map
generated
vendored
Normal file
7
app/node_modules/.vite/deps/react-dom_client.js.map
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
3
app/node_modules/.vite/deps/react.js
generated
vendored
3
app/node_modules/.vite/deps/react.js
generated
vendored
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
require_react
|
||||
} from "./chunk-E7O7WYRO.js";
|
||||
} from "./chunk-ZNKPWGXJ.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export default require_react();
|
||||
|
||||
6
app/node_modules/.vite/deps/react_jsx-dev-runtime.js
generated
vendored
6
app/node_modules/.vite/deps/react_jsx-dev-runtime.js
generated
vendored
@@ -1,7 +1,9 @@
|
||||
import {
|
||||
__commonJS,
|
||||
require_react
|
||||
} from "./chunk-E7O7WYRO.js";
|
||||
} from "./chunk-ZNKPWGXJ.js";
|
||||
import {
|
||||
__commonJS
|
||||
} from "./chunk-G3PMV62Z.js";
|
||||
|
||||
// node_modules/react/cjs/react-jsx-dev-runtime.development.js
|
||||
var require_react_jsx_dev_runtime_development = __commonJS({
|
||||
|
||||
4
app/node_modules/.vite/deps/react_jsx-dev-runtime.js.map
generated
vendored
4
app/node_modules/.vite/deps/react_jsx-dev-runtime.js.map
generated
vendored
File diff suppressed because one or more lines are too long
276
app/node_modules/.vite/deps/react_jsx-runtime.js
generated
vendored
276
app/node_modules/.vite/deps/react_jsx-runtime.js
generated
vendored
@@ -1,274 +1,6 @@
|
||||
import {
|
||||
__commonJS,
|
||||
require_react
|
||||
} from "./chunk-E7O7WYRO.js";
|
||||
|
||||
// node_modules/react/cjs/react-jsx-runtime.development.js
|
||||
var require_react_jsx_runtime_development = __commonJS({
|
||||
"node_modules/react/cjs/react-jsx-runtime.development.js"(exports) {
|
||||
"use strict";
|
||||
(function() {
|
||||
function getComponentNameFromType(type) {
|
||||
if (null == type) return null;
|
||||
if ("function" === typeof type)
|
||||
return type.$$typeof === REACT_CLIENT_REFERENCE ? null : type.displayName || type.name || null;
|
||||
if ("string" === typeof type) return type;
|
||||
switch (type) {
|
||||
case REACT_FRAGMENT_TYPE:
|
||||
return "Fragment";
|
||||
case REACT_PROFILER_TYPE:
|
||||
return "Profiler";
|
||||
case REACT_STRICT_MODE_TYPE:
|
||||
return "StrictMode";
|
||||
case REACT_SUSPENSE_TYPE:
|
||||
return "Suspense";
|
||||
case REACT_SUSPENSE_LIST_TYPE:
|
||||
return "SuspenseList";
|
||||
case REACT_ACTIVITY_TYPE:
|
||||
return "Activity";
|
||||
}
|
||||
if ("object" === typeof type)
|
||||
switch ("number" === typeof type.tag && console.error(
|
||||
"Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."
|
||||
), type.$$typeof) {
|
||||
case REACT_PORTAL_TYPE:
|
||||
return "Portal";
|
||||
case REACT_CONTEXT_TYPE:
|
||||
return type.displayName || "Context";
|
||||
case REACT_CONSUMER_TYPE:
|
||||
return (type._context.displayName || "Context") + ".Consumer";
|
||||
case REACT_FORWARD_REF_TYPE:
|
||||
var innerType = type.render;
|
||||
type = type.displayName;
|
||||
type || (type = innerType.displayName || innerType.name || "", type = "" !== type ? "ForwardRef(" + type + ")" : "ForwardRef");
|
||||
return type;
|
||||
case REACT_MEMO_TYPE:
|
||||
return innerType = type.displayName || null, null !== innerType ? innerType : getComponentNameFromType(type.type) || "Memo";
|
||||
case REACT_LAZY_TYPE:
|
||||
innerType = type._payload;
|
||||
type = type._init;
|
||||
try {
|
||||
return getComponentNameFromType(type(innerType));
|
||||
} catch (x) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function testStringCoercion(value) {
|
||||
return "" + value;
|
||||
}
|
||||
function checkKeyStringCoercion(value) {
|
||||
try {
|
||||
testStringCoercion(value);
|
||||
var JSCompiler_inline_result = false;
|
||||
} catch (e) {
|
||||
JSCompiler_inline_result = true;
|
||||
}
|
||||
if (JSCompiler_inline_result) {
|
||||
JSCompiler_inline_result = console;
|
||||
var JSCompiler_temp_const = JSCompiler_inline_result.error;
|
||||
var JSCompiler_inline_result$jscomp$0 = "function" === typeof Symbol && Symbol.toStringTag && value[Symbol.toStringTag] || value.constructor.name || "Object";
|
||||
JSCompiler_temp_const.call(
|
||||
JSCompiler_inline_result,
|
||||
"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.",
|
||||
JSCompiler_inline_result$jscomp$0
|
||||
);
|
||||
return testStringCoercion(value);
|
||||
}
|
||||
}
|
||||
function getTaskName(type) {
|
||||
if (type === REACT_FRAGMENT_TYPE) return "<>";
|
||||
if ("object" === typeof type && null !== type && type.$$typeof === REACT_LAZY_TYPE)
|
||||
return "<...>";
|
||||
try {
|
||||
var name = getComponentNameFromType(type);
|
||||
return name ? "<" + name + ">" : "<...>";
|
||||
} catch (x) {
|
||||
return "<...>";
|
||||
}
|
||||
}
|
||||
function getOwner() {
|
||||
var dispatcher = ReactSharedInternals.A;
|
||||
return null === dispatcher ? null : dispatcher.getOwner();
|
||||
}
|
||||
function UnknownOwner() {
|
||||
return Error("react-stack-top-frame");
|
||||
}
|
||||
function hasValidKey(config) {
|
||||
if (hasOwnProperty.call(config, "key")) {
|
||||
var getter = Object.getOwnPropertyDescriptor(config, "key").get;
|
||||
if (getter && getter.isReactWarning) return false;
|
||||
}
|
||||
return void 0 !== config.key;
|
||||
}
|
||||
function defineKeyPropWarningGetter(props, displayName) {
|
||||
function warnAboutAccessingKey() {
|
||||
specialPropKeyWarningShown || (specialPropKeyWarningShown = true, console.error(
|
||||
"%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)",
|
||||
displayName
|
||||
));
|
||||
}
|
||||
warnAboutAccessingKey.isReactWarning = true;
|
||||
Object.defineProperty(props, "key", {
|
||||
get: warnAboutAccessingKey,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
function elementRefGetterWithDeprecationWarning() {
|
||||
var componentName = getComponentNameFromType(this.type);
|
||||
didWarnAboutElementRef[componentName] || (didWarnAboutElementRef[componentName] = true, console.error(
|
||||
"Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release."
|
||||
));
|
||||
componentName = this.props.ref;
|
||||
return void 0 !== componentName ? componentName : null;
|
||||
}
|
||||
function ReactElement(type, key, props, owner, debugStack, debugTask) {
|
||||
var refProp = props.ref;
|
||||
type = {
|
||||
$$typeof: REACT_ELEMENT_TYPE,
|
||||
type,
|
||||
key,
|
||||
props,
|
||||
_owner: owner
|
||||
};
|
||||
null !== (void 0 !== refProp ? refProp : null) ? Object.defineProperty(type, "ref", {
|
||||
enumerable: false,
|
||||
get: elementRefGetterWithDeprecationWarning
|
||||
}) : Object.defineProperty(type, "ref", { enumerable: false, value: null });
|
||||
type._store = {};
|
||||
Object.defineProperty(type._store, "validated", {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: 0
|
||||
});
|
||||
Object.defineProperty(type, "_debugInfo", {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: null
|
||||
});
|
||||
Object.defineProperty(type, "_debugStack", {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: debugStack
|
||||
});
|
||||
Object.defineProperty(type, "_debugTask", {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: debugTask
|
||||
});
|
||||
Object.freeze && (Object.freeze(type.props), Object.freeze(type));
|
||||
return type;
|
||||
}
|
||||
function jsxDEVImpl(type, config, maybeKey, isStaticChildren, debugStack, debugTask) {
|
||||
var children = config.children;
|
||||
if (void 0 !== children)
|
||||
if (isStaticChildren)
|
||||
if (isArrayImpl(children)) {
|
||||
for (isStaticChildren = 0; isStaticChildren < children.length; isStaticChildren++)
|
||||
validateChildKeys(children[isStaticChildren]);
|
||||
Object.freeze && Object.freeze(children);
|
||||
} else
|
||||
console.error(
|
||||
"React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead."
|
||||
);
|
||||
else validateChildKeys(children);
|
||||
if (hasOwnProperty.call(config, "key")) {
|
||||
children = getComponentNameFromType(type);
|
||||
var keys = Object.keys(config).filter(function(k) {
|
||||
return "key" !== k;
|
||||
});
|
||||
isStaticChildren = 0 < keys.length ? "{key: someKey, " + keys.join(": ..., ") + ": ...}" : "{key: someKey}";
|
||||
didWarnAboutKeySpread[children + isStaticChildren] || (keys = 0 < keys.length ? "{" + keys.join(": ..., ") + ": ...}" : "{}", console.error(
|
||||
'A props object containing a "key" prop is being spread into JSX:\n let props = %s;\n <%s {...props} />\nReact keys must be passed directly to JSX without using spread:\n let props = %s;\n <%s key={someKey} {...props} />',
|
||||
isStaticChildren,
|
||||
children,
|
||||
keys,
|
||||
children
|
||||
), didWarnAboutKeySpread[children + isStaticChildren] = true);
|
||||
}
|
||||
children = null;
|
||||
void 0 !== maybeKey && (checkKeyStringCoercion(maybeKey), children = "" + maybeKey);
|
||||
hasValidKey(config) && (checkKeyStringCoercion(config.key), children = "" + config.key);
|
||||
if ("key" in config) {
|
||||
maybeKey = {};
|
||||
for (var propName in config)
|
||||
"key" !== propName && (maybeKey[propName] = config[propName]);
|
||||
} else maybeKey = config;
|
||||
children && defineKeyPropWarningGetter(
|
||||
maybeKey,
|
||||
"function" === typeof type ? type.displayName || type.name || "Unknown" : type
|
||||
);
|
||||
return ReactElement(
|
||||
type,
|
||||
children,
|
||||
maybeKey,
|
||||
getOwner(),
|
||||
debugStack,
|
||||
debugTask
|
||||
);
|
||||
}
|
||||
function validateChildKeys(node) {
|
||||
isValidElement(node) ? node._store && (node._store.validated = 1) : "object" === typeof node && null !== node && node.$$typeof === REACT_LAZY_TYPE && ("fulfilled" === node._payload.status ? isValidElement(node._payload.value) && node._payload.value._store && (node._payload.value._store.validated = 1) : node._store && (node._store.validated = 1));
|
||||
}
|
||||
function isValidElement(object) {
|
||||
return "object" === typeof object && null !== object && object.$$typeof === REACT_ELEMENT_TYPE;
|
||||
}
|
||||
var React = require_react(), REACT_ELEMENT_TYPE = /* @__PURE__ */ Symbol.for("react.transitional.element"), REACT_PORTAL_TYPE = /* @__PURE__ */ Symbol.for("react.portal"), REACT_FRAGMENT_TYPE = /* @__PURE__ */ Symbol.for("react.fragment"), REACT_STRICT_MODE_TYPE = /* @__PURE__ */ Symbol.for("react.strict_mode"), REACT_PROFILER_TYPE = /* @__PURE__ */ Symbol.for("react.profiler"), REACT_CONSUMER_TYPE = /* @__PURE__ */ Symbol.for("react.consumer"), REACT_CONTEXT_TYPE = /* @__PURE__ */ Symbol.for("react.context"), REACT_FORWARD_REF_TYPE = /* @__PURE__ */ Symbol.for("react.forward_ref"), REACT_SUSPENSE_TYPE = /* @__PURE__ */ Symbol.for("react.suspense"), REACT_SUSPENSE_LIST_TYPE = /* @__PURE__ */ Symbol.for("react.suspense_list"), REACT_MEMO_TYPE = /* @__PURE__ */ Symbol.for("react.memo"), REACT_LAZY_TYPE = /* @__PURE__ */ Symbol.for("react.lazy"), REACT_ACTIVITY_TYPE = /* @__PURE__ */ Symbol.for("react.activity"), REACT_CLIENT_REFERENCE = /* @__PURE__ */ Symbol.for("react.client.reference"), ReactSharedInternals = React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, hasOwnProperty = Object.prototype.hasOwnProperty, isArrayImpl = Array.isArray, createTask = console.createTask ? console.createTask : function() {
|
||||
return null;
|
||||
};
|
||||
React = {
|
||||
react_stack_bottom_frame: function(callStackForError) {
|
||||
return callStackForError();
|
||||
}
|
||||
};
|
||||
var specialPropKeyWarningShown;
|
||||
var didWarnAboutElementRef = {};
|
||||
var unknownOwnerDebugStack = React.react_stack_bottom_frame.bind(
|
||||
React,
|
||||
UnknownOwner
|
||||
)();
|
||||
var unknownOwnerDebugTask = createTask(getTaskName(UnknownOwner));
|
||||
var didWarnAboutKeySpread = {};
|
||||
exports.Fragment = REACT_FRAGMENT_TYPE;
|
||||
exports.jsx = function(type, config, maybeKey) {
|
||||
var trackActualOwner = 1e4 > ReactSharedInternals.recentlyCreatedOwnerStacks++;
|
||||
return jsxDEVImpl(
|
||||
type,
|
||||
config,
|
||||
maybeKey,
|
||||
false,
|
||||
trackActualOwner ? Error("react-stack-top-frame") : unknownOwnerDebugStack,
|
||||
trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask
|
||||
);
|
||||
};
|
||||
exports.jsxs = function(type, config, maybeKey) {
|
||||
var trackActualOwner = 1e4 > ReactSharedInternals.recentlyCreatedOwnerStacks++;
|
||||
return jsxDEVImpl(
|
||||
type,
|
||||
config,
|
||||
maybeKey,
|
||||
true,
|
||||
trackActualOwner ? Error("react-stack-top-frame") : unknownOwnerDebugStack,
|
||||
trackActualOwner ? createTask(getTaskName(type)) : unknownOwnerDebugTask
|
||||
);
|
||||
};
|
||||
})();
|
||||
}
|
||||
});
|
||||
|
||||
// node_modules/react/jsx-runtime.js
|
||||
var require_jsx_runtime = __commonJS({
|
||||
"node_modules/react/jsx-runtime.js"(exports, module) {
|
||||
if (false) {
|
||||
module.exports = null;
|
||||
} else {
|
||||
module.exports = require_react_jsx_runtime_development();
|
||||
}
|
||||
}
|
||||
});
|
||||
require_jsx_runtime
|
||||
} from "./chunk-USXRE7Q2.js";
|
||||
import "./chunk-ZNKPWGXJ.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export default require_jsx_runtime();
|
||||
//# sourceMappingURL=react_jsx-runtime.js.map
|
||||
|
||||
6
app/node_modules/.vite/deps/react_jsx-runtime.js.map
generated
vendored
6
app/node_modules/.vite/deps/react_jsx-runtime.js.map
generated
vendored
File diff suppressed because one or more lines are too long
2506
app/node_modules/.vite/deps/recharts.js
generated
vendored
2506
app/node_modules/.vite/deps/recharts.js
generated
vendored
File diff suppressed because it is too large
Load Diff
7
app/node_modules/.vite/deps/recharts.js.map
generated
vendored
Normal file
7
app/node_modules/.vite/deps/recharts.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
3095
app/node_modules/.vite/deps/tailwind-merge.js
generated
vendored
Normal file
3095
app/node_modules/.vite/deps/tailwind-merge.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
app/node_modules/.vite/deps/tailwind-merge.js.map
generated
vendored
Normal file
7
app/node_modules/.vite/deps/tailwind-merge.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
877
app/node_modules/.vite/deps/three.js
generated
vendored
Normal file
877
app/node_modules/.vite/deps/three.js
generated
vendored
Normal file
@@ -0,0 +1,877 @@
|
||||
import {
|
||||
ACESFilmicToneMapping,
|
||||
AddEquation,
|
||||
AddOperation,
|
||||
AdditiveAnimationBlendMode,
|
||||
AdditiveBlending,
|
||||
AgXToneMapping,
|
||||
AlphaFormat,
|
||||
AlwaysCompare,
|
||||
AlwaysDepth,
|
||||
AlwaysStencilFunc,
|
||||
AmbientLight,
|
||||
AnimationAction,
|
||||
AnimationClip,
|
||||
AnimationLoader,
|
||||
AnimationMixer,
|
||||
AnimationObjectGroup,
|
||||
AnimationUtils,
|
||||
ArcCurve,
|
||||
ArrayCamera,
|
||||
ArrowHelper,
|
||||
AttachedBindMode,
|
||||
Audio,
|
||||
AudioAnalyser,
|
||||
AudioContext,
|
||||
AudioListener,
|
||||
AudioLoader,
|
||||
AxesHelper,
|
||||
BackSide,
|
||||
BasicDepthPacking,
|
||||
BasicShadowMap,
|
||||
BatchedMesh,
|
||||
Bone,
|
||||
BooleanKeyframeTrack,
|
||||
Box2,
|
||||
Box3,
|
||||
Box3Helper,
|
||||
BoxGeometry,
|
||||
BoxHelper,
|
||||
BufferAttribute,
|
||||
BufferGeometry,
|
||||
BufferGeometryLoader,
|
||||
ByteType,
|
||||
Cache,
|
||||
Camera,
|
||||
CameraHelper,
|
||||
CanvasTexture,
|
||||
CapsuleGeometry,
|
||||
CatmullRomCurve3,
|
||||
CineonToneMapping,
|
||||
CircleGeometry,
|
||||
ClampToEdgeWrapping,
|
||||
Clock,
|
||||
Color,
|
||||
ColorKeyframeTrack,
|
||||
ColorManagement,
|
||||
CompressedArrayTexture,
|
||||
CompressedCubeTexture,
|
||||
CompressedTexture,
|
||||
CompressedTextureLoader,
|
||||
ConeGeometry,
|
||||
ConstantAlphaFactor,
|
||||
ConstantColorFactor,
|
||||
Controls,
|
||||
CubeCamera,
|
||||
CubeDepthTexture,
|
||||
CubeReflectionMapping,
|
||||
CubeRefractionMapping,
|
||||
CubeTexture,
|
||||
CubeTextureLoader,
|
||||
CubeUVReflectionMapping,
|
||||
CubicBezierCurve,
|
||||
CubicBezierCurve3,
|
||||
CubicInterpolant,
|
||||
CullFaceBack,
|
||||
CullFaceFront,
|
||||
CullFaceFrontBack,
|
||||
CullFaceNone,
|
||||
Curve,
|
||||
CurvePath,
|
||||
CustomBlending,
|
||||
CustomToneMapping,
|
||||
CylinderGeometry,
|
||||
Cylindrical,
|
||||
Data3DTexture,
|
||||
DataArrayTexture,
|
||||
DataTexture,
|
||||
DataTextureLoader,
|
||||
DataUtils,
|
||||
DecrementStencilOp,
|
||||
DecrementWrapStencilOp,
|
||||
DefaultLoadingManager,
|
||||
DepthFormat,
|
||||
DepthStencilFormat,
|
||||
DepthTexture,
|
||||
DetachedBindMode,
|
||||
DirectionalLight,
|
||||
DirectionalLightHelper,
|
||||
DiscreteInterpolant,
|
||||
DodecahedronGeometry,
|
||||
DoubleSide,
|
||||
DstAlphaFactor,
|
||||
DstColorFactor,
|
||||
DynamicCopyUsage,
|
||||
DynamicDrawUsage,
|
||||
DynamicReadUsage,
|
||||
EdgesGeometry,
|
||||
EllipseCurve,
|
||||
EqualCompare,
|
||||
EqualDepth,
|
||||
EqualStencilFunc,
|
||||
EquirectangularReflectionMapping,
|
||||
EquirectangularRefractionMapping,
|
||||
Euler,
|
||||
EventDispatcher,
|
||||
ExternalTexture,
|
||||
ExtrudeGeometry,
|
||||
FileLoader,
|
||||
Float16BufferAttribute,
|
||||
Float32BufferAttribute,
|
||||
FloatType,
|
||||
Fog,
|
||||
FogExp2,
|
||||
FramebufferTexture,
|
||||
FrontSide,
|
||||
Frustum,
|
||||
FrustumArray,
|
||||
GLBufferAttribute,
|
||||
GLSL1,
|
||||
GLSL3,
|
||||
GreaterCompare,
|
||||
GreaterDepth,
|
||||
GreaterEqualCompare,
|
||||
GreaterEqualDepth,
|
||||
GreaterEqualStencilFunc,
|
||||
GreaterStencilFunc,
|
||||
GridHelper,
|
||||
Group,
|
||||
HalfFloatType,
|
||||
HemisphereLight,
|
||||
HemisphereLightHelper,
|
||||
IcosahedronGeometry,
|
||||
ImageBitmapLoader,
|
||||
ImageLoader,
|
||||
ImageUtils,
|
||||
IncrementStencilOp,
|
||||
IncrementWrapStencilOp,
|
||||
InstancedBufferAttribute,
|
||||
InstancedBufferGeometry,
|
||||
InstancedInterleavedBuffer,
|
||||
InstancedMesh,
|
||||
Int16BufferAttribute,
|
||||
Int32BufferAttribute,
|
||||
Int8BufferAttribute,
|
||||
IntType,
|
||||
InterleavedBuffer,
|
||||
InterleavedBufferAttribute,
|
||||
Interpolant,
|
||||
InterpolateDiscrete,
|
||||
InterpolateLinear,
|
||||
InterpolateSmooth,
|
||||
InterpolationSamplingMode,
|
||||
InterpolationSamplingType,
|
||||
InvertStencilOp,
|
||||
KeepStencilOp,
|
||||
KeyframeTrack,
|
||||
LOD,
|
||||
LatheGeometry,
|
||||
Layers,
|
||||
LessCompare,
|
||||
LessDepth,
|
||||
LessEqualCompare,
|
||||
LessEqualDepth,
|
||||
LessEqualStencilFunc,
|
||||
LessStencilFunc,
|
||||
Light,
|
||||
LightProbe,
|
||||
Line,
|
||||
Line3,
|
||||
LineBasicMaterial,
|
||||
LineCurve,
|
||||
LineCurve3,
|
||||
LineDashedMaterial,
|
||||
LineLoop,
|
||||
LineSegments,
|
||||
LinearFilter,
|
||||
LinearInterpolant,
|
||||
LinearMipMapLinearFilter,
|
||||
LinearMipMapNearestFilter,
|
||||
LinearMipmapLinearFilter,
|
||||
LinearMipmapNearestFilter,
|
||||
LinearSRGBColorSpace,
|
||||
LinearToneMapping,
|
||||
LinearTransfer,
|
||||
Loader,
|
||||
LoaderUtils,
|
||||
LoadingManager,
|
||||
LoopOnce,
|
||||
LoopPingPong,
|
||||
LoopRepeat,
|
||||
MOUSE,
|
||||
Material,
|
||||
MaterialLoader,
|
||||
MathUtils,
|
||||
Matrix2,
|
||||
Matrix3,
|
||||
Matrix4,
|
||||
MaxEquation,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
MeshDepthMaterial,
|
||||
MeshDistanceMaterial,
|
||||
MeshLambertMaterial,
|
||||
MeshMatcapMaterial,
|
||||
MeshNormalMaterial,
|
||||
MeshPhongMaterial,
|
||||
MeshPhysicalMaterial,
|
||||
MeshStandardMaterial,
|
||||
MeshToonMaterial,
|
||||
MinEquation,
|
||||
MirroredRepeatWrapping,
|
||||
MixOperation,
|
||||
MultiplyBlending,
|
||||
MultiplyOperation,
|
||||
NearestFilter,
|
||||
NearestMipMapLinearFilter,
|
||||
NearestMipMapNearestFilter,
|
||||
NearestMipmapLinearFilter,
|
||||
NearestMipmapNearestFilter,
|
||||
NeutralToneMapping,
|
||||
NeverCompare,
|
||||
NeverDepth,
|
||||
NeverStencilFunc,
|
||||
NoBlending,
|
||||
NoColorSpace,
|
||||
NoNormalPacking,
|
||||
NoToneMapping,
|
||||
NormalAnimationBlendMode,
|
||||
NormalBlending,
|
||||
NormalGAPacking,
|
||||
NormalRGPacking,
|
||||
NotEqualCompare,
|
||||
NotEqualDepth,
|
||||
NotEqualStencilFunc,
|
||||
NumberKeyframeTrack,
|
||||
Object3D,
|
||||
ObjectLoader,
|
||||
ObjectSpaceNormalMap,
|
||||
OctahedronGeometry,
|
||||
OneFactor,
|
||||
OneMinusConstantAlphaFactor,
|
||||
OneMinusConstantColorFactor,
|
||||
OneMinusDstAlphaFactor,
|
||||
OneMinusDstColorFactor,
|
||||
OneMinusSrcAlphaFactor,
|
||||
OneMinusSrcColorFactor,
|
||||
OrthographicCamera,
|
||||
PCFShadowMap,
|
||||
PCFSoftShadowMap,
|
||||
PMREMGenerator,
|
||||
Path,
|
||||
PerspectiveCamera,
|
||||
Plane,
|
||||
PlaneGeometry,
|
||||
PlaneHelper,
|
||||
PointLight,
|
||||
PointLightHelper,
|
||||
Points,
|
||||
PointsMaterial,
|
||||
PolarGridHelper,
|
||||
PolyhedronGeometry,
|
||||
PositionalAudio,
|
||||
PropertyBinding,
|
||||
PropertyMixer,
|
||||
QuadraticBezierCurve,
|
||||
QuadraticBezierCurve3,
|
||||
Quaternion,
|
||||
QuaternionKeyframeTrack,
|
||||
QuaternionLinearInterpolant,
|
||||
R11_EAC_Format,
|
||||
RED_GREEN_RGTC2_Format,
|
||||
RED_RGTC1_Format,
|
||||
REVISION,
|
||||
RG11_EAC_Format,
|
||||
RGBADepthPacking,
|
||||
RGBAFormat,
|
||||
RGBAIntegerFormat,
|
||||
RGBA_ASTC_10x10_Format,
|
||||
RGBA_ASTC_10x5_Format,
|
||||
RGBA_ASTC_10x6_Format,
|
||||
RGBA_ASTC_10x8_Format,
|
||||
RGBA_ASTC_12x10_Format,
|
||||
RGBA_ASTC_12x12_Format,
|
||||
RGBA_ASTC_4x4_Format,
|
||||
RGBA_ASTC_5x4_Format,
|
||||
RGBA_ASTC_5x5_Format,
|
||||
RGBA_ASTC_6x5_Format,
|
||||
RGBA_ASTC_6x6_Format,
|
||||
RGBA_ASTC_8x5_Format,
|
||||
RGBA_ASTC_8x6_Format,
|
||||
RGBA_ASTC_8x8_Format,
|
||||
RGBA_BPTC_Format,
|
||||
RGBA_ETC2_EAC_Format,
|
||||
RGBA_PVRTC_2BPPV1_Format,
|
||||
RGBA_PVRTC_4BPPV1_Format,
|
||||
RGBA_S3TC_DXT1_Format,
|
||||
RGBA_S3TC_DXT3_Format,
|
||||
RGBA_S3TC_DXT5_Format,
|
||||
RGBDepthPacking,
|
||||
RGBFormat,
|
||||
RGBIntegerFormat,
|
||||
RGB_BPTC_SIGNED_Format,
|
||||
RGB_BPTC_UNSIGNED_Format,
|
||||
RGB_ETC1_Format,
|
||||
RGB_ETC2_Format,
|
||||
RGB_PVRTC_2BPPV1_Format,
|
||||
RGB_PVRTC_4BPPV1_Format,
|
||||
RGB_S3TC_DXT1_Format,
|
||||
RGDepthPacking,
|
||||
RGFormat,
|
||||
RGIntegerFormat,
|
||||
RawShaderMaterial,
|
||||
Ray,
|
||||
Raycaster,
|
||||
RectAreaLight,
|
||||
RedFormat,
|
||||
RedIntegerFormat,
|
||||
ReinhardToneMapping,
|
||||
RenderTarget,
|
||||
RenderTarget3D,
|
||||
RepeatWrapping,
|
||||
ReplaceStencilOp,
|
||||
ReverseSubtractEquation,
|
||||
RingGeometry,
|
||||
SIGNED_R11_EAC_Format,
|
||||
SIGNED_RED_GREEN_RGTC2_Format,
|
||||
SIGNED_RED_RGTC1_Format,
|
||||
SIGNED_RG11_EAC_Format,
|
||||
SRGBColorSpace,
|
||||
SRGBTransfer,
|
||||
Scene,
|
||||
ShaderChunk,
|
||||
ShaderLib,
|
||||
ShaderMaterial,
|
||||
ShadowMaterial,
|
||||
Shape,
|
||||
ShapeGeometry,
|
||||
ShapePath,
|
||||
ShapeUtils,
|
||||
ShortType,
|
||||
Skeleton,
|
||||
SkeletonHelper,
|
||||
SkinnedMesh,
|
||||
Source,
|
||||
Sphere,
|
||||
SphereGeometry,
|
||||
Spherical,
|
||||
SphericalHarmonics3,
|
||||
SplineCurve,
|
||||
SpotLight,
|
||||
SpotLightHelper,
|
||||
Sprite,
|
||||
SpriteMaterial,
|
||||
SrcAlphaFactor,
|
||||
SrcAlphaSaturateFactor,
|
||||
SrcColorFactor,
|
||||
StaticCopyUsage,
|
||||
StaticDrawUsage,
|
||||
StaticReadUsage,
|
||||
StereoCamera,
|
||||
StreamCopyUsage,
|
||||
StreamDrawUsage,
|
||||
StreamReadUsage,
|
||||
StringKeyframeTrack,
|
||||
SubtractEquation,
|
||||
SubtractiveBlending,
|
||||
TOUCH,
|
||||
TangentSpaceNormalMap,
|
||||
TetrahedronGeometry,
|
||||
Texture,
|
||||
TextureLoader,
|
||||
TextureUtils,
|
||||
Timer,
|
||||
TimestampQuery,
|
||||
TorusGeometry,
|
||||
TorusKnotGeometry,
|
||||
Triangle,
|
||||
TriangleFanDrawMode,
|
||||
TriangleStripDrawMode,
|
||||
TrianglesDrawMode,
|
||||
TubeGeometry,
|
||||
UVMapping,
|
||||
Uint16BufferAttribute,
|
||||
Uint32BufferAttribute,
|
||||
Uint8BufferAttribute,
|
||||
Uint8ClampedBufferAttribute,
|
||||
Uniform,
|
||||
UniformsGroup,
|
||||
UniformsLib,
|
||||
UniformsUtils,
|
||||
UnsignedByteType,
|
||||
UnsignedInt101111Type,
|
||||
UnsignedInt248Type,
|
||||
UnsignedInt5999Type,
|
||||
UnsignedIntType,
|
||||
UnsignedShort4444Type,
|
||||
UnsignedShort5551Type,
|
||||
UnsignedShortType,
|
||||
VSMShadowMap,
|
||||
Vector2,
|
||||
Vector3,
|
||||
Vector4,
|
||||
VectorKeyframeTrack,
|
||||
VideoFrameTexture,
|
||||
VideoTexture,
|
||||
WebGL3DRenderTarget,
|
||||
WebGLArrayRenderTarget,
|
||||
WebGLCoordinateSystem,
|
||||
WebGLCubeRenderTarget,
|
||||
WebGLRenderTarget,
|
||||
WebGLRenderer,
|
||||
WebGLUtils,
|
||||
WebGPUCoordinateSystem,
|
||||
WebXRController,
|
||||
WireframeGeometry,
|
||||
WrapAroundEnding,
|
||||
ZeroCurvatureEnding,
|
||||
ZeroFactor,
|
||||
ZeroSlopeEnding,
|
||||
ZeroStencilOp,
|
||||
createCanvasElement,
|
||||
error,
|
||||
getConsoleFunction,
|
||||
log,
|
||||
setConsoleFunction,
|
||||
warn,
|
||||
warnOnce
|
||||
} from "./chunk-INS7YHTD.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export {
|
||||
ACESFilmicToneMapping,
|
||||
AddEquation,
|
||||
AddOperation,
|
||||
AdditiveAnimationBlendMode,
|
||||
AdditiveBlending,
|
||||
AgXToneMapping,
|
||||
AlphaFormat,
|
||||
AlwaysCompare,
|
||||
AlwaysDepth,
|
||||
AlwaysStencilFunc,
|
||||
AmbientLight,
|
||||
AnimationAction,
|
||||
AnimationClip,
|
||||
AnimationLoader,
|
||||
AnimationMixer,
|
||||
AnimationObjectGroup,
|
||||
AnimationUtils,
|
||||
ArcCurve,
|
||||
ArrayCamera,
|
||||
ArrowHelper,
|
||||
AttachedBindMode,
|
||||
Audio,
|
||||
AudioAnalyser,
|
||||
AudioContext,
|
||||
AudioListener,
|
||||
AudioLoader,
|
||||
AxesHelper,
|
||||
BackSide,
|
||||
BasicDepthPacking,
|
||||
BasicShadowMap,
|
||||
BatchedMesh,
|
||||
Bone,
|
||||
BooleanKeyframeTrack,
|
||||
Box2,
|
||||
Box3,
|
||||
Box3Helper,
|
||||
BoxGeometry,
|
||||
BoxHelper,
|
||||
BufferAttribute,
|
||||
BufferGeometry,
|
||||
BufferGeometryLoader,
|
||||
ByteType,
|
||||
Cache,
|
||||
Camera,
|
||||
CameraHelper,
|
||||
CanvasTexture,
|
||||
CapsuleGeometry,
|
||||
CatmullRomCurve3,
|
||||
CineonToneMapping,
|
||||
CircleGeometry,
|
||||
ClampToEdgeWrapping,
|
||||
Clock,
|
||||
Color,
|
||||
ColorKeyframeTrack,
|
||||
ColorManagement,
|
||||
CompressedArrayTexture,
|
||||
CompressedCubeTexture,
|
||||
CompressedTexture,
|
||||
CompressedTextureLoader,
|
||||
ConeGeometry,
|
||||
ConstantAlphaFactor,
|
||||
ConstantColorFactor,
|
||||
Controls,
|
||||
CubeCamera,
|
||||
CubeDepthTexture,
|
||||
CubeReflectionMapping,
|
||||
CubeRefractionMapping,
|
||||
CubeTexture,
|
||||
CubeTextureLoader,
|
||||
CubeUVReflectionMapping,
|
||||
CubicBezierCurve,
|
||||
CubicBezierCurve3,
|
||||
CubicInterpolant,
|
||||
CullFaceBack,
|
||||
CullFaceFront,
|
||||
CullFaceFrontBack,
|
||||
CullFaceNone,
|
||||
Curve,
|
||||
CurvePath,
|
||||
CustomBlending,
|
||||
CustomToneMapping,
|
||||
CylinderGeometry,
|
||||
Cylindrical,
|
||||
Data3DTexture,
|
||||
DataArrayTexture,
|
||||
DataTexture,
|
||||
DataTextureLoader,
|
||||
DataUtils,
|
||||
DecrementStencilOp,
|
||||
DecrementWrapStencilOp,
|
||||
DefaultLoadingManager,
|
||||
DepthFormat,
|
||||
DepthStencilFormat,
|
||||
DepthTexture,
|
||||
DetachedBindMode,
|
||||
DirectionalLight,
|
||||
DirectionalLightHelper,
|
||||
DiscreteInterpolant,
|
||||
DodecahedronGeometry,
|
||||
DoubleSide,
|
||||
DstAlphaFactor,
|
||||
DstColorFactor,
|
||||
DynamicCopyUsage,
|
||||
DynamicDrawUsage,
|
||||
DynamicReadUsage,
|
||||
EdgesGeometry,
|
||||
EllipseCurve,
|
||||
EqualCompare,
|
||||
EqualDepth,
|
||||
EqualStencilFunc,
|
||||
EquirectangularReflectionMapping,
|
||||
EquirectangularRefractionMapping,
|
||||
Euler,
|
||||
EventDispatcher,
|
||||
ExternalTexture,
|
||||
ExtrudeGeometry,
|
||||
FileLoader,
|
||||
Float16BufferAttribute,
|
||||
Float32BufferAttribute,
|
||||
FloatType,
|
||||
Fog,
|
||||
FogExp2,
|
||||
FramebufferTexture,
|
||||
FrontSide,
|
||||
Frustum,
|
||||
FrustumArray,
|
||||
GLBufferAttribute,
|
||||
GLSL1,
|
||||
GLSL3,
|
||||
GreaterCompare,
|
||||
GreaterDepth,
|
||||
GreaterEqualCompare,
|
||||
GreaterEqualDepth,
|
||||
GreaterEqualStencilFunc,
|
||||
GreaterStencilFunc,
|
||||
GridHelper,
|
||||
Group,
|
||||
HalfFloatType,
|
||||
HemisphereLight,
|
||||
HemisphereLightHelper,
|
||||
IcosahedronGeometry,
|
||||
ImageBitmapLoader,
|
||||
ImageLoader,
|
||||
ImageUtils,
|
||||
IncrementStencilOp,
|
||||
IncrementWrapStencilOp,
|
||||
InstancedBufferAttribute,
|
||||
InstancedBufferGeometry,
|
||||
InstancedInterleavedBuffer,
|
||||
InstancedMesh,
|
||||
Int16BufferAttribute,
|
||||
Int32BufferAttribute,
|
||||
Int8BufferAttribute,
|
||||
IntType,
|
||||
InterleavedBuffer,
|
||||
InterleavedBufferAttribute,
|
||||
Interpolant,
|
||||
InterpolateDiscrete,
|
||||
InterpolateLinear,
|
||||
InterpolateSmooth,
|
||||
InterpolationSamplingMode,
|
||||
InterpolationSamplingType,
|
||||
InvertStencilOp,
|
||||
KeepStencilOp,
|
||||
KeyframeTrack,
|
||||
LOD,
|
||||
LatheGeometry,
|
||||
Layers,
|
||||
LessCompare,
|
||||
LessDepth,
|
||||
LessEqualCompare,
|
||||
LessEqualDepth,
|
||||
LessEqualStencilFunc,
|
||||
LessStencilFunc,
|
||||
Light,
|
||||
LightProbe,
|
||||
Line,
|
||||
Line3,
|
||||
LineBasicMaterial,
|
||||
LineCurve,
|
||||
LineCurve3,
|
||||
LineDashedMaterial,
|
||||
LineLoop,
|
||||
LineSegments,
|
||||
LinearFilter,
|
||||
LinearInterpolant,
|
||||
LinearMipMapLinearFilter,
|
||||
LinearMipMapNearestFilter,
|
||||
LinearMipmapLinearFilter,
|
||||
LinearMipmapNearestFilter,
|
||||
LinearSRGBColorSpace,
|
||||
LinearToneMapping,
|
||||
LinearTransfer,
|
||||
Loader,
|
||||
LoaderUtils,
|
||||
LoadingManager,
|
||||
LoopOnce,
|
||||
LoopPingPong,
|
||||
LoopRepeat,
|
||||
MOUSE,
|
||||
Material,
|
||||
MaterialLoader,
|
||||
MathUtils,
|
||||
Matrix2,
|
||||
Matrix3,
|
||||
Matrix4,
|
||||
MaxEquation,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
MeshDepthMaterial,
|
||||
MeshDistanceMaterial,
|
||||
MeshLambertMaterial,
|
||||
MeshMatcapMaterial,
|
||||
MeshNormalMaterial,
|
||||
MeshPhongMaterial,
|
||||
MeshPhysicalMaterial,
|
||||
MeshStandardMaterial,
|
||||
MeshToonMaterial,
|
||||
MinEquation,
|
||||
MirroredRepeatWrapping,
|
||||
MixOperation,
|
||||
MultiplyBlending,
|
||||
MultiplyOperation,
|
||||
NearestFilter,
|
||||
NearestMipMapLinearFilter,
|
||||
NearestMipMapNearestFilter,
|
||||
NearestMipmapLinearFilter,
|
||||
NearestMipmapNearestFilter,
|
||||
NeutralToneMapping,
|
||||
NeverCompare,
|
||||
NeverDepth,
|
||||
NeverStencilFunc,
|
||||
NoBlending,
|
||||
NoColorSpace,
|
||||
NoNormalPacking,
|
||||
NoToneMapping,
|
||||
NormalAnimationBlendMode,
|
||||
NormalBlending,
|
||||
NormalGAPacking,
|
||||
NormalRGPacking,
|
||||
NotEqualCompare,
|
||||
NotEqualDepth,
|
||||
NotEqualStencilFunc,
|
||||
NumberKeyframeTrack,
|
||||
Object3D,
|
||||
ObjectLoader,
|
||||
ObjectSpaceNormalMap,
|
||||
OctahedronGeometry,
|
||||
OneFactor,
|
||||
OneMinusConstantAlphaFactor,
|
||||
OneMinusConstantColorFactor,
|
||||
OneMinusDstAlphaFactor,
|
||||
OneMinusDstColorFactor,
|
||||
OneMinusSrcAlphaFactor,
|
||||
OneMinusSrcColorFactor,
|
||||
OrthographicCamera,
|
||||
PCFShadowMap,
|
||||
PCFSoftShadowMap,
|
||||
PMREMGenerator,
|
||||
Path,
|
||||
PerspectiveCamera,
|
||||
Plane,
|
||||
PlaneGeometry,
|
||||
PlaneHelper,
|
||||
PointLight,
|
||||
PointLightHelper,
|
||||
Points,
|
||||
PointsMaterial,
|
||||
PolarGridHelper,
|
||||
PolyhedronGeometry,
|
||||
PositionalAudio,
|
||||
PropertyBinding,
|
||||
PropertyMixer,
|
||||
QuadraticBezierCurve,
|
||||
QuadraticBezierCurve3,
|
||||
Quaternion,
|
||||
QuaternionKeyframeTrack,
|
||||
QuaternionLinearInterpolant,
|
||||
R11_EAC_Format,
|
||||
RED_GREEN_RGTC2_Format,
|
||||
RED_RGTC1_Format,
|
||||
REVISION,
|
||||
RG11_EAC_Format,
|
||||
RGBADepthPacking,
|
||||
RGBAFormat,
|
||||
RGBAIntegerFormat,
|
||||
RGBA_ASTC_10x10_Format,
|
||||
RGBA_ASTC_10x5_Format,
|
||||
RGBA_ASTC_10x6_Format,
|
||||
RGBA_ASTC_10x8_Format,
|
||||
RGBA_ASTC_12x10_Format,
|
||||
RGBA_ASTC_12x12_Format,
|
||||
RGBA_ASTC_4x4_Format,
|
||||
RGBA_ASTC_5x4_Format,
|
||||
RGBA_ASTC_5x5_Format,
|
||||
RGBA_ASTC_6x5_Format,
|
||||
RGBA_ASTC_6x6_Format,
|
||||
RGBA_ASTC_8x5_Format,
|
||||
RGBA_ASTC_8x6_Format,
|
||||
RGBA_ASTC_8x8_Format,
|
||||
RGBA_BPTC_Format,
|
||||
RGBA_ETC2_EAC_Format,
|
||||
RGBA_PVRTC_2BPPV1_Format,
|
||||
RGBA_PVRTC_4BPPV1_Format,
|
||||
RGBA_S3TC_DXT1_Format,
|
||||
RGBA_S3TC_DXT3_Format,
|
||||
RGBA_S3TC_DXT5_Format,
|
||||
RGBDepthPacking,
|
||||
RGBFormat,
|
||||
RGBIntegerFormat,
|
||||
RGB_BPTC_SIGNED_Format,
|
||||
RGB_BPTC_UNSIGNED_Format,
|
||||
RGB_ETC1_Format,
|
||||
RGB_ETC2_Format,
|
||||
RGB_PVRTC_2BPPV1_Format,
|
||||
RGB_PVRTC_4BPPV1_Format,
|
||||
RGB_S3TC_DXT1_Format,
|
||||
RGDepthPacking,
|
||||
RGFormat,
|
||||
RGIntegerFormat,
|
||||
RawShaderMaterial,
|
||||
Ray,
|
||||
Raycaster,
|
||||
RectAreaLight,
|
||||
RedFormat,
|
||||
RedIntegerFormat,
|
||||
ReinhardToneMapping,
|
||||
RenderTarget,
|
||||
RenderTarget3D,
|
||||
RepeatWrapping,
|
||||
ReplaceStencilOp,
|
||||
ReverseSubtractEquation,
|
||||
RingGeometry,
|
||||
SIGNED_R11_EAC_Format,
|
||||
SIGNED_RED_GREEN_RGTC2_Format,
|
||||
SIGNED_RED_RGTC1_Format,
|
||||
SIGNED_RG11_EAC_Format,
|
||||
SRGBColorSpace,
|
||||
SRGBTransfer,
|
||||
Scene,
|
||||
ShaderChunk,
|
||||
ShaderLib,
|
||||
ShaderMaterial,
|
||||
ShadowMaterial,
|
||||
Shape,
|
||||
ShapeGeometry,
|
||||
ShapePath,
|
||||
ShapeUtils,
|
||||
ShortType,
|
||||
Skeleton,
|
||||
SkeletonHelper,
|
||||
SkinnedMesh,
|
||||
Source,
|
||||
Sphere,
|
||||
SphereGeometry,
|
||||
Spherical,
|
||||
SphericalHarmonics3,
|
||||
SplineCurve,
|
||||
SpotLight,
|
||||
SpotLightHelper,
|
||||
Sprite,
|
||||
SpriteMaterial,
|
||||
SrcAlphaFactor,
|
||||
SrcAlphaSaturateFactor,
|
||||
SrcColorFactor,
|
||||
StaticCopyUsage,
|
||||
StaticDrawUsage,
|
||||
StaticReadUsage,
|
||||
StereoCamera,
|
||||
StreamCopyUsage,
|
||||
StreamDrawUsage,
|
||||
StreamReadUsage,
|
||||
StringKeyframeTrack,
|
||||
SubtractEquation,
|
||||
SubtractiveBlending,
|
||||
TOUCH,
|
||||
TangentSpaceNormalMap,
|
||||
TetrahedronGeometry,
|
||||
Texture,
|
||||
TextureLoader,
|
||||
TextureUtils,
|
||||
Timer,
|
||||
TimestampQuery,
|
||||
TorusGeometry,
|
||||
TorusKnotGeometry,
|
||||
Triangle,
|
||||
TriangleFanDrawMode,
|
||||
TriangleStripDrawMode,
|
||||
TrianglesDrawMode,
|
||||
TubeGeometry,
|
||||
UVMapping,
|
||||
Uint16BufferAttribute,
|
||||
Uint32BufferAttribute,
|
||||
Uint8BufferAttribute,
|
||||
Uint8ClampedBufferAttribute,
|
||||
Uniform,
|
||||
UniformsGroup,
|
||||
UniformsLib,
|
||||
UniformsUtils,
|
||||
UnsignedByteType,
|
||||
UnsignedInt101111Type,
|
||||
UnsignedInt248Type,
|
||||
UnsignedInt5999Type,
|
||||
UnsignedIntType,
|
||||
UnsignedShort4444Type,
|
||||
UnsignedShort5551Type,
|
||||
UnsignedShortType,
|
||||
VSMShadowMap,
|
||||
Vector2,
|
||||
Vector3,
|
||||
Vector4,
|
||||
VectorKeyframeTrack,
|
||||
VideoFrameTexture,
|
||||
VideoTexture,
|
||||
WebGL3DRenderTarget,
|
||||
WebGLArrayRenderTarget,
|
||||
WebGLCoordinateSystem,
|
||||
WebGLCubeRenderTarget,
|
||||
WebGLRenderTarget,
|
||||
WebGLRenderer,
|
||||
WebGLUtils,
|
||||
WebGPUCoordinateSystem,
|
||||
WebXRController,
|
||||
WireframeGeometry,
|
||||
WrapAroundEnding,
|
||||
ZeroCurvatureEnding,
|
||||
ZeroFactor,
|
||||
ZeroSlopeEnding,
|
||||
ZeroStencilOp,
|
||||
createCanvasElement,
|
||||
error,
|
||||
getConsoleFunction,
|
||||
log,
|
||||
setConsoleFunction,
|
||||
warn,
|
||||
warnOnce
|
||||
};
|
||||
7
app/node_modules/.vite/deps/three.js.map
generated
vendored
Normal file
7
app/node_modules/.vite/deps/three.js.map
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
14
app/node_modules/.vite/deps/zustand.js
generated
vendored
Normal file
14
app/node_modules/.vite/deps/zustand.js
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
create,
|
||||
useStore
|
||||
} from "./chunk-QJTQF54Q.js";
|
||||
import {
|
||||
createStore
|
||||
} from "./chunk-LTNRPUSL.js";
|
||||
import "./chunk-ZNKPWGXJ.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export {
|
||||
create,
|
||||
createStore,
|
||||
useStore
|
||||
};
|
||||
7
app/node_modules/.vite/deps/zustand.js.map
generated
vendored
Normal file
7
app/node_modules/.vite/deps/zustand.js.map
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
19
app/node_modules/.vite/deps/zustand_middleware.js
generated
vendored
Normal file
19
app/node_modules/.vite/deps/zustand_middleware.js
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
combine,
|
||||
createJSONStorage,
|
||||
devtools,
|
||||
persist,
|
||||
redux,
|
||||
ssrSafe,
|
||||
subscribeWithSelector
|
||||
} from "./chunk-XGWIEMTH.js";
|
||||
import "./chunk-G3PMV62Z.js";
|
||||
export {
|
||||
combine,
|
||||
createJSONStorage,
|
||||
devtools,
|
||||
persist,
|
||||
redux,
|
||||
subscribeWithSelector,
|
||||
ssrSafe as unstable_ssrSafe
|
||||
};
|
||||
7
app/node_modules/.vite/deps/zustand_middleware.js.map
generated
vendored
Normal file
7
app/node_modules/.vite/deps/zustand_middleware.js.map
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import { Inventory } from '@/components/modules/Inventory';
|
||||
import { Settings } from '@/components/modules/Settings';
|
||||
import { Catalyst } from '@/components/modules/Catalyst';
|
||||
import { CRM } from '@/components/modules/CRM';
|
||||
import { Comms } from '@/components/modules/Comms';
|
||||
import { NotificationCenter } from '@/components/layout/NotificationCenter';
|
||||
import { useCrmBootstrap } from '@/hooks/useCrmBootstrap';
|
||||
import type { ModuleId } from '@/types';
|
||||
@@ -54,7 +53,6 @@ export const MODULE_ROUTES: Array<{
|
||||
{ id: 'inventory', path: '/inventory', title: 'Inventory', component: Inventory },
|
||||
{ id: 'catalyst', path: '/catalyst', title: 'The Catalyst', component: Catalyst },
|
||||
{ id: 'crm', path: '/crm', title: 'CRM', component: CRM },
|
||||
{ id: 'comms', path: '/comms', title: 'Conversations', component: Comms },
|
||||
{ id: 'settings', path: '/settings', title: 'Settings', component: Settings },
|
||||
{ id: 'admin', path: '/admin', title: 'Admin', component: AdminPage, adminOnly: true },
|
||||
];
|
||||
|
||||
@@ -22,13 +22,11 @@ import {
|
||||
X,
|
||||
MessageSquarePlus,
|
||||
Sparkles,
|
||||
Zap,
|
||||
Brain,
|
||||
PanelLeft,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import type { CanvasPage, CanvasPageRevision, MergeRequest, OracleExecutionMode, UserProfile } from '@/oracle/types/canvas';
|
||||
import type { CanvasPage, CanvasPageRevision, MergeRequest, UserProfile } from '@/oracle/types/canvas';
|
||||
import type { ComponentRenderContext } from '@/oracle/components/ComponentRegistry';
|
||||
import { useOraclePage } from '@/oracle/hooks/useOraclePage';
|
||||
import { useOracleExecution } from '@/oracle/hooks/useOracleExecution';
|
||||
@@ -62,12 +60,6 @@ const PROMPT_MODES: Array<{ view: string; label: string; samplePrompt: string; i
|
||||
{ view: 'kpi', label: 'KPI Summary', samplePrompt: 'Give me a KPI summary of total pipeline value today.', icon: BarChart2 },
|
||||
];
|
||||
|
||||
const EXECUTION_MODES: Array<{ value: OracleExecutionMode; label: string; icon: LucideIcon }> = [
|
||||
{ value: 'auto', label: 'Auto', icon: Sparkles },
|
||||
{ value: 'fast', label: 'Fast', icon: Zap },
|
||||
{ value: 'thinking', label: 'Thinking', icon: Brain },
|
||||
];
|
||||
|
||||
const BASE_CTX: ComponentRenderContext = {
|
||||
tenantId: '',
|
||||
actorRole: 'sales_director',
|
||||
@@ -124,7 +116,6 @@ export default function OraclePage() {
|
||||
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [selectedMode, setSelectedMode] = useState(PROMPT_MODES[0]);
|
||||
const [executionMode, setExecutionMode] = useState<OracleExecutionMode>('auto');
|
||||
const [viewDropOpen, setViewDropOpen] = useState(false);
|
||||
const [listening, setListening] = useState(false);
|
||||
const [railOpen, setRailOpen] = useState(false);
|
||||
@@ -217,7 +208,6 @@ export default function OraclePage() {
|
||||
prompt: clean,
|
||||
tenantId: me.tenantId,
|
||||
actorId: me.userId,
|
||||
executionMode,
|
||||
placementMode: me.canvasPreferences.defaultPlacementMode,
|
||||
conversationContext: history.flatMap((entry) => [
|
||||
{ role: 'user' as const, content: entry.execution.prompt },
|
||||
@@ -237,7 +227,7 @@ export default function OraclePage() {
|
||||
}
|
||||
|
||||
await Promise.all([refresh(), loadCanvasSessions()]);
|
||||
}, [prompt, inFlight, page, me, submit, history, executionMode, applyRevision, refresh, loadCanvasSessions]);
|
||||
}, [prompt, inFlight, page, me, submit, history, applyRevision, refresh, loadCanvasSessions]);
|
||||
|
||||
const handleMic = useCallback(() => {
|
||||
const browserWindow = window as Window & {
|
||||
@@ -746,31 +736,6 @@ export default function OraclePage() {
|
||||
{viewDropOpen && <div className="fixed inset-0 z-[998]" onClick={() => setViewDropOpen(false)} />}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex items-center rounded-full p-0.5"
|
||||
style={{ background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.09)' }}
|
||||
>
|
||||
{EXECUTION_MODES.map((mode) => {
|
||||
const active = executionMode === mode.value;
|
||||
return (
|
||||
<button
|
||||
key={mode.value}
|
||||
type="button"
|
||||
onClick={() => setExecutionMode(mode.value)}
|
||||
className="flex h-7 items-center gap-1.5 rounded-full px-2.5 text-xs transition-all"
|
||||
style={{
|
||||
background: active ? 'rgba(59,130,246,0.18)' : 'transparent',
|
||||
color: active ? '#bfdbfe' : 'rgba(255,255,255,0.48)',
|
||||
}}
|
||||
title={mode.value === 'auto' ? 'Let Oracle choose the route' : mode.value === 'fast' ? 'Use Oracle directly' : 'Use Colony orchestration'}
|
||||
>
|
||||
<mode.icon className="h-3 w-3" />
|
||||
<span className={mode.value === 'thinking' ? 'hidden sm:inline' : ''}>{mode.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
id="oracle-rail-toggle"
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Megaphone,
|
||||
Shield,
|
||||
Users,
|
||||
MessageCircle,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { useStore } from '@/store/useStore';
|
||||
@@ -22,7 +21,6 @@ const NAV_ICONS: Record<string, LucideIcon> = {
|
||||
'/sentinel': ScanFace,
|
||||
'/inventory': Building2,
|
||||
'/catalyst': Megaphone,
|
||||
'/comms': MessageCircle,
|
||||
'/settings': Sliders,
|
||||
'/admin': Shield,
|
||||
'/crm': Users,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Zap, TrendingUp, Eye, MousePointerClick, DollarSign,
|
||||
Upload, Play, Image, Film, RefreshCw, ArrowRight, Plus, X,
|
||||
AlertTriangle, ArrowRightLeft, PlusCircle, SlidersHorizontal,
|
||||
Activity, Check, Link2, WandSparkles,
|
||||
Activity, Check, Link2,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
@@ -17,7 +17,6 @@ import { useCurrency } from '@/store/useCurrencyStore';
|
||||
import type { Campaign, MarketingAsset, LiveOptimizationEvent, LiveEventType } from '@/types';
|
||||
import { GroundTruthPicker } from './GroundTruthPicker';
|
||||
import { CatalystMarketingTab } from './CatalystMarketingTab';
|
||||
import { CatalystDreamWeaverTab } from './CatalystDreamWeaverTab';
|
||||
import type { GroundTruthSelection } from './GroundTruthPicker';
|
||||
|
||||
// ── Design tokens ─────────────────────────────────────────────────────────────
|
||||
@@ -918,7 +917,7 @@ function WarRoom() {
|
||||
// Tab Bar
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
type TabId = 'studio' | 'command' | 'intelligence' | 'war-room' | 'marketing' | 'dream-weaver';
|
||||
type TabId = 'studio' | 'command' | 'intelligence' | 'war-room' | 'marketing';
|
||||
|
||||
const TABS: Array<{ id: TabId; label: string; icon: LucideIcon }> = [
|
||||
{ id: 'studio', label: 'The Studio', icon: Clapperboard },
|
||||
@@ -926,7 +925,6 @@ const TABS: Array<{ id: TabId; label: string; icon: LucideIcon }> = [
|
||||
{ id: 'intelligence', label: 'Intelligence & ROI', icon: BarChart3 },
|
||||
{ id: 'war-room', label: 'War Room', icon: Globe },
|
||||
{ id: 'marketing', label: 'Marketing', icon: TrendingUp },
|
||||
{ id: 'dream-weaver', label: 'Dream Weaver', icon: WandSparkles },
|
||||
];
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -942,7 +940,6 @@ export function Catalyst() {
|
||||
'intelligence': <IntelligenceROI />,
|
||||
'war-room': <WarRoom />,
|
||||
'marketing': <CatalystMarketingTab />,
|
||||
'dream-weaver': <CatalystDreamWeaverTab />,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -1001,8 +998,8 @@ export function Catalyst() {
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Live Optimization Feed — hidden on Dream Weaver because generation has its own status surface. */}
|
||||
{activeTab !== 'dream-weaver' && <LiveOptimizationFeed />}
|
||||
{/* Live Optimization Feed — always visible */}
|
||||
<LiveOptimizationFeed />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,524 +0,0 @@
|
||||
import { useEffect, useRef, useState, type ChangeEvent, type CSSProperties } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
AlertTriangle,
|
||||
Check,
|
||||
Download,
|
||||
ExternalLink,
|
||||
Home,
|
||||
Image as ImageIcon,
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
Server,
|
||||
Sparkles,
|
||||
Upload,
|
||||
WandSparkles,
|
||||
} from 'lucide-react';
|
||||
import { useMarketingStore } from '@/store/useMarketingStore';
|
||||
import {
|
||||
DREAM_WEAVER_URL,
|
||||
checkDreamWeaverHealth,
|
||||
fetchDreamWeaverResult,
|
||||
getDreamWeaverStatus,
|
||||
submitDreamWeaverJob,
|
||||
type DreamWeaverHealth,
|
||||
type DreamWeaverJobResponse,
|
||||
type DreamWeaverStatusResponse,
|
||||
} from '@/lib/dreamWeaverApi';
|
||||
|
||||
const GLASS: CSSProperties = {
|
||||
background: 'rgba(8, 10, 18, 0.82)',
|
||||
border: '1px solid rgba(59,130,246,0.14)',
|
||||
backdropFilter: 'blur(24px)',
|
||||
WebkitBackdropFilter: 'blur(24px)',
|
||||
boxShadow: '0 0 0 1px rgba(255,255,255,0.04), 0 4px 32px rgba(0,0,0,0.55)',
|
||||
};
|
||||
|
||||
const INNER: CSSProperties = {
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
border: '1px solid rgba(255,255,255,0.07)',
|
||||
};
|
||||
|
||||
const ROOM_TYPES = [
|
||||
{ id: 'bedroom', label: 'Bedroom' },
|
||||
{ id: 'living_room', label: 'Living Room' },
|
||||
{ id: 'bathroom', label: 'Bathroom' },
|
||||
{ id: 'kitchen', label: 'Kitchen' },
|
||||
{ id: 'dining_room', label: 'Dining Room' },
|
||||
{ id: 'home_office', label: 'Office' },
|
||||
{ id: 'hallway', label: 'Hallway' },
|
||||
{ id: 'balcony', label: 'Balcony' },
|
||||
] as const;
|
||||
|
||||
type ProcessingState = 'idle' | 'checking' | 'submitting' | 'rendering' | 'downloading';
|
||||
|
||||
interface DreamWeaverOutput {
|
||||
id: string;
|
||||
roomLabel: string;
|
||||
keywords: string;
|
||||
imageUrl: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
function sleep(ms: number) {
|
||||
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function isReadyStatus(status: DreamWeaverStatusResponse) {
|
||||
const normalized = status.status?.toLowerCase() ?? '';
|
||||
return Boolean(status.ready) || ['ready', 'completed', 'complete', 'succeeded', 'success', 'finished'].includes(normalized);
|
||||
}
|
||||
|
||||
function isFailedStatus(status: DreamWeaverStatusResponse) {
|
||||
const normalized = status.status?.toLowerCase() ?? '';
|
||||
return ['failed', 'error', 'cancelled', 'canceled'].includes(normalized);
|
||||
}
|
||||
|
||||
function statusLabel(state: ProcessingState, health: DreamWeaverHealth | null) {
|
||||
if (state === 'checking') return 'Checking gateway';
|
||||
if (state === 'submitting') return 'Submitting render';
|
||||
if (state === 'rendering') return 'ComfyUI rendering';
|
||||
if (state === 'downloading') return 'Fetching result';
|
||||
if (!health) return 'Gateway unknown';
|
||||
if (health.online && health.routeMounted && health.comfyuiOnline === false) return 'Gateway online · ComfyUI offline';
|
||||
if (health.online && health.routeMounted && health.comfyuiOnline && health.checkpointReady === false) return 'Gateway online · Model missing';
|
||||
return health.online && health.routeMounted ? 'Gateway online' : 'Gateway offline';
|
||||
}
|
||||
|
||||
function ResultActions({ output }: { output: DreamWeaverOutput }) {
|
||||
function downloadResult() {
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = output.imageUrl;
|
||||
anchor.download = `dream-weaver-${output.id}.png`;
|
||||
anchor.click();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={downloadResult}
|
||||
className="h-9 w-9 rounded-xl flex items-center justify-center transition-colors hover:bg-white/10"
|
||||
style={INNER}
|
||||
title="Download generated image"
|
||||
>
|
||||
<Download className="w-4 h-4 text-white/75" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.open(output.imageUrl, '_blank', 'noopener,noreferrer')}
|
||||
className="h-9 w-9 rounded-xl flex items-center justify-center transition-colors hover:bg-white/10"
|
||||
style={INNER}
|
||||
title="Open generated image"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4 text-white/75" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CatalystDreamWeaverTab() {
|
||||
const { pushLiveEvent } = useMarketingStore();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const objectUrlsRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const [sourceFile, setSourceFile] = useState<File | null>(null);
|
||||
const [sourcePreview, setSourcePreview] = useState<string | null>(null);
|
||||
const [selectedRoomType, setSelectedRoomType] = useState<(typeof ROOM_TYPES)[number]['id']>('bedroom');
|
||||
const [keywords, setKeywords] = useState('');
|
||||
const [health, setHealth] = useState<DreamWeaverHealth | null>(null);
|
||||
const [processingState, setProcessingState] = useState<ProcessingState>('checking');
|
||||
const [progress, setProgress] = useState('Checking Dream Weaver gateway...');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [currentOutput, setCurrentOutput] = useState<DreamWeaverOutput | null>(null);
|
||||
const [history, setHistory] = useState<DreamWeaverOutput[]>([]);
|
||||
|
||||
const isProcessing = processingState !== 'idle' && processingState !== 'checking';
|
||||
const roomLabel = ROOM_TYPES.find((room) => room.id === selectedRoomType)?.label ?? 'Bedroom';
|
||||
|
||||
useEffect(() => {
|
||||
void refreshHealth();
|
||||
return () => {
|
||||
objectUrlsRef.current.forEach((url) => URL.revokeObjectURL(url));
|
||||
objectUrlsRef.current.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
function rememberObjectUrl(blobOrFile: Blob) {
|
||||
const url = URL.createObjectURL(blobOrFile);
|
||||
objectUrlsRef.current.add(url);
|
||||
return url;
|
||||
}
|
||||
|
||||
function setSourceFromFile(file: File) {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
setError('Dream Weaver needs a source room image.');
|
||||
return;
|
||||
}
|
||||
const previewUrl = rememberObjectUrl(file);
|
||||
setSourceFile(file);
|
||||
setSourcePreview(previewUrl);
|
||||
setCurrentOutput(null);
|
||||
setError(null);
|
||||
}
|
||||
|
||||
function handleFileChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
setSourceFromFile(file);
|
||||
}
|
||||
event.target.value = '';
|
||||
}
|
||||
|
||||
async function refreshHealth() {
|
||||
setProcessingState('checking');
|
||||
setProgress('Checking Dream Weaver gateway...');
|
||||
const nextHealth = await checkDreamWeaverHealth();
|
||||
setHealth(nextHealth);
|
||||
setProcessingState('idle');
|
||||
setProgress(nextHealth.online && nextHealth.routeMounted
|
||||
? nextHealth.comfyuiOnline === false
|
||||
? `Gateway is online and the Dream Weaver route is mounted. ComfyUI is offline${nextHealth.comfyuiUrl ? ` at ${nextHealth.comfyuiUrl}` : ''}.`
|
||||
: nextHealth.checkpointReady === false
|
||||
? `ComfyUI is online${nextHealth.comfyuiUrl ? ` at ${nextHealth.comfyuiUrl}` : ''}, but no checkpoint model is installed. Hydrate RealVisXL into ComfyUI/models/checkpoints.`
|
||||
: 'Gateway is online and the Dream Weaver route is mounted.'
|
||||
: nextHealth.detail ?? 'Dream Weaver gateway is not reachable.');
|
||||
}
|
||||
|
||||
async function pollUntilReady(job: DreamWeaverJobResponse) {
|
||||
let latestResultUrl = job.result_url;
|
||||
for (let attempt = 1; attempt <= 150; attempt += 1) {
|
||||
const status = await getDreamWeaverStatus(job);
|
||||
latestResultUrl = status.result_url ?? latestResultUrl;
|
||||
setProgress(status.status ? `Render ${status.status} · poll ${attempt}/150` : `Render queued · poll ${attempt}/150`);
|
||||
|
||||
if (isReadyStatus(status)) {
|
||||
return latestResultUrl;
|
||||
}
|
||||
if (isFailedStatus(status) || status.error) {
|
||||
throw new Error(status.error ?? `Dream Weaver render ${status.status ?? 'failed'}.`);
|
||||
}
|
||||
await sleep(2000);
|
||||
}
|
||||
throw new Error('Dream Weaver timed out after 5 minutes.');
|
||||
}
|
||||
|
||||
async function generate() {
|
||||
if (!sourceFile || isProcessing) return;
|
||||
setError(null);
|
||||
setCurrentOutput(null);
|
||||
|
||||
try {
|
||||
setProcessingState('submitting');
|
||||
setProgress(`Submitting ${roomLabel.toLowerCase()} staging request...`);
|
||||
const job = await submitDreamWeaverJob({
|
||||
image: sourceFile,
|
||||
roomType: selectedRoomType,
|
||||
keywords,
|
||||
});
|
||||
|
||||
setProcessingState('rendering');
|
||||
setProgress(`Job ${job.job_id} accepted. Waiting for ComfyUI output...`);
|
||||
const resultUrl = await pollUntilReady(job);
|
||||
|
||||
setProcessingState('downloading');
|
||||
setProgress('Fetching generated image...');
|
||||
const resultBlob = await fetchDreamWeaverResult(job.job_id, resultUrl);
|
||||
const imageUrl = rememberObjectUrl(resultBlob);
|
||||
const output: DreamWeaverOutput = {
|
||||
id: job.job_id,
|
||||
roomLabel,
|
||||
keywords: keywords.trim(),
|
||||
imageUrl,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
setCurrentOutput(output);
|
||||
setHistory((items) => [output, ...items].slice(0, 6));
|
||||
setProgress('Dream Weaver render complete.');
|
||||
pushLiveEvent({
|
||||
id: `dw-${job.job_id}-${Date.now()}`,
|
||||
type: 'create',
|
||||
campaignName: 'Dream Weaver',
|
||||
message: `${roomLabel} staging render completed.`,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Dream Weaver render failed.';
|
||||
setError(message);
|
||||
setProgress(message);
|
||||
pushLiveEvent({
|
||||
id: `dw-error-${Date.now()}`,
|
||||
type: 'alert',
|
||||
campaignName: 'Dream Weaver',
|
||||
message,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
} finally {
|
||||
setProcessingState('idle');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<motion.div
|
||||
className="relative rounded-2xl p-5 overflow-hidden"
|
||||
style={GLASS}
|
||||
initial={{ opacity: 0, y: 16, scale: 0.97 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{ duration: 0.35, ease: [0.4, 0, 0.2, 1] }}
|
||||
>
|
||||
<div className="absolute inset-x-0 top-0 h-px pointer-events-none"
|
||||
style={{ background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.10), transparent)' }} />
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white mb-0.5 flex items-center gap-2">
|
||||
<WandSparkles className="w-4 h-4 text-blue-400" /> Dream Weaver
|
||||
</h3>
|
||||
<p className="text-xs" style={{ color: 'rgba(148,163,184,0.55)' }}>
|
||||
Room image transformation pipeline using the same Dream Weaver gateway as the iPad app.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 rounded-xl px-3 py-2 text-xs" style={INNER}>
|
||||
<span className={`h-2 w-2 rounded-full ${health?.online && health.routeMounted ? 'bg-green-400' : 'bg-red-400'}`} />
|
||||
<span className="text-white/75">{statusLabel(processingState, health)}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void refreshHealth()}
|
||||
className="h-9 w-9 rounded-xl flex items-center justify-center transition-colors hover:bg-white/10"
|
||||
style={INNER}
|
||||
title="Check Dream Weaver gateway"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 text-white/75 ${processingState === 'checking' ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-[0.95fr_1.05fr] gap-5">
|
||||
<motion.div
|
||||
className="relative rounded-2xl p-5 overflow-hidden"
|
||||
style={GLASS}
|
||||
initial={{ opacity: 0, y: 16, scale: 0.97 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{ duration: 0.35, delay: 0.05, ease: [0.4, 0, 0.2, 1] }}
|
||||
>
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white flex items-center gap-2">
|
||||
<ImageIcon className="w-4 h-4 text-blue-400" /> Source Room
|
||||
</h3>
|
||||
<p className="text-xs mt-1" style={{ color: 'rgba(148,163,184,0.55)' }}>
|
||||
Upload a ground-truth room photograph, choose the target room type, then add optional styling keywords.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
onDragOver={(event) => event.preventDefault()}
|
||||
onDrop={(event) => {
|
||||
event.preventDefault();
|
||||
const file = event.dataTransfer.files?.[0];
|
||||
if (file) setSourceFromFile(file);
|
||||
}}
|
||||
className="relative w-full min-h-[300px] rounded-2xl overflow-hidden flex items-center justify-center text-left transition-colors hover:border-blue-400/40"
|
||||
style={{ ...INNER, background: sourcePreview ? 'rgba(255,255,255,0.02)' : 'rgba(255,255,255,0.025)' }}
|
||||
>
|
||||
{sourcePreview ? (
|
||||
<img src={sourcePreview} alt="Selected source room" className="absolute inset-0 h-full w-full object-cover" />
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3 text-center px-6">
|
||||
<div className="h-12 w-12 rounded-2xl flex items-center justify-center" style={INNER}>
|
||||
<Upload className="h-5 w-5 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">Upload room image</p>
|
||||
<p className="text-xs mt-1" style={{ color: 'rgba(148,163,184,0.55)' }}>
|
||||
Click to browse or drop a photo here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<input ref={fileInputRef} type="file" accept="image/*" className="hidden" onChange={handleFileChange} />
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<p className="text-xs font-medium uppercase tracking-widest mb-2" style={{ color: 'rgba(148,163,184,0.65)' }}>
|
||||
Room type
|
||||
</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
{ROOM_TYPES.map((room) => {
|
||||
const selected = selectedRoomType === room.id;
|
||||
return (
|
||||
<button
|
||||
key={room.id}
|
||||
type="button"
|
||||
onClick={() => setSelectedRoomType(room.id)}
|
||||
className="rounded-xl px-3 py-2 text-sm font-medium flex items-center gap-2 transition-colors"
|
||||
style={{
|
||||
background: selected ? 'rgba(59,130,246,0.18)' : 'rgba(255,255,255,0.04)',
|
||||
border: selected ? '1px solid rgba(59,130,246,0.38)' : '1px solid rgba(255,255,255,0.07)',
|
||||
color: selected ? '#fff' : 'rgba(226,232,240,0.72)',
|
||||
}}
|
||||
>
|
||||
<Home className="w-3.5 h-3.5 text-blue-400" />
|
||||
<span>{room.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-medium uppercase tracking-widest mb-2 block" style={{ color: 'rgba(148,163,184,0.65)' }}>
|
||||
Keywords
|
||||
</label>
|
||||
<textarea
|
||||
value={keywords}
|
||||
onChange={(event) => setKeywords(event.target.value)}
|
||||
placeholder="gold, marble, luxury, soft daylight"
|
||||
rows={3}
|
||||
className="w-full resize-none rounded-xl px-3 py-2 text-sm text-white placeholder-white/20 outline-none focus:border-blue-400/50"
|
||||
style={INNER}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{error && (
|
||||
<motion.div
|
||||
className="rounded-xl p-3 flex items-start gap-2"
|
||||
style={{ background: 'rgba(239,68,68,0.12)', border: '1px solid rgba(239,68,68,0.25)' }}
|
||||
initial={{ opacity: 0, y: -6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -6 }}
|
||||
>
|
||||
<AlertTriangle className="w-4 h-4 text-red-400 mt-0.5 flex-shrink-0" />
|
||||
<p className="text-sm text-red-200 leading-relaxed">{error}</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className="flex items-center justify-between gap-3 rounded-2xl p-3" style={INNER}>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-medium text-white truncate">{progress}</p>
|
||||
<p className="text-[11px] mt-1 truncate" style={{ color: 'rgba(148,163,184,0.5)' }}>
|
||||
Gateway: {DREAM_WEAVER_URL}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void generate()}
|
||||
disabled={!sourceFile || isProcessing || health?.routeMounted === false || health?.comfyuiOnline === false || health?.checkpointReady === false}
|
||||
className="h-11 px-5 rounded-xl text-sm font-semibold flex items-center justify-center gap-2 disabled:opacity-45 disabled:cursor-not-allowed transition-colors"
|
||||
style={{ background: 'hsl(var(--accent))', color: 'hsl(var(--accent-fg))' }}
|
||||
>
|
||||
{isProcessing ? <Loader2 className="w-4 h-4 animate-spin" /> : <Sparkles className="w-4 h-4" />}
|
||||
Generate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="relative rounded-2xl p-5 overflow-hidden"
|
||||
style={GLASS}
|
||||
initial={{ opacity: 0, y: 16, scale: 0.97 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{ duration: 0.35, delay: 0.1, ease: [0.4, 0, 0.2, 1] }}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 mb-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white flex items-center gap-2">
|
||||
<WandSparkles className="w-4 h-4 text-blue-400" /> Generated Staging
|
||||
</h3>
|
||||
<p className="text-xs mt-1" style={{ color: 'rgba(148,163,184,0.55)' }}>
|
||||
The result appears here as soon as the gateway marks the job ready.
|
||||
</p>
|
||||
</div>
|
||||
{currentOutput && <ResultActions output={currentOutput} />}
|
||||
</div>
|
||||
|
||||
<div className="relative min-h-[460px] rounded-2xl overflow-hidden flex items-center justify-center" style={INNER}>
|
||||
{currentOutput ? (
|
||||
<img src={currentOutput.imageUrl} alt={`${currentOutput.roomLabel} Dream Weaver result`} className="absolute inset-0 h-full w-full object-contain bg-black" />
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-3 text-center px-8">
|
||||
<div className="h-14 w-14 rounded-2xl flex items-center justify-center" style={INNER}>
|
||||
{isProcessing ? <Loader2 className="h-6 w-6 text-blue-400 animate-spin" /> : <WandSparkles className="h-6 w-6 text-blue-400" />}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-white">{isProcessing ? 'Dream Weaver is rendering' : 'No generated image yet'}</p>
|
||||
<p className="text-xs mt-1 max-w-md" style={{ color: 'rgba(148,163,184,0.55)' }}>
|
||||
Upload a source image and generate a staging render to populate this canvas.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{history.length > 0 && (
|
||||
<div className="mt-5">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<p className="text-xs font-medium uppercase tracking-widest" style={{ color: 'rgba(148,163,184,0.65)' }}>
|
||||
Recent renders
|
||||
</p>
|
||||
<span className="text-[11px]" style={{ color: 'rgba(148,163,184,0.45)' }}>{history.length}/6</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{history.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => setCurrentOutput(item)}
|
||||
className="group rounded-xl overflow-hidden text-left transition-colors hover:border-blue-400/40"
|
||||
style={INNER}
|
||||
>
|
||||
<div className="aspect-[4/3] bg-black overflow-hidden">
|
||||
<img src={item.imageUrl} alt={item.roomLabel} className="h-full w-full object-cover group-hover:scale-105 transition-transform duration-300" />
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<div className="flex items-center gap-1.5 text-xs font-semibold text-white">
|
||||
<Check className="w-3.5 h-3.5 text-green-400" />
|
||||
{item.roomLabel}
|
||||
</div>
|
||||
<p className="text-[11px] mt-1 truncate" style={{ color: 'rgba(148,163,184,0.55)' }}>
|
||||
{item.keywords || item.createdAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="relative rounded-2xl p-5 overflow-hidden"
|
||||
style={GLASS}
|
||||
initial={{ opacity: 0, y: 16, scale: 0.97 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{ duration: 0.35, delay: 0.15, ease: [0.4, 0, 0.2, 1] }}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h3 className="text-sm font-semibold text-white flex items-center gap-2">
|
||||
<Server className="w-4 h-4 text-blue-400" /> Gateway Contract
|
||||
</h3>
|
||||
<span className="text-xs font-medium" style={{ color: health?.routeMounted ? '#4ade80' : '#f87171' }}>
|
||||
{health?.routeMounted ? 'Route mounted' : 'Route not verified'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
{['POST /dream-weaver', 'GET /dream-weaver/status/{job_id}', 'GET /dream-weaver/result/{job_id}'].map((endpoint) => (
|
||||
<div key={endpoint} className="rounded-xl p-3" style={INNER}>
|
||||
<p className="text-xs font-mono text-blue-200">{endpoint}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,728 +0,0 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
MessageCircle,
|
||||
Search,
|
||||
Send,
|
||||
Phone,
|
||||
PhoneCall,
|
||||
Paperclip,
|
||||
MoreVertical,
|
||||
Link as LinkIcon,
|
||||
User,
|
||||
Settings,
|
||||
Bot,
|
||||
AlertCircle,
|
||||
CheckCheck,
|
||||
Clock,
|
||||
ChevronLeft,
|
||||
Inbox,
|
||||
Voicemail,
|
||||
Plus,
|
||||
Hash,
|
||||
} from 'lucide-react';
|
||||
import { useStore } from '@/store/useStore';
|
||||
import {
|
||||
fetchCommsThreads,
|
||||
fetchCommsMessages,
|
||||
sendCommsMessage,
|
||||
linkCommsThreadToPerson,
|
||||
fetchCommsSettings,
|
||||
} from '@/lib/commsApi';
|
||||
import type {
|
||||
CommsThread,
|
||||
CommsMessage,
|
||||
CommsSettings,
|
||||
SendMessagePayload,
|
||||
} from '@/types/commsTypes';
|
||||
|
||||
/* ── Mock generators for demo / offline mode ─────────────────────────────── */
|
||||
|
||||
function generateMockThreads(): CommsThread[] {
|
||||
const now = new Date().toISOString();
|
||||
return [
|
||||
{
|
||||
threadId: 'mock-1',
|
||||
phoneE164: '+919876543210',
|
||||
displayName: 'Rahul Sharma',
|
||||
channel: 'whatsapp',
|
||||
status: 'open',
|
||||
unreadCount: 2,
|
||||
lastMessageAt: now,
|
||||
lastMessagePreview: 'Is the 3BHK still available?',
|
||||
provider: 'mock',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
crmPerson: {
|
||||
id: 'p1',
|
||||
fullName: 'Rahul Sharma',
|
||||
primaryPhone: '+919876543210',
|
||||
leadStatus: 'hot',
|
||||
projectName: 'Atri Aqua',
|
||||
},
|
||||
},
|
||||
{
|
||||
threadId: 'mock-2',
|
||||
phoneE164: '+919988776655',
|
||||
displayName: 'Unknown Number',
|
||||
channel: 'whatsapp',
|
||||
status: 'open',
|
||||
unreadCount: 1,
|
||||
lastMessageAt: now,
|
||||
lastMessagePreview: 'Send me the brochure please',
|
||||
provider: 'mock',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
{
|
||||
threadId: 'mock-3',
|
||||
phoneE164: '+911122334455',
|
||||
displayName: 'Priya Patel',
|
||||
channel: 'call',
|
||||
status: 'resolved',
|
||||
unreadCount: 0,
|
||||
lastMessageAt: now,
|
||||
provider: 'mock',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
crmPerson: {
|
||||
id: 'p2',
|
||||
fullName: 'Priya Patel',
|
||||
primaryPhone: '+911122334455',
|
||||
leadStatus: 'qualified',
|
||||
projectName: 'Godrej Elevate',
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function generateMockMessages(threadId: string): CommsMessage[] {
|
||||
const now = new Date();
|
||||
const t1 = new Date(now.getTime() - 1000 * 60 * 60 * 2).toISOString();
|
||||
const t2 = new Date(now.getTime() - 1000 * 60 * 30).toISOString();
|
||||
const t3 = new Date(now.getTime() - 1000 * 60 * 5).toISOString();
|
||||
|
||||
if (threadId === 'mock-1') {
|
||||
return [
|
||||
{
|
||||
messageId: 'm1',
|
||||
threadId,
|
||||
direction: 'inbound',
|
||||
messageType: 'text',
|
||||
body: 'Hi, I saw your listing for Atri Aqua. Is the 3BHK still available?',
|
||||
deliveryStatus: 'read',
|
||||
createdAt: t1,
|
||||
provider: 'mock',
|
||||
senderName: 'Rahul Sharma',
|
||||
},
|
||||
{
|
||||
messageId: 'm2',
|
||||
threadId,
|
||||
direction: 'outbound',
|
||||
messageType: 'text',
|
||||
body: 'Yes sir, absolutely. We have a premium corner unit on the 12th floor with marina view.',
|
||||
deliveryStatus: 'read',
|
||||
createdAt: t2,
|
||||
provider: 'mock',
|
||||
},
|
||||
{
|
||||
messageId: 'm3',
|
||||
threadId,
|
||||
direction: 'inbound',
|
||||
messageType: 'text',
|
||||
body: 'What is the final price and can I schedule a visit this weekend?',
|
||||
deliveryStatus: 'delivered',
|
||||
createdAt: t3,
|
||||
provider: 'mock',
|
||||
senderName: 'Rahul Sharma',
|
||||
},
|
||||
];
|
||||
}
|
||||
if (threadId === 'mock-2') {
|
||||
return [
|
||||
{
|
||||
messageId: 'm4',
|
||||
threadId,
|
||||
direction: 'inbound',
|
||||
messageType: 'text',
|
||||
body: 'Send me the brochure please',
|
||||
deliveryStatus: 'delivered',
|
||||
createdAt: t3,
|
||||
provider: 'mock',
|
||||
senderName: 'Unknown',
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/* ── Component ───────────────────────────────────────────────────────────── */
|
||||
|
||||
export function Comms() {
|
||||
useStore();
|
||||
const [threads, setThreads] = useState<CommsThread[]>([]);
|
||||
const [activeThreadId, setActiveThreadId] = useState<string | null>(null);
|
||||
const [messages, setMessages] = useState<CommsMessage[]>([]);
|
||||
const [settings, setSettings] = useState<CommsSettings | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [composerText, setComposerText] = useState('');
|
||||
const [sending, setSending] = useState(false);
|
||||
const [mockMode, setMockMode] = useState(false);
|
||||
const [showCrmRail, setShowCrmRail] = useState(true);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const activeThread = threads.find((t) => t.threadId === activeThreadId) || null;
|
||||
|
||||
useEffect(() => { loadInitial(); }, []);
|
||||
useEffect(() => { if (activeThreadId) loadMessages(activeThreadId); }, [activeThreadId]);
|
||||
useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]);
|
||||
|
||||
async function loadInitial() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [threadsRes, settingsRes] = await Promise.all([
|
||||
fetchCommsThreads({ limit: 50 }),
|
||||
fetchCommsSettings().catch(() => null),
|
||||
]);
|
||||
setThreads(threadsRes.threads || []);
|
||||
setSettings(settingsRes);
|
||||
setMockMode(false);
|
||||
} catch (e) {
|
||||
console.warn('Comms backend unavailable, switching to mock mode', e);
|
||||
setMockMode(true);
|
||||
setThreads(generateMockThreads());
|
||||
setSettings({
|
||||
provider: 'mock',
|
||||
webhookSecretSet: false,
|
||||
autoLinkByPhone: false,
|
||||
createCrmInteractionOnInbound: false,
|
||||
defaultCountryCode: '91',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMessages(threadId: string) {
|
||||
try {
|
||||
if (mockMode) {
|
||||
setMessages(generateMockMessages(threadId));
|
||||
return;
|
||||
}
|
||||
const res = await fetchCommsMessages(threadId, { limit: 100 });
|
||||
setMessages(res.messages || []);
|
||||
} catch (e) {
|
||||
setMessages([]);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSend(e?: React.FormEvent) {
|
||||
e?.preventDefault();
|
||||
if (!composerText.trim() || !activeThreadId) return;
|
||||
|
||||
const payload: SendMessagePayload = {
|
||||
messageType: 'text',
|
||||
body: composerText.trim(),
|
||||
};
|
||||
|
||||
const optimistic: CommsMessage = {
|
||||
messageId: `opt-${Date.now()}`,
|
||||
threadId: activeThreadId,
|
||||
direction: 'outbound',
|
||||
messageType: 'text',
|
||||
body: payload.body,
|
||||
deliveryStatus: 'pending',
|
||||
createdAt: new Date().toISOString(),
|
||||
provider: activeThread?.provider || 'mock',
|
||||
};
|
||||
setMessages((prev) => [...prev, optimistic]);
|
||||
setComposerText('');
|
||||
setSending(true);
|
||||
|
||||
try {
|
||||
if (mockMode) {
|
||||
await new Promise((r) => setTimeout(r, 800));
|
||||
setMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.messageId === optimistic.messageId
|
||||
? { ...m, deliveryStatus: 'sent', messageId: `mock-${Date.now()}` }
|
||||
: m
|
||||
)
|
||||
);
|
||||
} else {
|
||||
await sendCommsMessage(activeThreadId, payload);
|
||||
await loadMessages(activeThreadId);
|
||||
}
|
||||
} catch (err) {
|
||||
setMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.messageId === optimistic.messageId ? { ...m, deliveryStatus: 'failed' } : m
|
||||
)
|
||||
);
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLinkPerson(personId: string) {
|
||||
if (!activeThreadId || mockMode) return;
|
||||
try {
|
||||
await linkCommsThreadToPerson(activeThreadId, { personId });
|
||||
await loadInitial();
|
||||
if (activeThreadId) await loadMessages(activeThreadId);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const filteredThreads = threads.filter((t) => {
|
||||
const q = searchQuery.toLowerCase();
|
||||
return (
|
||||
(t.displayName || '').toLowerCase().includes(q) ||
|
||||
t.phoneE164.includes(q) ||
|
||||
(t.lastMessagePreview || '').toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center text-zinc-400">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-5 h-5 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||
Loading conversations…
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Empty state: provider not configured ──────────────────────────────── */
|
||||
if (!mockMode && (!settings || settings.provider === 'mock')) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div
|
||||
className="max-w-md w-full p-8 rounded-2xl text-center"
|
||||
style={{
|
||||
background: 'hsl(var(--surface))',
|
||||
border: '1px solid hsl(var(--border-subtle))',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="w-14 h-14 rounded-2xl flex items-center justify-center mx-auto mb-5"
|
||||
style={{ background: 'hsl(var(--accent) / 0.1)' }}
|
||||
>
|
||||
<MessageCircle className="w-7 h-7 text-blue-400" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-white mb-2">Conversations</h2>
|
||||
<p className="text-sm text-zinc-400 mb-6">
|
||||
Connect a WhatsApp provider to start receiving and sending messages.
|
||||
Until then, preview the interface with mock data.
|
||||
</p>
|
||||
<div className="flex gap-3 justify-center">
|
||||
<button
|
||||
onClick={() => setMockMode(true)}
|
||||
className="px-4 py-2 rounded-xl text-sm font-medium text-white transition-colors"
|
||||
style={{ background: 'hsl(var(--surface-2))', border: '1px solid hsl(var(--border-subtle))' }}
|
||||
>
|
||||
<Bot className="w-4 h-4 inline mr-2" />
|
||||
Preview Mock Data
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { /* router push to settings */ }}
|
||||
className="px-4 py-2 rounded-xl text-sm font-medium text-white transition-colors"
|
||||
style={{ background: 'hsl(var(--accent))' }}
|
||||
>
|
||||
<Settings className="w-4 h-4 inline mr-2" />
|
||||
Configure Provider
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex gap-4" style={{ minHeight: 0 }}>
|
||||
{/* ════════════════════════════════════════════════════════════════════
|
||||
LEFT: Inbox
|
||||
════════════════════════════════════════════════════════════════════ */}
|
||||
<aside
|
||||
className="flex-none flex flex-col rounded-2xl overflow-hidden"
|
||||
style={{
|
||||
width: 320,
|
||||
background: 'hsl(var(--surface))',
|
||||
border: '1px solid hsl(var(--border-subtle))',
|
||||
}}
|
||||
>
|
||||
<div className="p-4 border-b" style={{ borderColor: 'hsl(var(--border-subtle))' }}>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-sm font-semibold text-white">Inbox</h2>
|
||||
{mockMode && (
|
||||
<span className="tag text-amber-400 border-amber-500/20 bg-amber-500/10">
|
||||
Mock Mode
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-500" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search threads…"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-9 pr-3 py-2 rounded-xl text-sm text-white placeholder-zinc-500 outline-none"
|
||||
style={{
|
||||
background: 'hsl(var(--surface-2))',
|
||||
border: '1px solid hsl(var(--border-subtle))',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
||||
{filteredThreads.length === 0 ? (
|
||||
<div className="p-8 text-center text-zinc-500 text-sm">No threads found</div>
|
||||
) : (
|
||||
filteredThreads.map((thread) => (
|
||||
<button
|
||||
key={thread.threadId}
|
||||
onClick={() => setActiveThreadId(thread.threadId)}
|
||||
className="w-full text-left px-4 py-3 transition-colors relative"
|
||||
style={{
|
||||
background: activeThreadId === thread.threadId ? 'hsl(var(--accent) / 0.08)' : 'transparent',
|
||||
borderLeft: activeThreadId === thread.threadId ? '2px solid hsl(var(--accent))' : '2px solid transparent',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
className="w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold flex-shrink-0"
|
||||
style={{
|
||||
background: thread.crmPerson ? 'hsl(var(--accent) / 0.15)' : 'hsl(var(--surface-3))',
|
||||
color: thread.crmPerson ? 'hsl(var(--accent))' : 'hsl(var(--muted-fg))',
|
||||
}}
|
||||
>
|
||||
{thread.displayName?.[0]?.toUpperCase() || <User className="w-4 h-4" />}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-white truncate">
|
||||
{thread.displayName || thread.phoneE164}
|
||||
</span>
|
||||
{thread.unreadCount > 0 && (
|
||||
<span className="ml-2 px-1.5 py-0.5 rounded-md text-[10px] font-bold bg-blue-500 text-white">
|
||||
{thread.unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-zinc-400 truncate mt-0.5">{thread.lastMessagePreview || 'No messages'}</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-[10px] text-zinc-500">{thread.phoneE164}</span>
|
||||
{!thread.personId && (
|
||||
<span className="text-[10px] px-1.5 rounded bg-amber-500/10 text-amber-400 border border-amber-500/20">
|
||||
Unresolved
|
||||
</span>
|
||||
)}
|
||||
{thread.channel === 'call' && <Phone className="w-3 h-3 text-zinc-500" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* ════════════════════════════════════════════════════════════════════
|
||||
CENTER: Conversation Timeline
|
||||
════════════════════════════════════════════════════════════════════ */}
|
||||
<section
|
||||
className="flex-1 flex flex-col rounded-2xl overflow-hidden"
|
||||
style={{
|
||||
background: 'hsl(var(--surface))',
|
||||
border: '1px solid hsl(var(--border-subtle))',
|
||||
}}
|
||||
>
|
||||
{activeThread ? (
|
||||
<>
|
||||
{/* ── Client Identity Strip ── */}
|
||||
<div
|
||||
className="flex-none px-5 py-3 flex items-center justify-between border-b"
|
||||
style={{ borderColor: 'hsl(var(--border-subtle))' }}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold"
|
||||
style={{
|
||||
background: activeThread.crmPerson ? 'hsl(var(--accent) / 0.15)' : 'hsl(var(--surface-3))',
|
||||
color: activeThread.crmPerson ? 'hsl(var(--accent))' : 'hsl(var(--muted-fg))',
|
||||
}}
|
||||
>
|
||||
{activeThread.displayName?.[0]?.toUpperCase() || <User className="w-4 h-4" />}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-semibold text-white">
|
||||
{activeThread.displayName || activeThread.phoneE164}
|
||||
</h3>
|
||||
{activeThread.channel === 'whatsapp' && (
|
||||
<span className="tag text-emerald-400 border-emerald-500/20 bg-emerald-500/10 text-[10px]">
|
||||
WhatsApp
|
||||
</span>
|
||||
)}
|
||||
{activeThread.channel === 'call' && (
|
||||
<span className="tag text-blue-300 border-blue-400/20 bg-blue-400/10 text-[10px]">
|
||||
Call
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-zinc-400">{activeThread.phoneE164}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="p-2 rounded-lg hover:bg-white/5 text-zinc-400 hover:text-white transition-colors">
|
||||
<PhoneCall className="w-4 h-4" />
|
||||
</button>
|
||||
<button className="p-2 rounded-lg hover:bg-white/5 text-zinc-400 hover:text-white transition-colors">
|
||||
<MoreVertical className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCrmRail((v) => !v)}
|
||||
className="p-2 rounded-lg hover:bg-white/5 text-zinc-400 hover:text-white transition-colors"
|
||||
title="Toggle CRM rail"
|
||||
>
|
||||
<ChevronLeft className={`w-4 h-4 transition-transform ${showCrmRail ? '' : 'rotate-180'}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Messages ── */}
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar px-5 py-4 space-y-4">
|
||||
<AnimatePresence initial={false}>
|
||||
{messages.map((msg) => (
|
||||
<motion.div
|
||||
key={msg.messageId}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className={`flex ${msg.direction === 'outbound' ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[70%] px-4 py-2.5 rounded-2xl text-sm ${
|
||||
msg.direction === 'outbound' ? 'rounded-br-md' : 'rounded-bl-md'
|
||||
}`}
|
||||
style={{
|
||||
background:
|
||||
msg.direction === 'outbound'
|
||||
? 'hsl(var(--accent) / 0.9)'
|
||||
: 'hsl(var(--surface-3))',
|
||||
color: msg.direction === 'outbound' ? '#fff' : 'hsl(var(--foreground))',
|
||||
}}
|
||||
>
|
||||
{msg.messageType === 'text' && <p className="leading-relaxed">{msg.body}</p>}
|
||||
{msg.mediaUrl && (
|
||||
<div className="mt-2 rounded-lg overflow-hidden">
|
||||
<img src={msg.mediaUrl} alt="media" className="max-w-full max-h-48 object-cover" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-end gap-1.5 mt-1.5">
|
||||
<span className="text-[10px] opacity-60">
|
||||
{new Date(msg.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
{msg.direction === 'outbound' && (
|
||||
<>
|
||||
{msg.deliveryStatus === 'pending' && <Clock className="w-3 h-3 opacity-60" />}
|
||||
{msg.deliveryStatus === 'sent' && <CheckCheck className="w-3 h-3 opacity-60" />}
|
||||
{msg.deliveryStatus === 'delivered' && <CheckCheck className="w-3 h-3 text-blue-200" />}
|
||||
{msg.deliveryStatus === 'read' && <CheckCheck className="w-3 h-3 text-emerald-300" />}
|
||||
{msg.deliveryStatus === 'failed' && <AlertCircle className="w-3 h-3 text-red-300" />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* ── Composer ── */}
|
||||
<div
|
||||
className="flex-none px-4 py-3 border-t"
|
||||
style={{ borderColor: 'hsl(var(--border-subtle))' }}
|
||||
>
|
||||
{!activeThread.personId && (
|
||||
<div
|
||||
className="mb-3 px-3 py-2 rounded-xl flex items-center gap-2 text-xs"
|
||||
style={{
|
||||
background: 'hsl(38 92% 50% / 0.08)',
|
||||
border: '1px solid hsl(38 92% 50% / 0.2)',
|
||||
color: 'hsl(38 92% 65%)',
|
||||
}}
|
||||
>
|
||||
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
||||
<span className="flex-1">This number is not linked to a CRM contact.</span>
|
||||
<button
|
||||
onClick={() => handleLinkPerson('demo-person-id')}
|
||||
className="px-2 py-1 rounded-md bg-amber-500/10 hover:bg-amber-500/20 transition-colors font-medium"
|
||||
>
|
||||
Link
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSend} className="flex items-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="p-2.5 rounded-xl text-zinc-400 hover:text-white hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<Paperclip className="w-5 h-5" />
|
||||
</button>
|
||||
<div className="flex-1 relative">
|
||||
<textarea
|
||||
rows={1}
|
||||
value={composerText}
|
||||
onChange={(e) => setComposerText(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
}}
|
||||
placeholder="Type a message…"
|
||||
className="w-full px-4 py-2.5 rounded-xl text-sm text-white placeholder-zinc-500 outline-none resize-none max-h-32"
|
||||
style={{
|
||||
background: 'hsl(var(--surface-2))',
|
||||
border: '1px solid hsl(var(--border-subtle))',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!composerText.trim() || sending}
|
||||
className="p-2.5 rounded-xl text-white transition-all disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
style={{ background: 'hsl(var(--accent))' }}
|
||||
>
|
||||
<Send className="w-5 h-5" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col items-center justify-center text-zinc-500">
|
||||
<Inbox className="w-12 h-12 mb-4 opacity-20" />
|
||||
<p className="text-sm">Select a conversation to start</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* ════════════════════════════════════════════════════════════════════
|
||||
RIGHT: CRM Intelligence Rail
|
||||
════════════════════════════════════════════════════════════════════ */}
|
||||
<AnimatePresence>
|
||||
{showCrmRail && activeThread && (
|
||||
<motion.aside
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 20 }}
|
||||
className="flex-none flex flex-col rounded-2xl overflow-hidden"
|
||||
style={{
|
||||
width: 300,
|
||||
background: 'hsl(var(--surface))',
|
||||
border: '1px solid hsl(var(--border-subtle))',
|
||||
}}
|
||||
>
|
||||
<div className="p-4 border-b" style={{ borderColor: 'hsl(var(--border-subtle))' }}>
|
||||
<h3 className="text-sm font-semibold text-white">CRM Intelligence</h3>
|
||||
<p className="text-xs text-zinc-500 mt-0.5">Velocity Lens · {activeThread.phoneE164}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-4 space-y-4">
|
||||
{activeThread.crmPerson ? (
|
||||
<>
|
||||
<div
|
||||
className="p-4 rounded-xl"
|
||||
style={{ background: 'hsl(var(--surface-2))', border: '1px solid hsl(var(--border-subtle))' }}
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div
|
||||
className="w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold"
|
||||
style={{ background: 'hsl(var(--accent) / 0.15)', color: 'hsl(var(--accent))' }}
|
||||
>
|
||||
{activeThread.crmPerson.fullName?.[0] || 'C'}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-white">{activeThread.crmPerson.fullName || 'Unknown'}</p>
|
||||
<p className="text-xs text-zinc-400">{activeThread.crmPerson.primaryEmail || 'No email'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-zinc-500">Status</span>
|
||||
<span className="text-white capitalize">{activeThread.crmPerson.leadStatus || 'New'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-zinc-500">Project</span>
|
||||
<span className="text-white">{activeThread.crmPerson.projectName || '-'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-zinc-500">Buyer Type</span>
|
||||
<span className="text-white">{activeThread.crmPerson.buyerType || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="p-4 rounded-xl"
|
||||
style={{ background: 'hsl(var(--surface-2))', border: '1px solid hsl(var(--border-subtle))' }}
|
||||
>
|
||||
<h4 className="text-xs font-semibold text-zinc-300 uppercase tracking-wider mb-3">Next Best Action</h4>
|
||||
<p className="text-sm text-white font-medium">Schedule site visit</p>
|
||||
<p className="text-xs text-zinc-400 mt-1">High intent signal detected</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="p-4 rounded-xl"
|
||||
style={{ background: 'hsl(var(--surface-2))', border: '1px solid hsl(var(--border-subtle))' }}
|
||||
>
|
||||
<h4 className="text-xs font-semibold text-zinc-300 uppercase tracking-wider mb-3">Quick Actions</h4>
|
||||
<div className="space-y-2">
|
||||
<button className="w-full text-left px-3 py-2 rounded-lg text-xs text-white hover:bg-white/5 transition-colors flex items-center gap-2">
|
||||
<Phone className="w-3.5 h-3.5 text-zinc-400" />
|
||||
Log Call
|
||||
</button>
|
||||
<button className="w-full text-left px-3 py-2 rounded-lg text-xs text-white hover:bg-white/5 transition-colors flex items-center gap-2">
|
||||
<Voicemail className="w-3.5 h-3.5 text-zinc-400" />
|
||||
Add Recording
|
||||
</button>
|
||||
<button className="w-full text-left px-3 py-2 rounded-lg text-xs text-white hover:bg-white/5 transition-colors flex items-center gap-2">
|
||||
<Plus className="w-3.5 h-3.5 text-zinc-400" />
|
||||
Create Task
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className="p-4 rounded-xl text-center"
|
||||
style={{ background: 'hsl(var(--surface-2))', border: '1px solid hsl(var(--border-subtle))' }}
|
||||
>
|
||||
<Hash className="w-8 h-8 text-amber-500/60 mx-auto mb-3" />
|
||||
<p className="text-sm text-white font-medium mb-1">Unresolved Number</p>
|
||||
<p className="text-xs text-zinc-400 mb-4">
|
||||
No CRM match found for {activeThread.phoneE164}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => handleLinkPerson('demo-person-id')}
|
||||
className="w-full px-3 py-2 rounded-lg text-xs font-medium text-white transition-colors"
|
||||
style={{ background: 'hsl(var(--accent))' }}
|
||||
>
|
||||
<LinkIcon className="w-3.5 h-3.5 inline mr-1.5" />
|
||||
Link to Contact
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.aside>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState, type ChangeEvent } from 'react';
|
||||
import { useRef, useState, type ChangeEvent } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
User,
|
||||
@@ -16,15 +16,12 @@ import {
|
||||
ChevronDown,
|
||||
LogOut,
|
||||
Pencil,
|
||||
MessageCircle,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { useStore } from '@/store/useStore';
|
||||
import { useCurrency, CURRENCY_OPTIONS } from '@/store/useCurrencyStore';
|
||||
import type { CurrencyCode } from '@/store/useCurrencyStore';
|
||||
import { API_URL } from '@/lib/api';
|
||||
import { fetchCommsSettings, testCommsProviderConnection, updateCommsSettings } from '@/lib/commsApi';
|
||||
import type { CommsProvider, CommsSettings } from '@/types/commsTypes';
|
||||
import {
|
||||
clearVelocityToken,
|
||||
getVelocityToken,
|
||||
@@ -616,160 +613,6 @@ function DisplaySettings() {
|
||||
}
|
||||
|
||||
// ── Data & Privacy ───────────────────────────────────────────────────────────
|
||||
function CommunicationsSettings() {
|
||||
const [settings, setSettings] = useState<CommsSettings | null>(null);
|
||||
const [draft, setDraft] = useState<Partial<CommsSettings>>({});
|
||||
const [statusText, setStatusText] = useState('Loading provider settings...');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const current: CommsSettings = {
|
||||
provider: 'mock',
|
||||
providerBaseUrl: '',
|
||||
providerApiKey: '',
|
||||
instanceId: '',
|
||||
phoneNumberId: '',
|
||||
webhookCallbackUrl: '/api/comms/webhooks/{provider}',
|
||||
webhookSecretSet: false,
|
||||
autoLinkByPhone: true,
|
||||
createCrmInteractionOnInbound: true,
|
||||
defaultCountryCode: '91',
|
||||
transcriptionProvider: 'none',
|
||||
...(settings ?? {}),
|
||||
...draft,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void fetchCommsSettings()
|
||||
.then((value) => {
|
||||
if (cancelled) return;
|
||||
setSettings(value);
|
||||
setStatusText('Settings loaded from backend.');
|
||||
})
|
||||
.catch((error) => {
|
||||
if (cancelled) return;
|
||||
setStatusText(error instanceof Error ? error.message : 'Unable to load comms settings.');
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const update = <K extends keyof CommsSettings>(key: K, value: CommsSettings[K]) => {
|
||||
setDraft((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await updateCommsSettings(draft);
|
||||
const latest = await fetchCommsSettings();
|
||||
setSettings(latest);
|
||||
setDraft({});
|
||||
setStatusText('Communications settings saved.');
|
||||
} catch (error) {
|
||||
setStatusText(error instanceof Error ? error.message : 'Failed to save communications settings.');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const test = async () => {
|
||||
try {
|
||||
const result = await testCommsProviderConnection();
|
||||
setStatusText(result.message || (result.success ? 'Provider connection succeeded.' : 'Provider connection failed.'));
|
||||
} catch (error) {
|
||||
setStatusText(error instanceof Error ? error.message : 'Provider test failed.');
|
||||
}
|
||||
};
|
||||
|
||||
const fieldClass = "w-64 rounded-xl px-3 py-2 text-sm text-white placeholder-zinc-500 outline-none";
|
||||
|
||||
return (
|
||||
<GlassCard delay={0.3}>
|
||||
<SectionHeader icon={MessageCircle} title="Communications" accent="#22d3ee" />
|
||||
<div>
|
||||
<SettingsRow label="Provider" description="Mock is local preview. WAHA and Evolution require a running provider service.">
|
||||
<DarkSelect
|
||||
value={current.provider}
|
||||
onChange={(v) => update('provider', v as CommsProvider)}
|
||||
options={[
|
||||
{ value: 'mock', label: 'Mock' },
|
||||
{ value: 'waha', label: 'WAHA' },
|
||||
{ value: 'evolution', label: 'Evolution API' },
|
||||
{ value: 'meta_cloud', label: 'Meta Cloud API' },
|
||||
]}
|
||||
/>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Provider Base URL" description="Internal or public base URL for WAHA/Evolution.">
|
||||
<input
|
||||
className={fieldClass}
|
||||
style={INNER_SURFACE}
|
||||
value={current.providerBaseUrl ?? ''}
|
||||
onChange={(event) => update('providerBaseUrl', event.target.value)}
|
||||
placeholder="http://127.0.0.1:3000"
|
||||
/>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="API Key" description="Stored in backend comms settings. Masked when read back.">
|
||||
<input
|
||||
className={fieldClass}
|
||||
style={INNER_SURFACE}
|
||||
type="password"
|
||||
value={current.providerApiKey ?? ''}
|
||||
onChange={(event) => update('providerApiKey', event.target.value)}
|
||||
placeholder="Provider API key"
|
||||
/>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Instance / Session" description="WAHA session name or Evolution instance name.">
|
||||
<input
|
||||
className={fieldClass}
|
||||
style={INNER_SURFACE}
|
||||
value={current.instanceId ?? ''}
|
||||
onChange={(event) => update('instanceId', event.target.value)}
|
||||
placeholder="default"
|
||||
/>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Webhook URL" description="Point provider inbound webhooks here.">
|
||||
<span className="text-xs font-mono text-zinc-300">{current.webhookCallbackUrl || '/api/comms/webhooks/{provider}'}</span>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Auto-link by Phone" description="Match inbound numbers to crm_people.primary_phone.">
|
||||
<Toggle enabled={Boolean(current.autoLinkByPhone)} onChange={(v) => update('autoLinkByPhone', v)} />
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Create CRM Interaction" description="Mirror inbound messages into canonical intelligence tables.">
|
||||
<Toggle enabled={Boolean(current.createCrmInteractionOnInbound)} onChange={(v) => update('createCrmInteractionOnInbound', v)} />
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Transcription Provider" description="Recording intake is stored now; transcription worker can be added later.">
|
||||
<DarkSelect
|
||||
value={current.transcriptionProvider ?? 'none'}
|
||||
onChange={(v) => update('transcriptionProvider', v as CommsSettings['transcriptionProvider'])}
|
||||
options={[
|
||||
{ value: 'none', label: 'None' },
|
||||
{ value: 'local', label: 'Local Whisper' },
|
||||
{ value: 'openai', label: 'OpenAI' },
|
||||
]}
|
||||
/>
|
||||
</SettingsRow>
|
||||
<div className="px-6 py-4 flex items-center justify-between gap-3">
|
||||
<p className="text-xs text-zinc-400">{statusText}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<GhostButton onClick={test}>Test</GhostButton>
|
||||
<motion.button
|
||||
type="button"
|
||||
onClick={save}
|
||||
disabled={saving || Object.keys(draft).length === 0}
|
||||
className="px-4 py-2 rounded-xl text-sm font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style={{ background: 'hsl(var(--accent))', color: 'hsl(var(--accent-fg))' }}
|
||||
whileHover={{ scale: saving ? 1 : 1.02 }}
|
||||
whileTap={{ scale: saving ? 1 : 0.97 }}
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
);
|
||||
}
|
||||
|
||||
function DataSettings() {
|
||||
const [retention, setRetention] = useState('90');
|
||||
const { leads, messages, units, status } = useStore();
|
||||
@@ -885,14 +728,9 @@ export function Settings() {
|
||||
<DisplaySettings />
|
||||
</div>
|
||||
|
||||
{/* Row 4: Communications + Data */}
|
||||
{/* Row 4: Data + About */}
|
||||
<div className="grid grid-cols-2 gap-4 relative z-10">
|
||||
<CommunicationsSettings />
|
||||
<DataSettings />
|
||||
</div>
|
||||
|
||||
{/* Row 5: About */}
|
||||
<div className="grid grid-cols-1 gap-4 relative z-0">
|
||||
<AboutSection />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { buildVelocityHeaders } from '@/lib/velocitySession';
|
||||
|
||||
const rawApiBase = import.meta.env.VITE_API_URL?.trim();
|
||||
const DEPLOYED_BACKEND_ORIGIN = 'https://velocity.desineuron.in';
|
||||
|
||||
@@ -77,17 +75,10 @@ export interface MarketingCampaignSummary {
|
||||
|
||||
async function requestJson<T>(path: string): Promise<T> {
|
||||
const response = await fetch(`${API_URL}${path}`, {
|
||||
headers: buildVelocityHeaders(undefined, false),
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
typeof body?.detail === 'string'
|
||||
? body.detail
|
||||
: typeof body?.message === 'string'
|
||||
? body.message
|
||||
: `Request failed: ${response.status}`,
|
||||
);
|
||||
throw new Error(`Request failed: ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import { getVelocityToken } from './velocityPlatformClient';
|
||||
import type {
|
||||
CommsThread,
|
||||
CommsSettings,
|
||||
CommsProviderTestResult,
|
||||
SendMessagePayload,
|
||||
CommsThreadListResponse,
|
||||
CommsMessageListResponse,
|
||||
ThreadLinkPayload,
|
||||
} from '@/types/commsTypes';
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE_URL || '';
|
||||
|
||||
async function commsFetch(path: string, options?: RequestInit) {
|
||||
const token = getVelocityToken();
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.detail || `HTTP ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function toQuery(params?: Record<string, string | number | undefined>) {
|
||||
const query = new URLSearchParams();
|
||||
Object.entries(params ?? {}).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== '') query.set(key, String(value));
|
||||
});
|
||||
const serialized = query.toString();
|
||||
return serialized ? `?${serialized}` : '';
|
||||
}
|
||||
|
||||
export const fetchCommsThreads = (params?: { status?: string; search?: string; limit?: number; offset?: number }) =>
|
||||
commsFetch(`/api/comms/threads${toQuery(params)}`) as Promise<CommsThreadListResponse>;
|
||||
|
||||
export const fetchCommsThread = (threadId: string) =>
|
||||
commsFetch(`/api/comms/threads/${threadId}`) as Promise<CommsThread>;
|
||||
|
||||
export const fetchCommsMessages = (threadId: string, params?: { limit?: number; offset?: number }) =>
|
||||
commsFetch(`/api/comms/threads/${threadId}/messages${toQuery(params)}`) as Promise<CommsMessageListResponse>;
|
||||
|
||||
export const sendCommsMessage = (threadId: string, payload: SendMessagePayload) =>
|
||||
commsFetch(`/api/comms/threads/${threadId}/messages`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
export const linkCommsThreadToPerson = (threadId: string, payload: ThreadLinkPayload) =>
|
||||
commsFetch(`/api/comms/threads/${threadId}/link-person`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
export const addCommsThreadNote = (threadId: string, body: { content: string }) =>
|
||||
commsFetch(`/api/comms/threads/${threadId}/notes`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
export const addCommsThreadTask = (threadId: string, body: { title: string; dueAt?: string }) =>
|
||||
commsFetch(`/api/comms/threads/${threadId}/tasks`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
export const fetchCommsSettings = () =>
|
||||
commsFetch(`/api/comms/settings`) as Promise<CommsSettings>;
|
||||
|
||||
export const updateCommsSettings = (payload: Partial<CommsSettings>) =>
|
||||
commsFetch(`/api/comms/settings`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
export const testCommsProviderConnection = () =>
|
||||
commsFetch(`/api/comms/provider/test`, { method: 'POST' }) as Promise<CommsProviderTestResult>;
|
||||
|
||||
export const transcribeCommsRecording = (callId: string) =>
|
||||
commsFetch(`/api/comms/recordings/transcribe`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ callId }),
|
||||
});
|
||||
@@ -8,7 +8,6 @@ import type {
|
||||
Client360Snapshot,
|
||||
CrmOpportunityCard,
|
||||
CrmTask,
|
||||
CrmLeadStageUpdate,
|
||||
KanbanColumn,
|
||||
ImportBatchSummary,
|
||||
ImportProposal,
|
||||
@@ -18,12 +17,13 @@ import type {
|
||||
OracleClientDataDetail,
|
||||
OracleClientTimelineItem,
|
||||
} from '@/types/crmTypes';
|
||||
import { buildVelocityHeaders } from '@/lib/velocitySession';
|
||||
import { VELOCITY_TOKEN_KEY } from '@/lib/velocityPlatformClient';
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? '';
|
||||
|
||||
function getAuthHeaders(): Record<string, string> {
|
||||
return Object.fromEntries(buildVelocityHeaders(undefined, false).entries());
|
||||
const token = localStorage.getItem(VELOCITY_TOKEN_KEY);
|
||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||
}
|
||||
|
||||
async function apiFetch<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
@@ -90,23 +90,6 @@ export async function fetchOpportunities(params?: {
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function updateOpportunity(body: {
|
||||
opportunity_id: string;
|
||||
stage?: string;
|
||||
value?: number | null;
|
||||
probability?: number | null;
|
||||
expected_close_date?: string | null;
|
||||
next_action?: string | null;
|
||||
notes?: string | null;
|
||||
}): Promise<CrmOpportunityCard> {
|
||||
const { opportunity_id, ...payload } = body;
|
||||
const res = await apiFetch<{ status: string; data: CrmOpportunityCard }>(`/api/crm/opportunities/${opportunity_id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
// ── Tasks ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function fetchTasks(params?: {
|
||||
@@ -138,23 +121,6 @@ export async function createTask(body: {
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function updateTask(body: {
|
||||
reminder_id: string;
|
||||
status: 'pending' | 'done' | 'snoozed' | 'cancelled';
|
||||
due_at?: string;
|
||||
notes?: string;
|
||||
}): Promise<CrmTask> {
|
||||
const res = await apiFetch<{ status: string; data: CrmTask }>(`/api/crm/tasks/${body.reminder_id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
status: body.status,
|
||||
due_at: body.due_at,
|
||||
notes: body.notes,
|
||||
}),
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
// ── Kanban ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function fetchKanbanBoard(): Promise<KanbanColumn[]> {
|
||||
@@ -162,21 +128,6 @@ export async function fetchKanbanBoard(): Promise<KanbanColumn[]> {
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function updateLeadStage(body: {
|
||||
lead_id: string;
|
||||
status: string;
|
||||
notes?: string;
|
||||
}): Promise<CrmLeadStageUpdate> {
|
||||
const res = await apiFetch<{ status: string; data: CrmLeadStageUpdate }>(`/api/crm/leads/${body.lead_id}/stage`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({
|
||||
status: body.status,
|
||||
notes: body.notes,
|
||||
}),
|
||||
});
|
||||
return res.data;
|
||||
}
|
||||
|
||||
// ── QD Scores ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function fetchQdScore(personId: string): Promise<{
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
import { API_URL } from '@/lib/api';
|
||||
import { buildVelocityHeaders } from '@/lib/velocitySession';
|
||||
|
||||
const rawDreamWeaverBase = import.meta.env.VITE_DREAM_WEAVER_URL?.trim();
|
||||
const rawDreamWeaverApiKey = import.meta.env.VITE_DREAM_WEAVER_API_KEY?.trim();
|
||||
const LOCAL_DREAM_WEAVER_GATEWAY = 'http://127.0.0.1:8082';
|
||||
|
||||
export const DREAM_WEAVER_URL = (rawDreamWeaverBase && rawDreamWeaverBase.length > 0
|
||||
? rawDreamWeaverBase
|
||||
: import.meta.env.DEV
|
||||
? LOCAL_DREAM_WEAVER_GATEWAY
|
||||
: API_URL
|
||||
).replace(/\/$/, '');
|
||||
|
||||
export interface DreamWeaverHealth {
|
||||
online: boolean;
|
||||
routeMounted: boolean;
|
||||
status: string;
|
||||
comfyuiOnline?: boolean;
|
||||
comfyuiUrl?: string;
|
||||
checkpointReady?: boolean;
|
||||
checkpointCount?: number;
|
||||
availableCheckpoints?: string[];
|
||||
preferredCheckpoints?: string[];
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
export interface DreamWeaverJobResponse {
|
||||
job_id: string;
|
||||
status?: string;
|
||||
poll_url?: string;
|
||||
result_url?: string;
|
||||
}
|
||||
|
||||
export interface DreamWeaverStatusResponse {
|
||||
status?: string;
|
||||
ready?: boolean;
|
||||
result_url?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface SubmitDreamWeaverJobInput {
|
||||
image: File;
|
||||
roomType: string;
|
||||
keywords: string;
|
||||
}
|
||||
|
||||
function buildDreamWeaverHeaders(init?: HeadersInit): Headers {
|
||||
const headers = buildVelocityHeaders(init, false);
|
||||
if (rawDreamWeaverApiKey && !headers.has('X-Dream-Weaver-API-Key')) {
|
||||
headers.set('X-Dream-Weaver-API-Key', rawDreamWeaverApiKey);
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
function resolveDreamWeaverUrl(candidate: string | undefined, fallbackPath: string): string {
|
||||
const path = candidate && candidate.trim().length > 0 ? candidate.trim() : fallbackPath;
|
||||
if (/^https?:\/\//i.test(path)) {
|
||||
return path;
|
||||
}
|
||||
return `${DREAM_WEAVER_URL}${path.startsWith('/') ? path : `/${path}`}`;
|
||||
}
|
||||
|
||||
async function readErrorMessage(response: Response, fallback: string): Promise<string> {
|
||||
const body = await response.json().catch(() => null) as { detail?: unknown; message?: unknown; error?: unknown } | null;
|
||||
if (typeof body?.detail === 'string') return body.detail;
|
||||
if (typeof body?.message === 'string') return body.message;
|
||||
if (typeof body?.error === 'string') return body.error;
|
||||
const text = await response.text().catch(() => '');
|
||||
return text.trim() || fallback;
|
||||
}
|
||||
|
||||
async function requestDreamWeaverJson<T>(url: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(url, {
|
||||
...init,
|
||||
headers: buildDreamWeaverHeaders(init?.headers),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(await readErrorMessage(response, `Dream Weaver request failed: ${response.status}`));
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export async function checkDreamWeaverHealth(): Promise<DreamWeaverHealth> {
|
||||
let status = 'offline';
|
||||
let detail: string | undefined;
|
||||
let comfyuiOnline: boolean | undefined;
|
||||
let comfyuiUrl: string | undefined;
|
||||
let checkpointReady: boolean | undefined;
|
||||
let checkpointCount: number | undefined;
|
||||
let availableCheckpoints: string[] | undefined;
|
||||
let preferredCheckpoints: string[] | undefined;
|
||||
let healthOk = false;
|
||||
|
||||
try {
|
||||
const response = await fetch(resolveDreamWeaverUrl(undefined, '/health'), {
|
||||
headers: buildDreamWeaverHeaders(),
|
||||
});
|
||||
const body = await response.json().catch(() => null) as {
|
||||
status?: unknown;
|
||||
detail?: unknown;
|
||||
comfyui?: unknown;
|
||||
comfyui_url?: unknown;
|
||||
comfyuiUrl?: unknown;
|
||||
checkpoint_ready?: unknown;
|
||||
checkpoint_count?: unknown;
|
||||
available_checkpoints?: unknown;
|
||||
preferred_checkpoints?: unknown;
|
||||
} | null;
|
||||
status = typeof body?.status === 'string' ? body.status : response.ok ? 'ok' : `HTTP ${response.status}`;
|
||||
detail = typeof body?.detail === 'string' ? body.detail : undefined;
|
||||
comfyuiOnline = typeof body?.comfyui === 'boolean' ? body.comfyui : undefined;
|
||||
comfyuiUrl = typeof body?.comfyui_url === 'string'
|
||||
? body.comfyui_url
|
||||
: typeof body?.comfyuiUrl === 'string'
|
||||
? body.comfyuiUrl
|
||||
: undefined;
|
||||
checkpointReady = typeof body?.checkpoint_ready === 'boolean' ? body.checkpoint_ready : undefined;
|
||||
checkpointCount = typeof body?.checkpoint_count === 'number' ? body.checkpoint_count : undefined;
|
||||
availableCheckpoints = Array.isArray(body?.available_checkpoints)
|
||||
? body.available_checkpoints.filter((item): item is string => typeof item === 'string')
|
||||
: undefined;
|
||||
preferredCheckpoints = Array.isArray(body?.preferred_checkpoints)
|
||||
? body.preferred_checkpoints.filter((item): item is string => typeof item === 'string')
|
||||
: undefined;
|
||||
healthOk = response.ok && ['ok', 'healthy', 'online'].includes(status.toLowerCase());
|
||||
} catch (error) {
|
||||
detail = error instanceof Error ? error.message : 'Unable to reach Dream Weaver gateway.';
|
||||
}
|
||||
|
||||
try {
|
||||
const probe = await fetch(resolveDreamWeaverUrl(undefined, '/dream-weaver/status/velocity-route-probe'), {
|
||||
headers: buildDreamWeaverHeaders(),
|
||||
});
|
||||
if (probe.ok) {
|
||||
return { online: healthOk, routeMounted: true, status, comfyuiOnline, comfyuiUrl, checkpointReady, checkpointCount, availableCheckpoints, preferredCheckpoints, detail };
|
||||
}
|
||||
const probeMessage = await readErrorMessage(probe, '');
|
||||
const expectedMissingJob = probe.status === 404 && /job|not found|missing/i.test(probeMessage);
|
||||
return {
|
||||
online: healthOk && expectedMissingJob,
|
||||
routeMounted: expectedMissingJob,
|
||||
status,
|
||||
comfyuiOnline,
|
||||
comfyuiUrl,
|
||||
checkpointReady,
|
||||
checkpointCount,
|
||||
availableCheckpoints,
|
||||
preferredCheckpoints,
|
||||
detail: detail ?? probeMessage,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
online: false,
|
||||
routeMounted: false,
|
||||
status,
|
||||
comfyuiOnline,
|
||||
comfyuiUrl,
|
||||
checkpointReady,
|
||||
checkpointCount,
|
||||
availableCheckpoints,
|
||||
preferredCheckpoints,
|
||||
detail: error instanceof Error ? error.message : detail,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function submitDreamWeaverJob(input: SubmitDreamWeaverJobInput): Promise<DreamWeaverJobResponse> {
|
||||
const formData = new FormData();
|
||||
formData.append('image', input.image, input.image.name || 'room-source.jpg');
|
||||
formData.append('room_type', input.roomType);
|
||||
const trimmedKeywords = input.keywords.trim();
|
||||
if (trimmedKeywords.length > 0) {
|
||||
formData.append('keywords', trimmedKeywords);
|
||||
}
|
||||
|
||||
return requestDreamWeaverJson<DreamWeaverJobResponse>(resolveDreamWeaverUrl(undefined, '/dream-weaver'), {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getDreamWeaverStatus(job: Pick<DreamWeaverJobResponse, 'job_id' | 'poll_url'>): Promise<DreamWeaverStatusResponse> {
|
||||
return requestDreamWeaverJson<DreamWeaverStatusResponse>(
|
||||
resolveDreamWeaverUrl(job.poll_url, `/dream-weaver/status/${encodeURIComponent(job.job_id)}`),
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchDreamWeaverResult(jobId: string, resultUrl?: string): Promise<Blob> {
|
||||
const response = await fetch(resolveDreamWeaverUrl(resultUrl, `/dream-weaver/result/${encodeURIComponent(jobId)}`), {
|
||||
headers: buildDreamWeaverHeaders({ Accept: 'image/png,image/*,*/*' }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(await readErrorMessage(response, `Dream Weaver result failed: ${response.status}`));
|
||||
}
|
||||
return response.blob();
|
||||
}
|
||||
@@ -1,19 +1,10 @@
|
||||
import { API_URL } from '@/lib/api';
|
||||
import {
|
||||
buildVelocityHeaders,
|
||||
setVelocityToken,
|
||||
} from '@/lib/velocitySession';
|
||||
export {
|
||||
VELOCITY_TOKEN_KEY,
|
||||
clearVelocityToken,
|
||||
getVelocityToken,
|
||||
setVelocityToken,
|
||||
} from '@/lib/velocitySession';
|
||||
|
||||
export const VELOCITY_TOKEN_KEY = 'velocity-api-token';
|
||||
|
||||
export interface VelocityUserProfile {
|
||||
user_id: string;
|
||||
role: string;
|
||||
tenant_id?: string;
|
||||
full_name?: string | null;
|
||||
email?: string | null;
|
||||
avatar_url?: string | null;
|
||||
@@ -22,7 +13,6 @@ export interface VelocityUserProfile {
|
||||
export interface VelocityActiveUser {
|
||||
user_id: string;
|
||||
role: string;
|
||||
tenant_id?: string;
|
||||
full_name?: string | null;
|
||||
email?: string | null;
|
||||
avatar_url?: string | null;
|
||||
@@ -158,7 +148,18 @@ export interface InventoryPropertySummary {
|
||||
}
|
||||
|
||||
function buildHeaders(init?: HeadersInit, includeJson = true): Headers {
|
||||
return buildVelocityHeaders(init, includeJson);
|
||||
const headers = new Headers(init);
|
||||
if (includeJson && !headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
if (!headers.has('Accept')) {
|
||||
headers.set('Accept', 'application/json');
|
||||
}
|
||||
const token = getVelocityToken();
|
||||
if (token && !headers.has('Authorization')) {
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
async function platformFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
@@ -181,6 +182,18 @@ async function platformFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export function setVelocityToken(token: string) {
|
||||
localStorage.setItem(VELOCITY_TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
export function getVelocityToken(): string | null {
|
||||
return localStorage.getItem(VELOCITY_TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function clearVelocityToken() {
|
||||
localStorage.removeItem(VELOCITY_TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function normalizeVelocityRole(role: string | null | undefined): string {
|
||||
return (role ?? '').trim().toUpperCase();
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
export const VELOCITY_TOKEN_KEY = 'velocity-api-token';
|
||||
|
||||
export function getVelocityToken(): string | null {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
return window.localStorage.getItem(VELOCITY_TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function setVelocityToken(token: string) {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(VELOCITY_TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
export function clearVelocityToken() {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
window.localStorage.removeItem(VELOCITY_TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function buildVelocityHeaders(init?: HeadersInit, includeJson = true): Headers {
|
||||
const headers = new Headers(init);
|
||||
if (includeJson && !headers.has('Content-Type')) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
if (!headers.has('Accept')) {
|
||||
headers.set('Accept', 'application/json');
|
||||
}
|
||||
const token = getVelocityToken();
|
||||
if (token && !headers.has('Authorization')) {
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* useOracleExecution — manages prompt submission and durable execution history.
|
||||
*/
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import type { PromptExecution, CanvasComponent, PlacementMode, OracleExecutionMode } from '../types/canvas';
|
||||
import type { PromptExecution, CanvasComponent, PlacementMode } from '../types/canvas';
|
||||
import { submitPrompt } from '../lib/oracleApiClient';
|
||||
|
||||
export interface ExecutionEntry {
|
||||
@@ -20,7 +20,6 @@ export interface OracleExecutionState {
|
||||
prompt: string;
|
||||
tenantId: string;
|
||||
actorId: string;
|
||||
executionMode?: OracleExecutionMode;
|
||||
placementMode?: PlacementMode;
|
||||
conversationContext?: Array<{ role: 'user' | 'assistant'; content: string }>;
|
||||
onExecutionCommitted?: (commit: {
|
||||
@@ -46,7 +45,6 @@ export function useOracleExecution(): OracleExecutionState {
|
||||
prompt,
|
||||
tenantId,
|
||||
actorId,
|
||||
executionMode = 'auto',
|
||||
placementMode = 'append_after_last_visible_component',
|
||||
conversationContext = [],
|
||||
onExecutionCommitted,
|
||||
@@ -56,7 +54,6 @@ export function useOracleExecution(): OracleExecutionState {
|
||||
prompt: string;
|
||||
tenantId: string;
|
||||
actorId: string;
|
||||
executionMode?: OracleExecutionMode;
|
||||
placementMode?: PlacementMode;
|
||||
conversationContext?: Array<{ role: 'user' | 'assistant'; content: string }>;
|
||||
onExecutionCommitted?: (commit: {
|
||||
@@ -76,11 +73,10 @@ export function useOracleExecution(): OracleExecutionState {
|
||||
prompt,
|
||||
intentClass: 'analytical',
|
||||
status: 'planning',
|
||||
modelRuntime: executionMode === 'thinking' ? 'colony_orchestrator' : 'oracle_runtime',
|
||||
modelRuntime: 'oracle_runtime',
|
||||
semanticModelVersion: 'oracle_semantic_v2026_04_08_01',
|
||||
warnings: [],
|
||||
createdAt: now,
|
||||
executionMode,
|
||||
};
|
||||
|
||||
setInFlight(optimistic);
|
||||
@@ -95,7 +91,6 @@ export function useOracleExecution(): OracleExecutionState {
|
||||
prompt,
|
||||
conversationContext,
|
||||
placementMode,
|
||||
executionMode,
|
||||
});
|
||||
|
||||
const completed: PromptExecution = {
|
||||
@@ -105,9 +100,6 @@ export function useOracleExecution(): OracleExecutionState {
|
||||
summary: response.summary,
|
||||
warnings: response.warnings,
|
||||
componentsCreated: response.componentsCreated,
|
||||
executionMode: response.executionMode ?? executionMode,
|
||||
resolvedMode: response.resolvedMode,
|
||||
colonyMissionId: response.colonyMissionId,
|
||||
completedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import type {
|
||||
OracleEnvelope,
|
||||
CanvasPageRevision,
|
||||
} from '../types/canvas';
|
||||
import { VELOCITY_TOKEN_KEY } from '@/lib/velocitySession';
|
||||
import { VELOCITY_TOKEN_KEY } from '@/lib/velocityPlatformClient';
|
||||
|
||||
function getBrowserOrigin(): string {
|
||||
return typeof window !== 'undefined' ? window.location.origin : '';
|
||||
|
||||
@@ -50,8 +50,6 @@ export type ExecutionStatus =
|
||||
| 'failed'
|
||||
| 'clarification_required';
|
||||
|
||||
export type OracleExecutionMode = 'auto' | 'fast' | 'thinking';
|
||||
|
||||
export type PageType = 'main' | 'fork';
|
||||
|
||||
export type ForkStatus = 'active' | 'merged' | 'closed';
|
||||
@@ -282,9 +280,6 @@ export interface PromptExecution {
|
||||
componentsCreated?: string[];
|
||||
createdAt: string;
|
||||
completedAt?: string;
|
||||
executionMode?: OracleExecutionMode;
|
||||
resolvedMode?: 'fast' | 'thinking';
|
||||
colonyMissionId?: string;
|
||||
}
|
||||
|
||||
export interface ComponentTemplate {
|
||||
@@ -388,7 +383,6 @@ export interface PromptSubmitRequest {
|
||||
prompt: string;
|
||||
conversationContext?: Array<{ role: 'user' | 'assistant'; content: string }>;
|
||||
placementMode?: PlacementMode;
|
||||
executionMode?: OracleExecutionMode;
|
||||
}
|
||||
|
||||
export interface PromptSubmitResponse {
|
||||
@@ -401,9 +395,6 @@ export interface PromptSubmitResponse {
|
||||
components: CanvasComponent[];
|
||||
summary: string;
|
||||
warnings: string[];
|
||||
executionMode?: OracleExecutionMode;
|
||||
resolvedMode?: 'fast' | 'thinking';
|
||||
colonyMissionId?: string;
|
||||
}
|
||||
|
||||
export interface CanvasPageRevision {
|
||||
|
||||
@@ -15,7 +15,7 @@ interface MarketingState {
|
||||
adInsights: AdInsight[];
|
||||
liveEvents: LiveOptimizationEvent[];
|
||||
settings: CatalystSettings;
|
||||
activeTab: 'studio' | 'command' | 'intelligence' | 'war-room' | 'marketing' | 'dream-weaver';
|
||||
activeTab: 'studio' | 'command' | 'intelligence' | 'war-room' | 'marketing';
|
||||
|
||||
// Actions
|
||||
addCampaign: (campaign: Campaign) => void;
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
// Velocity Comms Types
|
||||
// Native types for the Conversations module
|
||||
|
||||
export type CommsChannel = 'whatsapp' | 'sms' | 'call';
|
||||
export type CommsThreadStatus = 'open' | 'resolved' | 'spam' | 'archived';
|
||||
export type CommsMessageDirection = 'inbound' | 'outbound' | 'system';
|
||||
export type CommsMessageType = 'text' | 'image' | 'video' | 'audio' | 'document' | 'location' | 'template';
|
||||
export type CommsProvider = 'mock' | 'waha' | 'evolution' | 'meta_cloud';
|
||||
export type CommsDeliveryStatus = 'pending' | 'sent' | 'delivered' | 'read' | 'failed';
|
||||
|
||||
export interface CommsThread {
|
||||
threadId: string;
|
||||
provider: CommsProvider;
|
||||
externalThreadId?: string;
|
||||
personId?: string;
|
||||
phoneE164: string;
|
||||
displayName?: string;
|
||||
channel: CommsChannel;
|
||||
status: CommsThreadStatus;
|
||||
assignedUserId?: string;
|
||||
lastMessageAt?: string;
|
||||
unreadCount: number;
|
||||
metadataJson?: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
crmPerson?: {
|
||||
id: string;
|
||||
fullName?: string;
|
||||
primaryPhone?: string;
|
||||
primaryEmail?: string;
|
||||
buyerType?: string;
|
||||
leadStatus?: string;
|
||||
projectName?: string;
|
||||
};
|
||||
lastMessagePreview?: string;
|
||||
}
|
||||
|
||||
export interface CommsMessage {
|
||||
messageId: string;
|
||||
threadId: string;
|
||||
provider: CommsProvider;
|
||||
externalMessageId?: string;
|
||||
direction: CommsMessageDirection;
|
||||
messageType: CommsMessageType;
|
||||
body: string;
|
||||
mediaUrl?: string;
|
||||
mediaMimeType?: string;
|
||||
deliveryStatus: CommsDeliveryStatus;
|
||||
sentAt?: string;
|
||||
deliveredAt?: string;
|
||||
readAt?: string;
|
||||
rawPayload?: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
senderName?: string;
|
||||
senderAvatar?: string;
|
||||
}
|
||||
|
||||
export interface CommsCallLog {
|
||||
callId: string;
|
||||
threadId?: string;
|
||||
personId?: string;
|
||||
provider: CommsProvider;
|
||||
externalCallId?: string;
|
||||
phoneE164: string;
|
||||
direction: 'inbound' | 'outbound';
|
||||
status: 'ringing' | 'answered' | 'missed' | 'voicemail' | 'completed';
|
||||
startedAt: string;
|
||||
endedAt?: string;
|
||||
durationSeconds?: number;
|
||||
recordingUrl?: string;
|
||||
transcriptId?: string;
|
||||
transcriptText?: string;
|
||||
rawPayload?: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface CommsSettings {
|
||||
provider: CommsProvider;
|
||||
providerBaseUrl?: string;
|
||||
providerApiKey?: string;
|
||||
instanceId?: string;
|
||||
phoneNumberId?: string;
|
||||
webhookCallbackUrl?: string;
|
||||
webhookSecretSet: boolean;
|
||||
defaultAssignmentUserId?: string;
|
||||
autoLinkByPhone: boolean;
|
||||
createCrmInteractionOnInbound: boolean;
|
||||
defaultCountryCode: string;
|
||||
mediaStorageDir?: string;
|
||||
transcriptionProvider?: 'none' | 'openai' | 'local';
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface ThreadLinkPayload {
|
||||
personId: string;
|
||||
}
|
||||
|
||||
export interface SendMessagePayload {
|
||||
messageType: CommsMessageType;
|
||||
body: string;
|
||||
mediaUrl?: string;
|
||||
templateName?: string;
|
||||
templateLanguage?: string;
|
||||
}
|
||||
|
||||
export interface CommsProviderTestResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
accountInfo?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface CommsThreadListResponse {
|
||||
threads: CommsThread[];
|
||||
total: number;
|
||||
unreadTotal: number;
|
||||
}
|
||||
|
||||
export interface CommsMessageListResponse {
|
||||
messages: CommsMessage[];
|
||||
thread: CommsThread;
|
||||
}
|
||||
@@ -69,7 +69,6 @@ export interface CrmOpportunityCard {
|
||||
probability: number | null;
|
||||
expected_close_date: string | null;
|
||||
next_action: string | null;
|
||||
notes?: string | null;
|
||||
project_id: string | null;
|
||||
unit_id: string | null;
|
||||
// When fetched from list endpoint, person-level fields are included
|
||||
@@ -110,16 +109,6 @@ export interface CrmTask {
|
||||
client_phone?: string;
|
||||
}
|
||||
|
||||
export interface CrmLeadStageUpdate {
|
||||
lead_id: string;
|
||||
person_id: string;
|
||||
status: CrmLeadStatus;
|
||||
budget_band: string | null;
|
||||
urgency: string | null;
|
||||
client_name?: string;
|
||||
client_phone?: string;
|
||||
}
|
||||
|
||||
// ── Property Interest ─────────────────────────────────────────────────────────
|
||||
|
||||
export interface PropertyInterest {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Navigation Module Types
|
||||
export type ModuleId = 'dashboard' | 'oracle' | 'sentinel' | 'inventory' | 'settings' | 'catalyst' | 'admin' | 'crm' | 'comms';
|
||||
export type ModuleId = 'dashboard' | 'oracle' | 'sentinel' | 'inventory' | 'settings' | 'catalyst' | 'admin' | 'crm';
|
||||
export type SentinelSubTab = 'overview' | 'live-session';
|
||||
|
||||
|
||||
|
||||
@@ -7,11 +7,6 @@ META_ACCESS_TOKEN=PLACEHOLDER_your_meta_system_user_token
|
||||
# Meta Business Manager → Ad Accounts
|
||||
META_AD_ACCOUNT_ID=PLACEHOLDER_act_1234567890
|
||||
|
||||
# Page publishing values for Catalyst social posting
|
||||
META_PAGE_ACCESS_TOKEN=PLACEHOLDER_your_meta_page_access_token
|
||||
META_PAGE_ID=PLACEHOLDER_1234567890
|
||||
META_INSTAGRAM_BUSINESS_ID=PLACEHOLDER_17841400000000000
|
||||
|
||||
# Business Portfolio ID
|
||||
# Meta Business Settings → Business Info → Business ID
|
||||
META_BUSINESS_ID=PLACEHOLDER_1234567890
|
||||
@@ -37,16 +32,6 @@ SUPABASE_SERVICE_ROLE_KEY=PLACEHOLDER_your_supabase_service_role_key
|
||||
# Base URL of ComfyUI server running locally or on GPU node
|
||||
COMFY_BASE_URL=http://localhost:8188
|
||||
|
||||
# ── Colony Orchestration ─────────────────────────────────────────────────────
|
||||
# Real colony orchestrator service URL. Required; no local mock fallback is used.
|
||||
COLONY_SERVICE_URL=PLACEHOLDER_http://localhost:8090
|
||||
COLONY_TIMEOUT_SECONDS=30
|
||||
|
||||
# ── Social Posting ───────────────────────────────────────────────────────────
|
||||
LINKEDIN_ACCESS_TOKEN=PLACEHOLDER_your_linkedin_access_token
|
||||
LINKEDIN_ORG_ID=PLACEHOLDER_123456
|
||||
TWITTER_BEARER_TOKEN=PLACEHOLDER_your_twitter_bearer_token
|
||||
|
||||
# —— Shared Desineuron coding / Oracle / NemoClaw runtime —————————————————————
|
||||
# Stable OpenAI-compatible SGLang route rendered through ingress.
|
||||
LLM_BASE_URL=https://llm.desineuron.in
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
# Project Velocity production environment template.
|
||||
# Copy to backend/.env.production on the deployment host, or map these names into
|
||||
# your secrets manager / systemd EnvironmentFile. Keep real values out of git.
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Runtime / Deployment
|
||||
# -----------------------------------------------------------------------------
|
||||
ENVIRONMENT=production
|
||||
VELOCITY_ENV_FILE=/opt/velocity/backend/.env.production
|
||||
VELOCITY_PUBLIC_BACKEND_URL=https://api.desineuron.in
|
||||
VELOCITY_API_BASE_URL=https://api.desineuron.in
|
||||
VELOCITY_DREAM_WEAVER_URL=https://dreamweaver.desineuron.in
|
||||
VELOCITY_DEFAULT_TENANT_ID=tenant_velocity
|
||||
VELOCITY_DEMO_TENANT_ID=tenant_velocity
|
||||
VELOCITY_DEMO_OPERATOR_EMAIL=
|
||||
CORS_ORIGINS=https://velocity.desineuron.in,https://api.desineuron.in
|
||||
TRUSTED_HOSTS=api.desineuron.in,dreamweaver.desineuron.in,velocity.desineuron.in
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# PostgreSQL
|
||||
# -----------------------------------------------------------------------------
|
||||
# Prefer DATABASE_URL in production. VELOCITY_DB_* is retained for services and
|
||||
# seed scripts that construct asyncpg pools from discrete credentials.
|
||||
DATABASE_URL=
|
||||
VELOCITY_DB_HOST=
|
||||
VELOCITY_DB_PORT=5432
|
||||
VELOCITY_DB_NAME=
|
||||
VELOCITY_DB_USER=
|
||||
VELOCITY_DB_PASSWORD=
|
||||
VELOCITY_DB_SSLMODE=require
|
||||
|
||||
# Optional read-only Oracle database credentials for natural-language DB agent.
|
||||
ORACLE_READ_DATABASE_URL=
|
||||
VELOCITY_DB_READ_HOST=
|
||||
VELOCITY_DB_READ_PORT=5432
|
||||
VELOCITY_DB_READ_NAME=
|
||||
VELOCITY_DB_READ_USER=
|
||||
VELOCITY_DB_READ_PASSWORD=
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Auth / JWT / Sessions
|
||||
# -----------------------------------------------------------------------------
|
||||
VELOCITY_JWT_SECRET=
|
||||
SECRET_KEY=
|
||||
VELOCITY_PASSWORD_RECOVERY_MINUTES=30
|
||||
# Set to true only in a sealed internal test environment; never on public prod.
|
||||
VELOCITY_AUTH_RETURN_RECOVERY_TOKEN=false
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Enterprise SSO: OAuth / OIDC / SAML
|
||||
# -----------------------------------------------------------------------------
|
||||
# Comma-separated provider IDs exposed to the iPad Settings screen.
|
||||
# Example: VELOCITY_SSO_PROVIDERS=azure_ad,okta
|
||||
VELOCITY_SSO_PROVIDERS=
|
||||
VELOCITY_DEFAULT_SSO_PROVIDER=
|
||||
|
||||
# OAuth/OIDC provider: Azure AD.
|
||||
VELOCITY_SSO_AZURE_AD_TYPE=oauth
|
||||
VELOCITY_SSO_AZURE_AD_NAME=Azure AD
|
||||
VELOCITY_SSO_AZURE_AD_ISSUER=
|
||||
VELOCITY_SSO_AZURE_AD_METADATA_URL=
|
||||
VELOCITY_SSO_AZURE_AD_AUTH_URL=
|
||||
VELOCITY_SSO_AZURE_AD_TOKEN_URL=
|
||||
VELOCITY_SSO_AZURE_AD_CLIENT_ID=
|
||||
VELOCITY_SSO_AZURE_AD_CLIENT_SECRET=
|
||||
VELOCITY_SSO_AZURE_AD_REDIRECT_URI=https://api.desineuron.in/api/auth/sso/azure_ad/callback
|
||||
|
||||
# OAuth/OIDC provider: Okta.
|
||||
VELOCITY_SSO_OKTA_TYPE=oauth
|
||||
VELOCITY_SSO_OKTA_NAME=Okta
|
||||
VELOCITY_SSO_OKTA_ISSUER=
|
||||
VELOCITY_SSO_OKTA_METADATA_URL=
|
||||
VELOCITY_SSO_OKTA_AUTH_URL=
|
||||
VELOCITY_SSO_OKTA_TOKEN_URL=
|
||||
VELOCITY_SSO_OKTA_CLIENT_ID=
|
||||
VELOCITY_SSO_OKTA_CLIENT_SECRET=
|
||||
VELOCITY_SSO_OKTA_REDIRECT_URI=https://api.desineuron.in/api/auth/sso/okta/callback
|
||||
|
||||
# SAML provider values for enterprise tenants that require SAML.
|
||||
VELOCITY_SAML_ENTITY_ID=
|
||||
VELOCITY_SAML_SSO_URL=
|
||||
VELOCITY_SAML_CERTIFICATE_PEM=
|
||||
VELOCITY_SAML_PRIVATE_KEY_PEM=
|
||||
VELOCITY_SAML_ASSERTION_CONSUMER_SERVICE_URL=https://api.desineuron.in/api/auth/saml/acs
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# MDM / Managed App Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
VELOCITY_MDM_REQUIRED=true
|
||||
VELOCITY_MDM_ORG_NAME=
|
||||
VELOCITY_MDM_SUPPORT_EMAIL=
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Communications: WAHA / Evolution / Meta WhatsApp
|
||||
# -----------------------------------------------------------------------------
|
||||
# COMMS_PROVIDER valid values: waha, evolution, mock.
|
||||
COMMS_PROVIDER=waha
|
||||
COMMS_PROVIDER_BASE_URL=
|
||||
COMMS_PROVIDER_API_KEY=
|
||||
COMMS_INSTANCE_ID=
|
||||
COMMS_DEFAULT_COUNTRY_CODE=91
|
||||
COMMS_WEBHOOK_SECRET=
|
||||
COMMS_MEDIA_STORAGE_DIR=/opt/dlami/nvme/assets/comms
|
||||
|
||||
# WAHA-specific values, if production uses WAHA directly.
|
||||
WAHA_BASE_URL=
|
||||
WAHA_API_KEY=
|
||||
WAHA_SESSION=velocity-production
|
||||
WAHA_WEBHOOK_SECRET=
|
||||
WAHA_WEBHOOK_CALLBACK_URL=https://api.desineuron.in/api/comms/webhooks/waha
|
||||
|
||||
# Evolution API-specific values, if production uses Evolution.
|
||||
EVOLUTION_BASE_URL=
|
||||
EVOLUTION_API_KEY=
|
||||
EVOLUTION_INSTANCE_ID=
|
||||
EVOLUTION_WEBHOOK_SECRET=
|
||||
EVOLUTION_WEBHOOK_CALLBACK_URL=https://api.desineuron.in/api/comms/webhooks/evolution
|
||||
|
||||
# Meta Graph / WhatsApp Cloud API values.
|
||||
META_ACCESS_TOKEN=
|
||||
META_APP_ID=
|
||||
META_APP_SECRET=
|
||||
META_BUSINESS_ID=
|
||||
META_AD_ACCOUNT_ID=
|
||||
META_PAGE_ACCESS_TOKEN=
|
||||
META_PAGE_ID=
|
||||
META_INSTAGRAM_BUSINESS_ID=
|
||||
META_PHONE_NUMBER_ID=
|
||||
META_WHATSAPP_BUSINESS_ACCOUNT_ID=
|
||||
META_WEBHOOK_VERIFY_TOKEN=
|
||||
META_API_VERSION=v21.0
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Communications Transcription Providers
|
||||
# -----------------------------------------------------------------------------
|
||||
# COMMS_TRANSCRIPTION_PROVIDER valid values: openai, deepgram, http, none.
|
||||
COMMS_TRANSCRIPTION_PROVIDER=openai
|
||||
COMMS_TRANSCRIPTION_LANGUAGE=en
|
||||
OPENAI_API_KEY=
|
||||
COMMS_OPENAI_TRANSCRIPTION_MODEL=whisper-1
|
||||
DEEPGRAM_API_KEY=
|
||||
COMMS_DEEPGRAM_MODEL=nova-2
|
||||
COMMS_TRANSCRIPTION_ENDPOINT=
|
||||
COMMS_TRANSCRIPTION_ENDPOINT_TOKEN=
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Media Storage / AWS S3
|
||||
# -----------------------------------------------------------------------------
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_SESSION_TOKEN=
|
||||
AWS_REGION=ap-south-1
|
||||
AWS_S3_BUCKET=
|
||||
AWS_S3_PUBLIC_BASE_URL=
|
||||
AWS_S3_MEDIA_PREFIX=velocity-production
|
||||
VELOCITY_ASSET_DIR=/opt/dlami/nvme/assets
|
||||
VELOCITY_VIDEO_DIR=/opt/dlami/nvme/assets/videos
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Dream Weaver / ComfyUI / GPU Gateway
|
||||
# -----------------------------------------------------------------------------
|
||||
COMFY_BASE_URL=http://127.0.0.1:8188
|
||||
DREAM_WEAVER_GATEWAY_URL=https://dreamweaver.desineuron.in
|
||||
DREAM_WEAVER_API_KEY=
|
||||
COMFY_CHECKPOINT_NAME=
|
||||
COMFY_WORKFLOW_DIR=/opt/dlami/nvme/velocity/comfy_workflows
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# LLM / NemoClaw Runtime
|
||||
# -----------------------------------------------------------------------------
|
||||
LLM_BASE_URL=https://llm.desineuron.in
|
||||
SGLANG_BASE_URL=https://llm.desineuron.in
|
||||
SGLANG_CHAT_URL=https://llm.desineuron.in/v1/chat/completions
|
||||
SGLANG_MODELS_URL=https://llm.desineuron.in/v1/models
|
||||
SGLANG_MODEL=qwen3.6:35b-a3b
|
||||
SGLANG_API_TOKEN=
|
||||
RUNTIME_LLM_TIMEOUT_S=90.0
|
||||
RUNTIME_LLM_BATCH_CONCURRENCY=2
|
||||
|
||||
NEMOCLAW_BASE_URL=https://llm.desineuron.in
|
||||
NEMOCLAW_CHAT_URL=https://llm.desineuron.in/v1/chat/completions
|
||||
NEMOCLAW_MODEL=qwen3.6:35b-a3b
|
||||
NEMOCLAW_API_TOKEN=
|
||||
NEMOCLAW_WEBHOOK_SECRET=
|
||||
NEMOCLAW_PROMPT_DIR=/opt/dlami/nvme/nemoclaw/prompts
|
||||
NEMOCLAW_TIMEOUT_S=45.0
|
||||
NEMOCLAW_TEMPERATURE=0.2
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Oracle / Sentinel Runtime
|
||||
# -----------------------------------------------------------------------------
|
||||
ORACLE_DEFAULT_TENANT_ID=tenant_velocity
|
||||
ORACLE_DEFAULT_TIMEZONE=Asia/Dubai
|
||||
ORACLE_DEFAULT_LOCALE=en-AE
|
||||
ORACLE_POLICY_PROFILE_ID=policy_sales_director_standard_v4
|
||||
ORACLE_DEFAULT_PAGE_TITLE=Oracle Main Canvas
|
||||
ORACLE_ALLOW_IN_MEMORY_FALLBACK=false
|
||||
SENTINEL_PERCEPTION_INTERVAL_SECONDS=3
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Legacy / Adjacent Integrations
|
||||
# -----------------------------------------------------------------------------
|
||||
# Supabase is retained only for legacy Catalyst CRM/marketing surfaces.
|
||||
SUPABASE_URL=
|
||||
SUPABASE_ANON_KEY=
|
||||
SUPABASE_SERVICE_ROLE_KEY=
|
||||
|
||||
# Ad-network integrations for Catalyst surfaces.
|
||||
GOOGLE_ADS_DEVELOPER_TOKEN=
|
||||
GOOGLE_ADS_CLIENT_ID=
|
||||
GOOGLE_ADS_CLIENT_SECRET=
|
||||
GOOGLE_ADS_REFRESH_TOKEN=
|
||||
GOOGLE_ADS_CUSTOMER_ID=
|
||||
LINKEDIN_ACCESS_TOKEN=
|
||||
LINKEDIN_ORG_ID=
|
||||
TWITTER_BEARER_TOKEN=
|
||||
BRAVE_API_KEY=
|
||||
|
||||
# Colony orchestration service. Required for /api/colony mission dispatch.
|
||||
COLONY_SERVICE_URL=
|
||||
COLONY_TIMEOUT_SECONDS=30
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Observability / Alerts
|
||||
# -----------------------------------------------------------------------------
|
||||
SENTRY_DSN=
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=
|
||||
SLACK_WEBHOOK_URL=
|
||||
PAGERDUTY_ROUTING_KEY=
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Fastlane / Apple Release Automation
|
||||
# -----------------------------------------------------------------------------
|
||||
# These are consumed from the operator Mac when running fastlane, not by the
|
||||
# backend service. They are documented here so release secrets are tracked.
|
||||
FASTLANE_APPLE_ID=
|
||||
FASTLANE_TEAM_ID=
|
||||
FASTLANE_ITC_TEAM_ID=
|
||||
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD=
|
||||
FASTLANE_FORCE_CERT=0
|
||||
FASTLANE_FORCE_PROFILE=0
|
||||
FASTLANE_SKIP_WAITING=true
|
||||
FASTLANE_DISTRIBUTE_EXTERNAL=0
|
||||
FASTLANE_NOTIFY_EXTERNAL_TESTERS=0
|
||||
FASTLANE_CHANGELOG=
|
||||
APP_STORE_CONNECT_API_KEY_KEY_ID=
|
||||
APP_STORE_CONNECT_API_KEY_ISSUER_ID=
|
||||
APP_STORE_CONNECT_API_KEY_KEY=
|
||||
@@ -25,7 +25,7 @@ from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||
@@ -36,7 +36,6 @@ from backend.auth.dependencies import get_current_user
|
||||
logger = logging.getLogger("velocity.admin_surface")
|
||||
|
||||
router = APIRouter()
|
||||
dashboard_router = APIRouter()
|
||||
|
||||
# ── RBAC guard ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -62,128 +61,6 @@ def _pool(request: Request):
|
||||
return pool
|
||||
|
||||
|
||||
class OfflineReplayAuditRecord(BaseModel):
|
||||
id: str
|
||||
kind: str
|
||||
operation: str
|
||||
targetId: str | None = None
|
||||
queuedAt: str
|
||||
attemptCount: int
|
||||
lastAttemptAt: str | None = None
|
||||
lastError: str | None = None
|
||||
|
||||
|
||||
class OfflineReplayAuditRequest(BaseModel):
|
||||
records: list[OfflineReplayAuditRecord] = Field(default_factory=list)
|
||||
pendingCount: int
|
||||
|
||||
|
||||
@dashboard_router.get("/metrics", summary="Canonical dashboard metrics for WebOS and iPad parity")
|
||||
async def get_dashboard_metrics(
|
||||
request: Request,
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
pool = _pool(request)
|
||||
tenant_id = user.tenant_id or "tenant_velocity"
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT
|
||||
(SELECT COUNT(*) FROM crm_leads WHERE tenant_id = $1)::int AS lead_count,
|
||||
(
|
||||
SELECT COUNT(DISTINCT p.person_id)::int
|
||||
FROM crm_people p
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT current_value
|
||||
FROM intel_qd_scores q
|
||||
WHERE q.person_id = p.person_id
|
||||
ORDER BY q.computed_at DESC
|
||||
LIMIT 1
|
||||
) q ON TRUE
|
||||
WHERE p.tenant_id = $1
|
||||
AND (
|
||||
COALESCE(p.buyer_type, '') ILIKE '%whale%'
|
||||
OR COALESCE(q.current_value, 0) >= 0.90
|
||||
)
|
||||
) AS whale_lead_count,
|
||||
(SELECT COUNT(*) FROM inventory_properties WHERE tenant_id = $1 AND status <> 'archived')::int AS property_count,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM user_calendar_events
|
||||
WHERE tenant_id = $1
|
||||
AND owner_user_id = $2
|
||||
AND status NOT IN ('cancelled', 'done')
|
||||
AND start_at >= date_trunc('day', NOW())
|
||||
AND start_at < date_trunc('day', NOW()) + INTERVAL '1 day'
|
||||
)::int AS today_calendar_count,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM intel_reminders
|
||||
WHERE COALESCE(tenant_id, $1) = $1
|
||||
AND status IN ('pending', 'open', 'scheduled', 'snoozed', 'confirmed')
|
||||
)::int AS pending_task_count,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM intel_reminders
|
||||
WHERE COALESCE(tenant_id, $1) = $1
|
||||
AND status IN ('pending', 'open', 'scheduled', 'snoozed', 'confirmed')
|
||||
AND priority IN ('urgent', 'high')
|
||||
)::int AS urgent_task_count,
|
||||
(SELECT COUNT(*) FROM insight_recommendations WHERE tenant_id = $1 AND status = 'pending')::int AS pending_insights,
|
||||
(SELECT COUNT(*) FROM edge_transcription_jobs WHERE tenant_id = $1 AND status = 'pending')::int AS pending_transcriptions
|
||||
""",
|
||||
tenant_id,
|
||||
user.user_id,
|
||||
)
|
||||
return {
|
||||
"status": "ok",
|
||||
"data": {
|
||||
"leadCount": row["lead_count"],
|
||||
"whaleLeadCount": row["whale_lead_count"],
|
||||
"propertyCount": row["property_count"],
|
||||
"todayCalendarCount": row["today_calendar_count"],
|
||||
"pendingTaskCount": row["pending_task_count"],
|
||||
"urgentTaskCount": row["urgent_task_count"],
|
||||
"pendingInsights": row["pending_insights"],
|
||||
"pendingTranscriptions": row["pending_transcriptions"],
|
||||
},
|
||||
"meta": {"generatedAt": datetime.now(timezone.utc).isoformat()},
|
||||
}
|
||||
|
||||
|
||||
@dashboard_router.post("/offline-replay/audit", summary="Publish native offline replay queue observability")
|
||||
async def publish_offline_replay_audit(
|
||||
body: OfflineReplayAuditRequest,
|
||||
request: Request,
|
||||
user=Depends(get_current_user),
|
||||
):
|
||||
pool = _pool(request)
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS mobile_offline_replay_audits (
|
||||
audit_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
pending_count INT NOT NULL,
|
||||
records JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
"""
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO mobile_offline_replay_audits (tenant_id, user_id, pending_count, records)
|
||||
VALUES ($1, $2, $3, $4::jsonb)
|
||||
""",
|
||||
user.tenant_id,
|
||||
user.user_id,
|
||||
body.pendingCount,
|
||||
json.dumps([record.model_dump() for record in body.records]),
|
||||
)
|
||||
return {"status": "ok", "pendingCount": body.pendingCount}
|
||||
|
||||
|
||||
# ── Pydantic Models ───────────────────────────────────────────────────────────
|
||||
|
||||
VALID_ACTION_TYPES = {
|
||||
@@ -254,7 +131,7 @@ async def get_health(
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"timestamp": datetime.now(UTC).isoformat(),
|
||||
"database": {
|
||||
"connected": True,
|
||||
"latency_ms": db_latency_ms,
|
||||
@@ -314,7 +191,7 @@ async def get_queues(
|
||||
"synthetic_jobs": {r["status"]: r["count"] for r in synthetic_queue},
|
||||
"inventory_batches": {r["status"]: r["count"] for r in inventory_queue},
|
||||
"admin_actions": {r["status"]: r["count"] for r in admin_queue},
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"timestamp": datetime.now(UTC).isoformat(),
|
||||
}
|
||||
|
||||
|
||||
@@ -338,7 +215,7 @@ async def get_installs(
|
||||
)
|
||||
return {
|
||||
"installs": [dict(r) for r in rows],
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"timestamp": datetime.now(UTC).isoformat(),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@ Routes:
|
||||
POST /api/catalyst/auth/meta — OAuth token acquisition
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import hashlib
|
||||
@@ -19,11 +17,9 @@ import logging
|
||||
from typing import Any
|
||||
from datetime import datetime
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||
from fastapi import APIRouter, HTTPException, Query, Request, status
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from backend.auth.dependencies import UserPrincipal, get_current_user
|
||||
from backend.services.ad_network_service import (
|
||||
AdInsight,
|
||||
BidStrategyUpdate,
|
||||
@@ -31,17 +27,6 @@ from backend.services.ad_network_service import (
|
||||
Platform,
|
||||
ad_network_service,
|
||||
)
|
||||
from backend.services.social_posting import (
|
||||
PostRequest,
|
||||
PostStatus,
|
||||
SocialPlatform,
|
||||
SocialPostingConfigurationError,
|
||||
SocialPostingError,
|
||||
get_post,
|
||||
list_posts,
|
||||
publish_content,
|
||||
publish_due_scheduled,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -106,13 +91,6 @@ def _ok(data: Any, meta: dict | None = None) -> dict:
|
||||
return {"status": "ok", "data": data, "meta": meta or {}}
|
||||
|
||||
|
||||
def _get_db_pool(request: Request) -> Any:
|
||||
pool = getattr(request.app.state, "db_pool", None)
|
||||
if pool is None:
|
||||
raise HTTPException(status_code=503, detail="Database unavailable.")
|
||||
return pool
|
||||
|
||||
|
||||
def _sha256_hash(value: str) -> str:
|
||||
"""SHA-256 hash an email for Meta's hashed audience upload."""
|
||||
return hashlib.sha256(value.strip().lower().encode()).hexdigest()
|
||||
@@ -532,91 +510,3 @@ async def meta_oauth(payload: MetaAuthRequest) -> dict:
|
||||
"token_type": token_data.get("token_type", "bearer"),
|
||||
"expires_in": token_data.get("expires_in"),
|
||||
})
|
||||
|
||||
|
||||
# ── 6. Social publishing ─────────────────────────────────────────────────────
|
||||
|
||||
@router.post("/publish", status_code=status.HTTP_201_CREATED, summary="Publish or schedule content to social channels")
|
||||
async def api_publish_content(
|
||||
payload: PostRequest,
|
||||
request: Request,
|
||||
user: UserPrincipal = Depends(get_current_user),
|
||||
) -> dict:
|
||||
try:
|
||||
result = await publish_content(
|
||||
pool=_get_db_pool(request),
|
||||
tenant_id=user.tenant_id,
|
||||
actor_id=user.user_id,
|
||||
payload=payload,
|
||||
)
|
||||
except SocialPostingConfigurationError as exc:
|
||||
raise HTTPException(status_code=503, detail=str(exc)) from exc
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=422, detail=f"Invalid schedule_time: {exc}") from exc
|
||||
except (SocialPostingError, httpx.HTTPError) as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
return _ok(result)
|
||||
|
||||
|
||||
@router.get("/posts", summary="List tenant-scoped social posts")
|
||||
async def api_list_social_posts(
|
||||
request: Request,
|
||||
platform: SocialPlatform | None = Query(default=None),
|
||||
post_status: PostStatus | None = Query(default=None, alias="status"),
|
||||
limit: int = Query(default=50, ge=1, le=200),
|
||||
user: UserPrincipal = Depends(get_current_user),
|
||||
) -> dict:
|
||||
posts = await list_posts(
|
||||
pool=_get_db_pool(request),
|
||||
tenant_id=user.tenant_id,
|
||||
platform=platform,
|
||||
status=post_status,
|
||||
limit=limit,
|
||||
)
|
||||
return _ok(posts, meta={"count": len(posts)})
|
||||
|
||||
|
||||
@router.get("/posts/{post_id}", summary="Get a tenant-scoped social post")
|
||||
async def api_get_social_post(
|
||||
post_id: str,
|
||||
request: Request,
|
||||
user: UserPrincipal = Depends(get_current_user),
|
||||
) -> dict:
|
||||
post = await get_post(pool=_get_db_pool(request), tenant_id=user.tenant_id, post_id=post_id)
|
||||
if post is None:
|
||||
raise HTTPException(status_code=404, detail=f"Social post '{post_id}' not found.")
|
||||
return _ok(post)
|
||||
|
||||
|
||||
@router.get("/scheduled", summary="List scheduled social posts for the authenticated tenant")
|
||||
async def api_scheduled_posts(
|
||||
request: Request,
|
||||
limit: int = Query(default=50, ge=1, le=200),
|
||||
user: UserPrincipal = Depends(get_current_user),
|
||||
) -> dict:
|
||||
posts = await list_posts(
|
||||
pool=_get_db_pool(request),
|
||||
tenant_id=user.tenant_id,
|
||||
status=PostStatus.SCHEDULED,
|
||||
limit=limit,
|
||||
)
|
||||
return _ok(posts, meta={"count": len(posts)})
|
||||
|
||||
|
||||
@router.post("/scheduled/publish-due", summary="Publish due scheduled social posts for the authenticated tenant")
|
||||
async def api_publish_due_scheduled(
|
||||
request: Request,
|
||||
limit: int = Query(default=20, ge=1, le=100),
|
||||
user: UserPrincipal = Depends(get_current_user),
|
||||
) -> dict:
|
||||
try:
|
||||
result = await publish_due_scheduled(
|
||||
pool=_get_db_pool(request),
|
||||
tenant_id=user.tenant_id,
|
||||
limit=limit,
|
||||
)
|
||||
except SocialPostingConfigurationError as exc:
|
||||
raise HTTPException(status_code=503, detail=str(exc)) from exc
|
||||
except (SocialPostingError, httpx.HTTPError) as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
return _ok(result)
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any, Literal
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from backend.auth.dependencies import UserPrincipal, get_current_user
|
||||
from backend.services.colony_gateway import ColonyConfigurationError, ColonyGateway, ColonyGatewayError
|
||||
from backend.services.colony_repository import ColonyRepository
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
MissionType = Literal["oracle_advisory", "crm_lead_intelligence", "catalyst_strategy_brief"]
|
||||
RiskLevel = Literal["low", "medium", "high"]
|
||||
SensitivityClass = Literal["public", "internal", "confidential"]
|
||||
|
||||
|
||||
class MissionCreateRequest(BaseModel):
|
||||
mission_type: MissionType
|
||||
user_goal: str = Field(..., min_length=1, max_length=2000)
|
||||
normalized_goal: str | None = Field(default=None, max_length=2000)
|
||||
origin_surface: str = Field(default="api", min_length=1, max_length=128)
|
||||
actor_role: str | None = Field(default=None, max_length=128)
|
||||
risk_level: RiskLevel = "low"
|
||||
sensitivity_class: SensitivityClass = "internal"
|
||||
time_budget_ms: int = Field(default=30000, gt=0, le=300000)
|
||||
token_budget: int = Field(default=4096, gt=0, le=200000)
|
||||
context_refs: dict[str, Any] = Field(default_factory=dict)
|
||||
requested_outputs: list[str] = Field(default_factory=list)
|
||||
payload: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ApprovalRequest(BaseModel):
|
||||
reason: str | None = Field(default=None, max_length=2000)
|
||||
|
||||
|
||||
def _get_repo(request: Request) -> ColonyRepository:
|
||||
pool = getattr(request.app.state, "db_pool", None)
|
||||
if pool is None:
|
||||
raise HTTPException(status_code=503, detail="Database unavailable.")
|
||||
return ColonyRepository(pool)
|
||||
|
||||
|
||||
def _serialize_mission(row: dict[str, Any], dispatch: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
data = {
|
||||
"mission_id": str(row["mission_id"]),
|
||||
"tenant_id": row["tenant_id"],
|
||||
"mission_type": row["mission_type"],
|
||||
"origin_surface": row["origin_surface"],
|
||||
"actor_id": row["actor_id"],
|
||||
"actor_role": row["actor_role"],
|
||||
"risk_level": row["risk_level"],
|
||||
"sensitivity_class": row["sensitivity_class"],
|
||||
"status": row["status"],
|
||||
"review_status": row["review_status"],
|
||||
"time_budget_ms": row["time_budget_ms"],
|
||||
"token_budget": row["token_budget"],
|
||||
"user_goal": row["user_goal"],
|
||||
"normalized_goal": row["normalized_goal"],
|
||||
"context_refs": row["context_refs"] or {},
|
||||
"requested_outputs": row["requested_outputs"] or [],
|
||||
"payload": row["payload"] or {},
|
||||
"created_at": row["created_at"].isoformat() if row.get("created_at") else None,
|
||||
"updated_at": row["updated_at"].isoformat() if row.get("updated_at") else None,
|
||||
"completed_at": row["completed_at"].isoformat() if row.get("completed_at") else None,
|
||||
}
|
||||
if dispatch is not None:
|
||||
data["dispatch"] = dispatch
|
||||
return data
|
||||
|
||||
|
||||
@router.post("/missions", status_code=status.HTTP_201_CREATED, summary="Create and dispatch a colony mission")
|
||||
async def create_mission(
|
||||
body: MissionCreateRequest,
|
||||
request: Request,
|
||||
user: UserPrincipal = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
repo = _get_repo(request)
|
||||
try:
|
||||
gateway = ColonyGateway()
|
||||
except ColonyConfigurationError as exc:
|
||||
raise HTTPException(status_code=503, detail=str(exc)) from exc
|
||||
|
||||
mission_id = str(uuid.uuid4())
|
||||
mission = {
|
||||
"mission_id": mission_id,
|
||||
"mission_type": body.mission_type,
|
||||
"origin_surface": body.origin_surface,
|
||||
"tenant_id": user.tenant_id,
|
||||
"actor_id": user.user_id,
|
||||
"actor_role": body.actor_role or user.role,
|
||||
"risk_level": body.risk_level,
|
||||
"sensitivity_class": body.sensitivity_class,
|
||||
"time_budget_ms": body.time_budget_ms,
|
||||
"token_budget": body.token_budget,
|
||||
"user_goal": body.user_goal,
|
||||
"normalized_goal": body.normalized_goal or body.user_goal,
|
||||
"context_refs": body.context_refs,
|
||||
"requested_outputs": body.requested_outputs,
|
||||
"payload": body.payload,
|
||||
}
|
||||
row = await repo.create_mission(mission)
|
||||
await repo.log_event(
|
||||
mission_id=mission_id,
|
||||
tenant_id=user.tenant_id,
|
||||
event_type="mission_created",
|
||||
actor=user.user_id,
|
||||
detail={"mission_type": body.mission_type},
|
||||
)
|
||||
|
||||
try:
|
||||
dispatch = await gateway.dispatch_mission(mission)
|
||||
except ColonyGatewayError as exc:
|
||||
failed = await repo.update_status(mission_id, user.tenant_id, "dispatch_failed")
|
||||
await repo.log_event(
|
||||
mission_id=mission_id,
|
||||
tenant_id=user.tenant_id,
|
||||
event_type="mission_dispatch_failed",
|
||||
actor=user.user_id,
|
||||
detail={"error": str(exc)},
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail={
|
||||
"message": str(exc),
|
||||
"mission": _serialize_mission(failed or row),
|
||||
},
|
||||
) from exc
|
||||
|
||||
queued = await repo.update_status(mission_id, user.tenant_id, "queued")
|
||||
await repo.log_event(
|
||||
mission_id=mission_id,
|
||||
tenant_id=user.tenant_id,
|
||||
event_type="mission_dispatched",
|
||||
actor=user.user_id,
|
||||
detail={"dispatch": dispatch},
|
||||
)
|
||||
return {"status": "ok", "data": _serialize_mission(queued or row, dispatch=dispatch)}
|
||||
|
||||
|
||||
@router.get("/missions", summary="List colony missions for the authenticated tenant")
|
||||
async def list_missions(
|
||||
request: Request,
|
||||
limit: int = Query(default=50, ge=1, le=200),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
user: UserPrincipal = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
rows = await _get_repo(request).list_missions(user.tenant_id, limit=limit, offset=offset)
|
||||
return {"status": "ok", "data": [_serialize_mission(row) for row in rows], "meta": {"count": len(rows)}}
|
||||
|
||||
|
||||
@router.get("/missions/{mission_id}", summary="Get a colony mission")
|
||||
async def get_mission(
|
||||
mission_id: str,
|
||||
request: Request,
|
||||
user: UserPrincipal = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
row = await _get_repo(request).get_mission(mission_id, user.tenant_id)
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail=f"Mission '{mission_id}' not found.")
|
||||
return {"status": "ok", "data": _serialize_mission(row)}
|
||||
|
||||
|
||||
@router.get("/missions/{mission_id}/artifacts", summary="Get mission tasks, results, and writeback proposals")
|
||||
async def get_artifacts(
|
||||
mission_id: str,
|
||||
request: Request,
|
||||
user: UserPrincipal = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
artifacts = await _get_repo(request).artifacts(mission_id, user.tenant_id)
|
||||
if not artifacts:
|
||||
raise HTTPException(status_code=404, detail=f"Mission '{mission_id}' not found.")
|
||||
return {"status": "ok", "data": artifacts}
|
||||
|
||||
|
||||
@router.post("/missions/{mission_id}/approve", summary="Approve all pending writeback proposals for a mission")
|
||||
async def approve_writebacks(
|
||||
mission_id: str,
|
||||
body: ApprovalRequest,
|
||||
request: Request,
|
||||
user: UserPrincipal = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
del body
|
||||
repo = _get_repo(request)
|
||||
if await repo.get_mission(mission_id, user.tenant_id) is None:
|
||||
raise HTTPException(status_code=404, detail=f"Mission '{mission_id}' not found.")
|
||||
count = await repo.approve_pending_writebacks(mission_id, user.tenant_id, user.user_id)
|
||||
if count == 0:
|
||||
raise HTTPException(status_code=404, detail="No pending writeback proposals.")
|
||||
await repo.log_event(
|
||||
mission_id=mission_id,
|
||||
tenant_id=user.tenant_id,
|
||||
event_type="writeback_approved",
|
||||
actor=user.user_id,
|
||||
detail={"approved": count},
|
||||
)
|
||||
return {"status": "ok", "data": {"approved": count}}
|
||||
|
||||
|
||||
@router.post("/missions/{mission_id}/reject", summary="Reject all pending writeback proposals for a mission")
|
||||
async def reject_writebacks(
|
||||
mission_id: str,
|
||||
body: ApprovalRequest,
|
||||
request: Request,
|
||||
user: UserPrincipal = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
repo = _get_repo(request)
|
||||
if await repo.get_mission(mission_id, user.tenant_id) is None:
|
||||
raise HTTPException(status_code=404, detail=f"Mission '{mission_id}' not found.")
|
||||
count = await repo.reject_pending_writebacks(
|
||||
mission_id,
|
||||
user.tenant_id,
|
||||
user.user_id,
|
||||
body.reason or "Rejected by operator.",
|
||||
)
|
||||
if count == 0:
|
||||
raise HTTPException(status_code=404, detail="No pending writeback proposals.")
|
||||
await repo.log_event(
|
||||
mission_id=mission_id,
|
||||
tenant_id=user.tenant_id,
|
||||
event_type="writeback_rejected",
|
||||
actor=user.user_id,
|
||||
detail={"rejected": count, "reason": body.reason},
|
||||
)
|
||||
return {"status": "ok", "data": {"rejected": count}}
|
||||
|
||||
|
||||
@router.get("/health", summary="Check colony root persistence and orchestrator connectivity")
|
||||
async def colony_health(
|
||||
request: Request,
|
||||
user: UserPrincipal = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
repo = _get_repo(request)
|
||||
try:
|
||||
gateway = ColonyGateway()
|
||||
service = await gateway.health()
|
||||
except (ColonyConfigurationError, ColonyGatewayError, httpx.HTTPError) as exc: # type: ignore[name-defined]
|
||||
raise HTTPException(status_code=503, detail=str(exc)) from exc
|
||||
rows = await repo.list_missions(user.tenant_id, limit=1, offset=0)
|
||||
return {
|
||||
"status": "ok",
|
||||
"data": {
|
||||
"tenant_id": user.tenant_id,
|
||||
"root_db": "connected",
|
||||
"orchestrator": service,
|
||||
"has_missions": bool(rows),
|
||||
},
|
||||
}
|
||||
@@ -1,841 +0,0 @@
|
||||
"""
|
||||
Velocity Conversations API.
|
||||
|
||||
Native WhatsApp-first communications surface for Velocity WebOS. The routes are
|
||||
provider-abstracted and CRM-aware, while remaining safe to run in mock mode.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
|
||||
from backend.auth.dependencies import UserPrincipal, get_current_user
|
||||
from backend.services.comms_evolution_provider import EvolutionProvider
|
||||
from backend.services.comms_ingest import TranscriptionError, ingest_inbound_message, transcribe_recording as run_transcription
|
||||
from backend.services.comms_provider import MockProvider
|
||||
from backend.services.comms_waha_provider import WahaProvider
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
_SCHEMA_READY = False
|
||||
|
||||
|
||||
class SendMessageBody(BaseModel):
|
||||
messageType: str = "text"
|
||||
body: str
|
||||
mediaUrl: str | None = None
|
||||
templateName: str | None = None
|
||||
templateLanguage: str | None = None
|
||||
|
||||
|
||||
class LinkPersonBody(BaseModel):
|
||||
personId: str
|
||||
|
||||
|
||||
class NoteBody(BaseModel):
|
||||
content: str
|
||||
|
||||
|
||||
class TaskBody(BaseModel):
|
||||
title: str
|
||||
dueAt: str | None = None
|
||||
notes: str | None = None
|
||||
priority: str = "normal"
|
||||
|
||||
|
||||
class SettingsPatch(BaseModel):
|
||||
provider: str | None = None
|
||||
providerBaseUrl: str | None = None
|
||||
providerApiKey: str | None = None
|
||||
instanceId: str | None = None
|
||||
phoneNumberId: str | None = None
|
||||
webhookCallbackUrl: str | None = None
|
||||
webhookSecret: str | None = None
|
||||
defaultAssignmentUserId: str | None = None
|
||||
autoLinkByPhone: bool | None = None
|
||||
createCrmInteractionOnInbound: bool | None = None
|
||||
defaultCountryCode: str | None = None
|
||||
transcriptionProvider: str | None = None
|
||||
|
||||
|
||||
class TranscribeBody(BaseModel):
|
||||
callId: str | None = None
|
||||
recordingUrl: str | None = None
|
||||
|
||||
|
||||
def _get_provider():
|
||||
return _provider_from_config({})
|
||||
|
||||
|
||||
def _provider_from_config(config: dict[str, Any], provider_override: str | None = None):
|
||||
provider = (provider_override or config.get("provider") or os.getenv("COMMS_PROVIDER", "mock")).strip().lower()
|
||||
base_url = (config.get("provider_base_url") or os.getenv("COMMS_PROVIDER_BASE_URL", "")).strip()
|
||||
api_key = (config.get("provider_api_key") or os.getenv("COMMS_PROVIDER_API_KEY", "")).strip()
|
||||
instance_id = (config.get("instance_id") or os.getenv("COMMS_INSTANCE_ID", "")).strip() or None
|
||||
|
||||
if provider == "waha":
|
||||
return WahaProvider(base_url, api_key, instance_id)
|
||||
if provider == "evolution":
|
||||
return EvolutionProvider(base_url, api_key, instance_id)
|
||||
return MockProvider("", "", "mock")
|
||||
|
||||
|
||||
async def _load_config(pool) -> dict[str, Any]:
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow("SELECT value_json FROM comms_settings WHERE key = 'config'")
|
||||
return _json_obj(row["value_json"]) if row else {}
|
||||
|
||||
|
||||
async def _get_provider_for_pool(pool, provider_override: str | None = None):
|
||||
return _provider_from_config(await _load_config(pool), provider_override)
|
||||
|
||||
|
||||
def _camel_settings(config: dict[str, Any], updated_at: datetime | None = None) -> dict[str, Any]:
|
||||
return {
|
||||
"provider": config.get("provider", os.getenv("COMMS_PROVIDER", "mock")),
|
||||
"providerBaseUrl": config.get("provider_base_url", os.getenv("COMMS_PROVIDER_BASE_URL", "")),
|
||||
"providerApiKey": config.get("provider_api_key", ""),
|
||||
"instanceId": config.get("instance_id", os.getenv("COMMS_INSTANCE_ID", "")),
|
||||
"phoneNumberId": config.get("phone_number_id", ""),
|
||||
"webhookCallbackUrl": config.get("webhook_callback_url", "/api/comms/webhooks/{provider}"),
|
||||
"webhookSecretSet": bool(config.get("webhook_secret_hash") or config.get("webhook_secret_set")),
|
||||
"defaultAssignmentUserId": config.get("default_assignment_user_id"),
|
||||
"autoLinkByPhone": bool(config.get("auto_link_by_phone", True)),
|
||||
"createCrmInteractionOnInbound": bool(config.get("create_crm_interaction_on_inbound", True)),
|
||||
"defaultCountryCode": str(config.get("default_country_code", os.getenv("COMMS_DEFAULT_COUNTRY_CODE", "91"))),
|
||||
"mediaStorageDir": config.get("media_storage_dir", os.getenv("COMMS_MEDIA_STORAGE_DIR", "/opt/dlami/nvme/assets/comms")),
|
||||
"transcriptionProvider": config.get("transcription_provider", os.getenv("COMMS_TRANSCRIPTION_PROVIDER", "none")),
|
||||
**({"updatedAt": updated_at.isoformat()} if updated_at else {}),
|
||||
}
|
||||
|
||||
|
||||
def _snake_settings(body: SettingsPatch) -> dict[str, Any]:
|
||||
mapping = {
|
||||
"provider": "provider",
|
||||
"providerBaseUrl": "provider_base_url",
|
||||
"providerApiKey": "provider_api_key",
|
||||
"instanceId": "instance_id",
|
||||
"phoneNumberId": "phone_number_id",
|
||||
"webhookCallbackUrl": "webhook_callback_url",
|
||||
"defaultAssignmentUserId": "default_assignment_user_id",
|
||||
"autoLinkByPhone": "auto_link_by_phone",
|
||||
"createCrmInteractionOnInbound": "create_crm_interaction_on_inbound",
|
||||
"defaultCountryCode": "default_country_code",
|
||||
"transcriptionProvider": "transcription_provider",
|
||||
}
|
||||
raw = body.model_dump(exclude_unset=True)
|
||||
updates: dict[str, Any] = {}
|
||||
for src, dst in mapping.items():
|
||||
if src in raw:
|
||||
updates[dst] = raw[src]
|
||||
if body.webhookSecret is not None:
|
||||
updates["webhook_secret_hash"] = hashlib.sha256(body.webhookSecret.encode()).hexdigest() if body.webhookSecret else ""
|
||||
updates["webhook_secret_set"] = bool(body.webhookSecret)
|
||||
return updates
|
||||
|
||||
|
||||
def _json_obj(value: Any) -> dict[str, Any]:
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
if isinstance(value, str) and value.strip():
|
||||
try:
|
||||
parsed = json.loads(value)
|
||||
return parsed if isinstance(parsed, dict) else {}
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
return {}
|
||||
|
||||
|
||||
def _record_value(row: Any, key: str, default: Any = None) -> Any:
|
||||
try:
|
||||
return row[key]
|
||||
except (KeyError, IndexError, TypeError):
|
||||
return default
|
||||
|
||||
|
||||
def _optional_datetime(value: str | None) -> datetime | None:
|
||||
if not value or not value.strip():
|
||||
return None
|
||||
normalized = value.strip().replace("Z", "+00:00")
|
||||
try:
|
||||
return datetime.fromisoformat(normalized)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=422, detail="dueAt must be an ISO-8601 timestamp.") from exc
|
||||
|
||||
|
||||
async def _thread_context(conn, thread_id: str, tenant_id: str):
|
||||
thread = await conn.fetchrow("SELECT * FROM comms_threads WHERE thread_id = $1::uuid", thread_id)
|
||||
if not thread:
|
||||
raise HTTPException(status_code=404, detail="Thread not found")
|
||||
lead_id = None
|
||||
if thread["person_id"]:
|
||||
lead_id = await conn.fetchval(
|
||||
"""
|
||||
SELECT lead_id
|
||||
FROM crm_leads
|
||||
WHERE person_id = $1::uuid
|
||||
AND tenant_id = $2
|
||||
ORDER BY updated_at DESC NULLS LAST, created_at DESC NULLS LAST
|
||||
LIMIT 1
|
||||
""",
|
||||
thread["person_id"],
|
||||
tenant_id,
|
||||
)
|
||||
return thread, lead_id
|
||||
|
||||
|
||||
async def _ensure_schema(pool) -> None:
|
||||
global _SCHEMA_READY
|
||||
if _SCHEMA_READY:
|
||||
return
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"""
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
CREATE TABLE IF NOT EXISTS comms_threads (
|
||||
thread_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
provider TEXT NOT NULL DEFAULT 'mock',
|
||||
external_thread_id TEXT,
|
||||
person_id UUID NULL REFERENCES crm_people(person_id) ON DELETE SET NULL,
|
||||
phone_e164 TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
channel TEXT NOT NULL DEFAULT 'whatsapp',
|
||||
status TEXT NOT NULL DEFAULT 'open',
|
||||
assigned_user_id UUID NULL,
|
||||
last_message_at TIMESTAMPTZ,
|
||||
unread_count INT NOT NULL DEFAULT 0,
|
||||
metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
ALTER TABLE comms_threads ADD COLUMN IF NOT EXISTS provider TEXT NOT NULL DEFAULT 'mock';
|
||||
ALTER TABLE comms_threads ADD COLUMN IF NOT EXISTS external_thread_id TEXT;
|
||||
ALTER TABLE comms_threads ADD COLUMN IF NOT EXISTS person_id UUID NULL REFERENCES crm_people(person_id) ON DELETE SET NULL;
|
||||
ALTER TABLE comms_threads ADD COLUMN IF NOT EXISTS phone_e164 TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE comms_threads ADD COLUMN IF NOT EXISTS display_name TEXT;
|
||||
ALTER TABLE comms_threads ADD COLUMN IF NOT EXISTS channel TEXT NOT NULL DEFAULT 'whatsapp';
|
||||
ALTER TABLE comms_threads ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'open';
|
||||
ALTER TABLE comms_threads ADD COLUMN IF NOT EXISTS assigned_user_id UUID NULL;
|
||||
ALTER TABLE comms_threads ADD COLUMN IF NOT EXISTS last_message_at TIMESTAMPTZ;
|
||||
ALTER TABLE comms_threads ADD COLUMN IF NOT EXISTS unread_count INT NOT NULL DEFAULT 0;
|
||||
ALTER TABLE comms_threads ADD COLUMN IF NOT EXISTS metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||
ALTER TABLE comms_threads ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||
ALTER TABLE comms_threads ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||
CREATE INDEX IF NOT EXISTS idx_comms_threads_phone_provider ON comms_threads(provider, phone_e164);
|
||||
CREATE INDEX IF NOT EXISTS idx_comms_threads_person ON comms_threads(person_id) WHERE person_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_comms_threads_status ON comms_threads(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_comms_threads_last_message ON comms_threads(last_message_at DESC NULLS LAST);
|
||||
CREATE TABLE IF NOT EXISTS comms_messages (
|
||||
message_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
thread_id UUID NOT NULL REFERENCES comms_threads(thread_id) ON DELETE CASCADE,
|
||||
provider TEXT NOT NULL DEFAULT 'mock',
|
||||
external_message_id TEXT,
|
||||
direction TEXT NOT NULL CHECK (direction IN ('inbound', 'outbound', 'system')),
|
||||
message_type TEXT NOT NULL DEFAULT 'text',
|
||||
body TEXT NOT NULL DEFAULT '',
|
||||
media_url TEXT,
|
||||
media_mime_type TEXT,
|
||||
delivery_status TEXT NOT NULL DEFAULT 'pending',
|
||||
sent_at TIMESTAMPTZ,
|
||||
delivered_at TIMESTAMPTZ,
|
||||
read_at TIMESTAMPTZ,
|
||||
raw_payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
ALTER TABLE comms_messages ADD COLUMN IF NOT EXISTS provider TEXT NOT NULL DEFAULT 'mock';
|
||||
ALTER TABLE comms_messages ADD COLUMN IF NOT EXISTS external_message_id TEXT;
|
||||
ALTER TABLE comms_messages ADD COLUMN IF NOT EXISTS direction TEXT NOT NULL DEFAULT 'system';
|
||||
ALTER TABLE comms_messages ADD COLUMN IF NOT EXISTS message_type TEXT NOT NULL DEFAULT 'text';
|
||||
ALTER TABLE comms_messages ADD COLUMN IF NOT EXISTS body TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE comms_messages ADD COLUMN IF NOT EXISTS media_url TEXT;
|
||||
ALTER TABLE comms_messages ADD COLUMN IF NOT EXISTS media_mime_type TEXT;
|
||||
ALTER TABLE comms_messages ADD COLUMN IF NOT EXISTS delivery_status TEXT NOT NULL DEFAULT 'pending';
|
||||
ALTER TABLE comms_messages ADD COLUMN IF NOT EXISTS sent_at TIMESTAMPTZ;
|
||||
ALTER TABLE comms_messages ADD COLUMN IF NOT EXISTS delivered_at TIMESTAMPTZ;
|
||||
ALTER TABLE comms_messages ADD COLUMN IF NOT EXISTS read_at TIMESTAMPTZ;
|
||||
ALTER TABLE comms_messages ADD COLUMN IF NOT EXISTS raw_payload JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||
ALTER TABLE comms_messages ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||
CREATE INDEX IF NOT EXISTS idx_comms_messages_thread ON comms_messages(thread_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_comms_messages_external ON comms_messages(external_message_id) WHERE external_message_id IS NOT NULL;
|
||||
CREATE TABLE IF NOT EXISTS comms_call_logs (
|
||||
call_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
thread_id UUID NULL REFERENCES comms_threads(thread_id) ON DELETE SET NULL,
|
||||
person_id UUID NULL REFERENCES crm_people(person_id) ON DELETE SET NULL,
|
||||
provider TEXT NOT NULL DEFAULT 'mock',
|
||||
external_call_id TEXT,
|
||||
phone_e164 TEXT NOT NULL,
|
||||
direction TEXT NOT NULL CHECK (direction IN ('inbound', 'outbound')),
|
||||
status TEXT NOT NULL DEFAULT 'completed',
|
||||
started_at TIMESTAMPTZ NOT NULL,
|
||||
ended_at TIMESTAMPTZ,
|
||||
duration_seconds INT,
|
||||
recording_url TEXT,
|
||||
transcript_id UUID,
|
||||
transcript_text TEXT,
|
||||
raw_payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
ALTER TABLE comms_call_logs ADD COLUMN IF NOT EXISTS thread_id UUID NULL REFERENCES comms_threads(thread_id) ON DELETE SET NULL;
|
||||
ALTER TABLE comms_call_logs ADD COLUMN IF NOT EXISTS person_id UUID NULL REFERENCES crm_people(person_id) ON DELETE SET NULL;
|
||||
ALTER TABLE comms_call_logs ADD COLUMN IF NOT EXISTS provider TEXT NOT NULL DEFAULT 'mock';
|
||||
ALTER TABLE comms_call_logs ADD COLUMN IF NOT EXISTS external_call_id TEXT;
|
||||
ALTER TABLE comms_call_logs ADD COLUMN IF NOT EXISTS phone_e164 TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE comms_call_logs ADD COLUMN IF NOT EXISTS direction TEXT NOT NULL DEFAULT 'inbound';
|
||||
ALTER TABLE comms_call_logs ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'completed';
|
||||
ALTER TABLE comms_call_logs ADD COLUMN IF NOT EXISTS started_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||
ALTER TABLE comms_call_logs ADD COLUMN IF NOT EXISTS ended_at TIMESTAMPTZ;
|
||||
ALTER TABLE comms_call_logs ADD COLUMN IF NOT EXISTS duration_seconds INT;
|
||||
ALTER TABLE comms_call_logs ADD COLUMN IF NOT EXISTS recording_url TEXT;
|
||||
ALTER TABLE comms_call_logs ADD COLUMN IF NOT EXISTS transcript_id UUID;
|
||||
ALTER TABLE comms_call_logs ADD COLUMN IF NOT EXISTS transcript_text TEXT;
|
||||
ALTER TABLE comms_call_logs ADD COLUMN IF NOT EXISTS raw_payload JSONB NOT NULL DEFAULT '{}'::jsonb;
|
||||
ALTER TABLE comms_call_logs ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||
CREATE INDEX IF NOT EXISTS idx_comms_call_logs_phone ON comms_call_logs(phone_e164);
|
||||
CREATE INDEX IF NOT EXISTS idx_comms_call_logs_thread ON comms_call_logs(thread_id) WHERE thread_id IS NOT NULL;
|
||||
CREATE TABLE IF NOT EXISTS comms_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value_json JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
INSERT INTO comms_settings (key, value_json)
|
||||
VALUES ('config', '{"provider":"mock","auto_link_by_phone":true,"create_crm_interaction_on_inbound":true,"default_country_code":"91","transcription_provider":"none"}'::jsonb)
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
"""
|
||||
)
|
||||
_SCHEMA_READY = True
|
||||
|
||||
|
||||
async def _pool(request: Request):
|
||||
pool = request.app.state.db_pool
|
||||
if pool is None:
|
||||
raise HTTPException(status_code=503, detail="Database unavailable")
|
||||
await _ensure_schema(pool)
|
||||
return pool
|
||||
|
||||
|
||||
def _row_thread(row) -> dict[str, Any]:
|
||||
return {
|
||||
"threadId": str(row["thread_id"]),
|
||||
"provider": row["provider"],
|
||||
"externalThreadId": row["external_thread_id"],
|
||||
"personId": str(row["person_id"]) if row["person_id"] else None,
|
||||
"phoneE164": row["phone_e164"],
|
||||
"displayName": row["display_name"],
|
||||
"channel": row["channel"],
|
||||
"status": row["status"],
|
||||
"assignedUserId": str(row["assigned_user_id"]) if row["assigned_user_id"] else None,
|
||||
"lastMessageAt": row["last_message_at"].isoformat() if row["last_message_at"] else None,
|
||||
"unreadCount": row["unread_count"],
|
||||
"metadataJson": _json_obj(row["metadata_json"]),
|
||||
"createdAt": row["created_at"].isoformat(),
|
||||
"updatedAt": row["updated_at"].isoformat(),
|
||||
"lastMessagePreview": _record_value(row, "last_message_preview"),
|
||||
"crmPerson": {
|
||||
"id": str(row["person_id"]),
|
||||
"fullName": row["crm_full_name"],
|
||||
"primaryPhone": row["crm_primary_phone"],
|
||||
"primaryEmail": row["crm_primary_email"],
|
||||
"buyerType": row["crm_buyer_type"],
|
||||
"leadStatus": row["crm_lead_status"],
|
||||
"projectName": row["crm_project_name"],
|
||||
} if row["person_id"] else None,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/threads")
|
||||
async def list_threads(
|
||||
request: Request,
|
||||
status: str | None = None,
|
||||
search: str | None = None,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
_: UserPrincipal = Depends(get_current_user),
|
||||
):
|
||||
pool = await _pool(request)
|
||||
limit = max(1, min(limit, 100))
|
||||
offset = max(0, offset)
|
||||
conditions = ["1=1"]
|
||||
values: list[Any] = []
|
||||
if status:
|
||||
values.append(status)
|
||||
conditions.append(f"t.status = ${len(values)}")
|
||||
if search:
|
||||
values.append(f"%{search}%")
|
||||
conditions.append(f"(t.phone_e164 ILIKE ${len(values)} OR t.display_name ILIKE ${len(values)} OR p.full_name ILIKE ${len(values)} OR p.primary_email ILIKE ${len(values)})")
|
||||
where_clause = " AND ".join(conditions)
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
rows = await conn.fetch(
|
||||
f"""
|
||||
SELECT t.*,
|
||||
p.full_name AS crm_full_name,
|
||||
p.primary_email AS crm_primary_email,
|
||||
p.primary_phone AS crm_primary_phone,
|
||||
p.buyer_type AS crm_buyer_type,
|
||||
COALESCE(l.status, '') AS crm_lead_status,
|
||||
(
|
||||
SELECT pi.project_name FROM crm_property_interests pi
|
||||
WHERE pi.person_id = p.person_id
|
||||
ORDER BY pi.priority ASC NULLS LAST, pi.created_at DESC NULLS LAST
|
||||
LIMIT 1
|
||||
) AS crm_project_name,
|
||||
(
|
||||
SELECT m.body FROM comms_messages m
|
||||
WHERE m.thread_id = t.thread_id
|
||||
ORDER BY m.created_at DESC
|
||||
LIMIT 1
|
||||
) AS last_message_preview
|
||||
FROM comms_threads t
|
||||
LEFT JOIN crm_people p ON t.person_id = p.person_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT status FROM crm_leads l
|
||||
WHERE l.person_id = p.person_id
|
||||
ORDER BY l.updated_at DESC NULLS LAST
|
||||
LIMIT 1
|
||||
) l ON TRUE
|
||||
WHERE {where_clause}
|
||||
ORDER BY t.last_message_at DESC NULLS LAST, t.updated_at DESC
|
||||
LIMIT ${len(values)+1} OFFSET ${len(values)+2}
|
||||
""",
|
||||
*values,
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
total = await conn.fetchval(
|
||||
f"SELECT COUNT(*) FROM comms_threads t LEFT JOIN crm_people p ON t.person_id = p.person_id WHERE {where_clause}",
|
||||
*values,
|
||||
)
|
||||
unread = await conn.fetchval("SELECT COALESCE(SUM(unread_count),0)::int FROM comms_threads WHERE status = 'open'")
|
||||
|
||||
return {"threads": [_row_thread(row) for row in rows], "total": total or 0, "unreadTotal": unread or 0}
|
||||
|
||||
|
||||
@router.get("/threads/{thread_id}")
|
||||
async def get_thread(thread_id: str, request: Request, _: UserPrincipal = Depends(get_current_user)):
|
||||
pool = await _pool(request)
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow(
|
||||
"""
|
||||
SELECT t.*, p.full_name AS crm_full_name, p.primary_email AS crm_primary_email,
|
||||
p.primary_phone AS crm_primary_phone, p.buyer_type AS crm_buyer_type,
|
||||
COALESCE(l.status, '') AS crm_lead_status,
|
||||
(
|
||||
SELECT pi.project_name FROM crm_property_interests pi
|
||||
WHERE pi.person_id = p.person_id
|
||||
ORDER BY pi.priority ASC NULLS LAST, pi.created_at DESC NULLS LAST
|
||||
LIMIT 1
|
||||
) AS crm_project_name,
|
||||
NULL::text AS last_message_preview
|
||||
FROM comms_threads t
|
||||
LEFT JOIN crm_people p ON t.person_id = p.person_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT status FROM crm_leads l
|
||||
WHERE l.person_id = p.person_id
|
||||
ORDER BY l.updated_at DESC NULLS LAST
|
||||
LIMIT 1
|
||||
) l ON TRUE
|
||||
WHERE t.thread_id = $1::uuid
|
||||
""",
|
||||
thread_id,
|
||||
)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Thread not found")
|
||||
return _row_thread(row)
|
||||
|
||||
|
||||
@router.get("/threads/{thread_id}/messages")
|
||||
async def list_messages(
|
||||
thread_id: str,
|
||||
request: Request,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
_: UserPrincipal = Depends(get_current_user),
|
||||
):
|
||||
pool = await _pool(request)
|
||||
limit = max(1, min(limit, 200))
|
||||
offset = max(0, offset)
|
||||
async with pool.acquire() as conn:
|
||||
rows = await conn.fetch(
|
||||
"""
|
||||
SELECT * FROM comms_messages
|
||||
WHERE thread_id = $1::uuid
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
""",
|
||||
thread_id,
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
messages = [
|
||||
{
|
||||
"messageId": str(row["message_id"]),
|
||||
"threadId": str(row["thread_id"]),
|
||||
"provider": row["provider"],
|
||||
"externalMessageId": row["external_message_id"],
|
||||
"direction": row["direction"],
|
||||
"messageType": row["message_type"],
|
||||
"body": row["body"],
|
||||
"mediaUrl": row["media_url"],
|
||||
"mediaMimeType": row["media_mime_type"],
|
||||
"deliveryStatus": row["delivery_status"],
|
||||
"sentAt": row["sent_at"].isoformat() if row["sent_at"] else None,
|
||||
"deliveredAt": row["delivered_at"].isoformat() if row["delivered_at"] else None,
|
||||
"readAt": row["read_at"].isoformat() if row["read_at"] else None,
|
||||
"rawPayload": _json_obj(row["raw_payload"]),
|
||||
"createdAt": row["created_at"].isoformat(),
|
||||
}
|
||||
for row in reversed(rows)
|
||||
]
|
||||
return {"messages": messages, "thread": await get_thread(thread_id, request)}
|
||||
|
||||
|
||||
@router.get("/threads/{thread_id}/calls")
|
||||
async def list_thread_calls(
|
||||
thread_id: str,
|
||||
request: Request,
|
||||
limit: int = 25,
|
||||
offset: int = 0,
|
||||
_: UserPrincipal = Depends(get_current_user),
|
||||
):
|
||||
pool = await _pool(request)
|
||||
limit = max(1, min(limit, 100))
|
||||
offset = max(0, offset)
|
||||
async with pool.acquire() as conn:
|
||||
rows = await conn.fetch(
|
||||
"""
|
||||
SELECT *
|
||||
FROM comms_call_logs
|
||||
WHERE thread_id = $1::uuid
|
||||
ORDER BY started_at DESC, created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
""",
|
||||
thread_id,
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
calls = [
|
||||
{
|
||||
"callId": str(row["call_id"]),
|
||||
"threadId": str(row["thread_id"]) if row["thread_id"] else None,
|
||||
"personId": str(row["person_id"]) if row["person_id"] else None,
|
||||
"provider": row["provider"],
|
||||
"externalCallId": row["external_call_id"],
|
||||
"phoneE164": row["phone_e164"],
|
||||
"direction": row["direction"],
|
||||
"status": row["status"],
|
||||
"startedAt": row["started_at"].isoformat(),
|
||||
"endedAt": row["ended_at"].isoformat() if row["ended_at"] else None,
|
||||
"durationSeconds": row["duration_seconds"],
|
||||
"recordingUrl": row["recording_url"],
|
||||
"transcriptId": str(row["transcript_id"]) if row["transcript_id"] else None,
|
||||
"transcriptText": row["transcript_text"],
|
||||
"rawPayload": _json_obj(row["raw_payload"]),
|
||||
"createdAt": row["created_at"].isoformat(),
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
return {"calls": calls, "thread": await get_thread(thread_id, request)}
|
||||
|
||||
|
||||
@router.post("/threads/{thread_id}/messages")
|
||||
async def send_message(
|
||||
thread_id: str,
|
||||
body: SendMessageBody,
|
||||
request: Request,
|
||||
_: UserPrincipal = Depends(get_current_user),
|
||||
):
|
||||
pool = await _pool(request)
|
||||
async with pool.acquire() as conn:
|
||||
thread = await conn.fetchrow("SELECT * FROM comms_threads WHERE thread_id = $1::uuid", thread_id)
|
||||
if not thread:
|
||||
raise HTTPException(status_code=404, detail="Thread not found")
|
||||
provider = await _get_provider_for_pool(pool)
|
||||
result = await provider.send_message(
|
||||
phone=thread["phone_e164"],
|
||||
message=body.body,
|
||||
message_type=body.messageType,
|
||||
media_url=body.mediaUrl,
|
||||
)
|
||||
async with pool.acquire() as conn:
|
||||
msg_id = await conn.fetchval(
|
||||
"""
|
||||
INSERT INTO comms_messages
|
||||
(thread_id, provider, external_message_id, direction, message_type, body, media_url, delivery_status, sent_at)
|
||||
VALUES ($1::uuid, $2, $3, 'outbound', $4, $5, $6, 'sent', NOW())
|
||||
RETURNING message_id
|
||||
""",
|
||||
thread_id,
|
||||
os.getenv("COMMS_PROVIDER", "mock").lower(),
|
||||
result.get("external_message_id"),
|
||||
body.messageType,
|
||||
body.body,
|
||||
body.mediaUrl,
|
||||
)
|
||||
await conn.execute("UPDATE comms_threads SET last_message_at = NOW(), updated_at = NOW() WHERE thread_id = $1::uuid", thread_id)
|
||||
return {"messageId": str(msg_id), "providerResult": result}
|
||||
|
||||
|
||||
@router.post("/threads/{thread_id}/link-person")
|
||||
async def link_person(
|
||||
thread_id: str,
|
||||
body: LinkPersonBody,
|
||||
request: Request,
|
||||
user: UserPrincipal = Depends(get_current_user),
|
||||
):
|
||||
pool = await _pool(request)
|
||||
async with pool.acquire() as conn:
|
||||
exists = await conn.fetchval(
|
||||
"SELECT EXISTS (SELECT 1 FROM crm_people WHERE person_id = $1::uuid AND tenant_id = $2)",
|
||||
body.personId,
|
||||
user.tenant_id,
|
||||
)
|
||||
if not exists:
|
||||
raise HTTPException(status_code=404, detail="CRM person not found")
|
||||
updated = await conn.execute(
|
||||
"UPDATE comms_threads SET person_id = $1::uuid, updated_at = NOW() WHERE thread_id = $2::uuid",
|
||||
body.personId,
|
||||
thread_id,
|
||||
)
|
||||
return {"success": updated.endswith("1"), "threadId": thread_id, "personId": body.personId}
|
||||
|
||||
|
||||
@router.post("/threads/{thread_id}/notes")
|
||||
async def add_note(thread_id: str, body: NoteBody, request: Request, user: UserPrincipal = Depends(get_current_user)):
|
||||
pool = await _pool(request)
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.transaction():
|
||||
thread, lead_id = await _thread_context(conn, thread_id, user.tenant_id)
|
||||
msg_id = await conn.fetchval(
|
||||
"""
|
||||
INSERT INTO comms_messages (thread_id, provider, direction, message_type, body, delivery_status)
|
||||
VALUES ($1::uuid, 'system', 'system', 'text', $2, 'delivered')
|
||||
RETURNING message_id
|
||||
""",
|
||||
thread_id,
|
||||
f"Note: {body.content}",
|
||||
)
|
||||
interaction_id = None
|
||||
canonical_message_id = None
|
||||
if thread["person_id"]:
|
||||
interaction_id = await conn.fetchval(
|
||||
"""
|
||||
INSERT INTO intel_interactions (
|
||||
interaction_id, tenant_id, person_id, lead_id, channel,
|
||||
interaction_type, happened_at, summary, source_ref, metadata_json
|
||||
) VALUES (
|
||||
gen_random_uuid(), COALESCE($1, 'tenant_velocity'), $2::uuid, $3::uuid, 'whatsapp',
|
||||
'operator_note', NOW(), $4, $5, $6::jsonb
|
||||
)
|
||||
RETURNING interaction_id
|
||||
""",
|
||||
user.tenant_id,
|
||||
thread["person_id"],
|
||||
lead_id,
|
||||
body.content,
|
||||
f"comms:{thread_id}",
|
||||
json.dumps({"source": "comms_thread_note", "thread_id": thread_id, "message_id": str(msg_id)}),
|
||||
)
|
||||
canonical_message_id = await conn.fetchval(
|
||||
"""
|
||||
INSERT INTO intel_messages (
|
||||
message_id, interaction_id, thread_id, sender_role, sender_name,
|
||||
message_text, delivered_at, metadata_json
|
||||
) VALUES (
|
||||
gen_random_uuid(), $1::uuid, $2::uuid, 'operator', 'iPad operator',
|
||||
$3, NOW(), $4::jsonb
|
||||
)
|
||||
RETURNING message_id
|
||||
""",
|
||||
interaction_id,
|
||||
thread_id,
|
||||
body.content,
|
||||
json.dumps({"source": "comms_thread_note"}),
|
||||
)
|
||||
return {
|
||||
"messageId": str(msg_id),
|
||||
"canonicalInteractionId": str(interaction_id) if interaction_id else None,
|
||||
"canonicalMessageId": str(canonical_message_id) if canonical_message_id else None,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/threads/{thread_id}/tasks")
|
||||
async def add_task(thread_id: str, body: TaskBody, request: Request, user: UserPrincipal = Depends(get_current_user)):
|
||||
pool = await _pool(request)
|
||||
text = f"Task: {body.title}" + (f" (Due: {body.dueAt})" if body.dueAt else "")
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.transaction():
|
||||
thread, lead_id = await _thread_context(conn, thread_id, user.tenant_id)
|
||||
msg_id = await conn.fetchval(
|
||||
"""
|
||||
INSERT INTO comms_messages (thread_id, provider, direction, message_type, body, delivery_status)
|
||||
VALUES ($1::uuid, 'system', 'system', 'text', $2, 'delivered')
|
||||
RETURNING message_id
|
||||
""",
|
||||
thread_id,
|
||||
text,
|
||||
)
|
||||
reminder_id = None
|
||||
interaction_id = None
|
||||
if thread["person_id"]:
|
||||
interaction_id = await conn.fetchval(
|
||||
"""
|
||||
INSERT INTO intel_interactions (
|
||||
interaction_id, tenant_id, person_id, lead_id, channel,
|
||||
interaction_type, happened_at, summary, source_ref, metadata_json
|
||||
) VALUES (
|
||||
gen_random_uuid(), COALESCE($1, 'tenant_velocity'), $2::uuid, $3::uuid, 'whatsapp',
|
||||
'next_best_action', NOW(), $4, $5, $6::jsonb
|
||||
)
|
||||
RETURNING interaction_id
|
||||
""",
|
||||
user.tenant_id,
|
||||
thread["person_id"],
|
||||
lead_id,
|
||||
body.title,
|
||||
f"comms:{thread_id}",
|
||||
json.dumps({"source": "comms_thread_task", "thread_id": thread_id, "message_id": str(msg_id)}),
|
||||
)
|
||||
reminder_id = await conn.fetchval(
|
||||
"""
|
||||
INSERT INTO intel_reminders (
|
||||
reminder_id, tenant_id, person_id, lead_id, interaction_id,
|
||||
reminder_type, title, notes, due_at, status, priority,
|
||||
created_by_type, created_at
|
||||
) VALUES (
|
||||
gen_random_uuid(), COALESCE($1, 'tenant_velocity'), $2::uuid, $3::uuid, $4::uuid,
|
||||
'follow_up', $5, $6, $7, 'pending', $8, 'human', NOW()
|
||||
)
|
||||
RETURNING reminder_id
|
||||
""",
|
||||
user.tenant_id,
|
||||
thread["person_id"],
|
||||
lead_id,
|
||||
interaction_id,
|
||||
body.title,
|
||||
body.notes,
|
||||
_optional_datetime(body.dueAt),
|
||||
body.priority,
|
||||
)
|
||||
return {
|
||||
"messageId": str(msg_id),
|
||||
"canonicalInteractionId": str(interaction_id) if interaction_id else None,
|
||||
"canonicalReminderId": str(reminder_id) if reminder_id else None,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/webhooks/{provider}")
|
||||
async def receive_webhook(provider: str, request: Request):
|
||||
pool = await _pool(request)
|
||||
raw_body = await request.body()
|
||||
secret = os.getenv("COMMS_WEBHOOK_SECRET", "").strip()
|
||||
if secret:
|
||||
signature = request.headers.get("x-velocity-signature", "")
|
||||
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
|
||||
if not hmac.compare_digest(signature, expected):
|
||||
raise HTTPException(status_code=401, detail="Invalid comms webhook signature")
|
||||
payload = await request.json()
|
||||
provider_impl = await _get_provider_for_pool(pool, provider)
|
||||
normalized = await provider_impl.normalize_webhook(payload)
|
||||
normalized["provider"] = provider
|
||||
return {"received": True, "ingest": await ingest_inbound_message(pool, normalized)}
|
||||
|
||||
|
||||
@router.get("/settings")
|
||||
async def get_settings(request: Request, _: UserPrincipal = Depends(get_current_user)):
|
||||
pool = await _pool(request)
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow("SELECT value_json, updated_at FROM comms_settings WHERE key = 'config'")
|
||||
config = _json_obj(row["value_json"]) if row else {}
|
||||
result = _camel_settings(config, row["updated_at"] if row else None)
|
||||
if result.get("providerApiKey"):
|
||||
result["providerApiKey"] = "********" + str(result["providerApiKey"])[-4:]
|
||||
return result
|
||||
|
||||
|
||||
@router.patch("/settings")
|
||||
async def patch_settings(body: SettingsPatch, request: Request, _: UserPrincipal = Depends(get_current_user)):
|
||||
pool = await _pool(request)
|
||||
updates = _snake_settings(body)
|
||||
if updates.get("provider_api_key", "").startswith("*"):
|
||||
updates.pop("provider_api_key", None)
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow("SELECT value_json FROM comms_settings WHERE key = 'config'")
|
||||
config = _json_obj(row["value_json"]) if row else {}
|
||||
config.update(updates)
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO comms_settings (key, value_json, updated_at)
|
||||
VALUES ('config', $1::jsonb, NOW())
|
||||
ON CONFLICT (key) DO UPDATE SET value_json = EXCLUDED.value_json, updated_at = NOW()
|
||||
""",
|
||||
json.dumps(config),
|
||||
)
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.post("/provider/test")
|
||||
async def test_provider(request: Request, _: UserPrincipal = Depends(get_current_user)):
|
||||
pool = await _pool(request)
|
||||
return await (await _get_provider_for_pool(pool)).test_connection()
|
||||
|
||||
|
||||
@router.post("/recordings/transcribe")
|
||||
async def transcribe_recording(body: TranscribeBody, request: Request, _: UserPrincipal = Depends(get_current_user)):
|
||||
pool = await _pool(request)
|
||||
config = await _load_config(pool)
|
||||
configured_provider = str(config.get("transcription_provider") or "").strip().lower()
|
||||
env_provider = os.getenv("COMMS_TRANSCRIPTION_PROVIDER", "none").strip().lower()
|
||||
provider = env_provider if configured_provider in {"", "none", "disabled"} else configured_provider
|
||||
recording_url = body.recordingUrl
|
||||
if body.callId and not recording_url:
|
||||
async with pool.acquire() as conn:
|
||||
recording_url = await conn.fetchval(
|
||||
"SELECT recording_url FROM comms_call_logs WHERE call_id = $1::uuid",
|
||||
body.callId,
|
||||
)
|
||||
if not recording_url:
|
||||
raise HTTPException(status_code=422, detail="recordingUrl is required when callId has no stored recording_url.")
|
||||
|
||||
try:
|
||||
result = await run_transcription(recording_url, provider=provider)
|
||||
except TranscriptionError as exc:
|
||||
if body.callId:
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"UPDATE comms_call_logs SET transcript_text = $1 WHERE call_id = $2::uuid",
|
||||
f"Transcription failed: {exc}",
|
||||
body.callId,
|
||||
)
|
||||
raise HTTPException(status_code=503, detail=str(exc)) from exc
|
||||
|
||||
if body.callId:
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"""
|
||||
UPDATE comms_call_logs
|
||||
SET transcript_text = $1,
|
||||
raw_payload = COALESCE(raw_payload, '{}'::jsonb) || $2::jsonb
|
||||
WHERE call_id = $3::uuid
|
||||
""",
|
||||
result["text"],
|
||||
json.dumps({"transcription": {"provider": result["provider"], "language": result["language"]}}),
|
||||
body.callId,
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"status": "completed",
|
||||
"message": "Transcription completed.",
|
||||
"callId": body.callId,
|
||||
"recordingUrl": body.recordingUrl,
|
||||
"provider": result["provider"],
|
||||
"language": result["language"],
|
||||
"text": result["text"],
|
||||
"segments": result["segments"],
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user