feat: Ipad app features and Dream Weaver for Velocity WebOS
Some checks failed
Production Readiness / backend-contracts (pull_request) Has been cancelled
Production Readiness / webos-typecheck (pull_request) Has been cancelled
Production Readiness / ipad-parse (pull_request) Has been cancelled

This commit is contained in:
Sayan Datta
2026-04-28 10:59:07 +05:30
parent 184bfa77f8
commit fefe8373ec
117 changed files with 19510 additions and 6383 deletions

26
app/dist/index.html vendored
View File

@@ -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-Bj2Xa_13.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-W2SBxMnB.css">
</head>
<body>
<div id="root"></div>
</body>
<!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-C0KOan5Q.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-CrH2wIGN.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@@ -1 +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/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/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/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"}

View File

@@ -1,18 +1,18 @@
"use client";
import {
require_shim
} from "./chunk-TXHHHGR3.js";
import {
useCallbackRef,
useLayoutEffect2
} from "./chunk-J4JAFMOP.js";
import {
require_react_dom
} from "./chunk-YF4B4G2L.js";
import {
createSlot
} from "./chunk-YWBEB5PG.js";
import "./chunk-2VUH7NEY.js";
import {
require_shim
} from "./chunk-TXHHHGR3.js";
import {
require_react_dom
} from "./chunk-YF4B4G2L.js";
import {
require_jsx_runtime
} from "./chunk-2YVA4HRZ.js";

View File

@@ -3,13 +3,13 @@ import {
useCallbackRef,
useLayoutEffect2
} from "./chunk-J4JAFMOP.js";
import {
require_react_dom
} from "./chunk-YF4B4G2L.js";
import {
composeRefs,
useComposedRefs
} from "./chunk-2VUH7NEY.js";
import {
require_react_dom
} from "./chunk-YF4B4G2L.js";
import {
require_jsx_runtime
} from "./chunk-2YVA4HRZ.js";

View File

@@ -1,12 +1,9 @@
import {
_extends
} from "./chunk-EQCCHGRT.js";
import {
create
} from "./chunk-7GZ4CI6Q.js";
import {
subscribeWithSelector
} from "./chunk-O4L7C4YS.js";
import {
Events
} from "./chunk-OAEA5FZL.js";
import {
addAfterEffect,
addEffect,
@@ -24,6 +21,9 @@ import {
useThree
} from "./chunk-5ESDTKMP.js";
import "./chunk-NJ4V5H3P.js";
import {
subscribeWithSelector
} from "./chunk-O4L7C4YS.js";
import {
AddEquation,
AdditiveBlending,
@@ -218,13 +218,13 @@ import {
WireframeGeometry,
ZeroFactor
} from "./chunk-L3Z576C2.js";
import {
Events
} from "./chunk-OAEA5FZL.js";
import {
require_client
} from "./chunk-6MXH2QM6.js";
import "./chunk-GUQHL3N7.js";
import {
_extends
} from "./chunk-EQCCHGRT.js";
import "./chunk-TXHHHGR3.js";
import "./chunk-YF4B4G2L.js";
import "./chunk-2YVA4HRZ.js";

View File

@@ -1,133 +1,133 @@
{
"hash": "4594f192",
"hash": "d63ca5ca",
"configHash": "1dd3b956",
"lockfileHash": "e8550e82",
"browserHash": "7e7e8c10",
"lockfileHash": "db47663b",
"browserHash": "b8dcfecc",
"optimized": {
"react": {
"src": "../../react/index.js",
"file": "react.js",
"fileHash": "bc0c1f26",
"fileHash": "0c4ff044",
"needsInterop": true
},
"react-dom": {
"src": "../../react-dom/index.js",
"file": "react-dom.js",
"fileHash": "36a8d9c0",
"fileHash": "d9b3477a",
"needsInterop": true
},
"react/jsx-dev-runtime": {
"src": "../../react/jsx-dev-runtime.js",
"file": "react_jsx-dev-runtime.js",
"fileHash": "3d8f6460",
"fileHash": "60584ffa",
"needsInterop": true
},
"react/jsx-runtime": {
"src": "../../react/jsx-runtime.js",
"file": "react_jsx-runtime.js",
"fileHash": "6f4aca26",
"fileHash": "0909256b",
"needsInterop": true
},
"@radix-ui/react-avatar": {
"src": "../../@radix-ui/react-avatar/dist/index.mjs",
"file": "@radix-ui_react-avatar.js",
"fileHash": "2a702dd2",
"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": "a5efb9bf",
"fileHash": "eef7ef00",
"needsInterop": false
},
"@radix-ui/react-slot": {
"src": "../../@radix-ui/react-slot/dist/index.mjs",
"file": "@radix-ui_react-slot.js",
"fileHash": "986d9c0d",
"fileHash": "6745f8b7",
"needsInterop": false
},
"@react-three/drei": {
"src": "../../@react-three/drei/index.js",
"file": "@react-three_drei.js",
"fileHash": "6cd60875",
"fileHash": "62f4e280",
"needsInterop": false
},
"@react-three/fiber": {
"src": "../../@react-three/fiber/dist/react-three-fiber.esm.js",
"file": "@react-three_fiber.js",
"fileHash": "27a7d4df",
"fileHash": "c4b868b0",
"needsInterop": false
},
"class-variance-authority": {
"src": "../../class-variance-authority/dist/index.mjs",
"file": "class-variance-authority.js",
"fileHash": "b0c32b93",
"fileHash": "db4ee666",
"needsInterop": false
},
"clsx": {
"src": "../../clsx/dist/clsx.mjs",
"file": "clsx.js",
"fileHash": "c855e729",
"fileHash": "0a67ca45",
"needsInterop": false
},
"framer-motion": {
"src": "../../framer-motion/dist/es/index.mjs",
"file": "framer-motion.js",
"fileHash": "e0841dfa",
"fileHash": "9694d550",
"needsInterop": false
},
"lucide-react": {
"src": "../../lucide-react/dist/esm/lucide-react.js",
"file": "lucide-react.js",
"fileHash": "4d79a586",
"fileHash": "15d2dc31",
"needsInterop": false
},
"react-dom/client": {
"src": "../../react-dom/client.js",
"file": "react-dom_client.js",
"fileHash": "2e02376b",
"fileHash": "a8f9db58",
"needsInterop": true
},
"react-router-dom": {
"src": "../../react-router-dom/dist/index.mjs",
"file": "react-router-dom.js",
"fileHash": "bd4cf4c4",
"fileHash": "3a519f93",
"needsInterop": false
},
"recharts": {
"src": "../../recharts/es6/index.js",
"file": "recharts.js",
"fileHash": "b44545db",
"fileHash": "1cac0e9f",
"needsInterop": false
},
"sonner": {
"src": "../../sonner/dist/index.mjs",
"file": "sonner.js",
"fileHash": "02632b99",
"fileHash": "1ad92981",
"needsInterop": false
},
"tailwind-merge": {
"src": "../../tailwind-merge/dist/bundle-mjs.mjs",
"file": "tailwind-merge.js",
"fileHash": "ab22bcc4",
"fileHash": "e2d07b44",
"needsInterop": false
},
"three": {
"src": "../../three/build/three.module.js",
"file": "three.js",
"fileHash": "43012f83",
"fileHash": "09fb4882",
"needsInterop": false
},
"zustand": {
"src": "../../zustand/esm/index.mjs",
"file": "zustand.js",
"fileHash": "dbfba0e2",
"fileHash": "4607d0bf",
"needsInterop": false
},
"zustand/middleware": {
"src": "../../zustand/esm/middleware.mjs",
"file": "zustand_middleware.js",
"fileHash": "e524c2dc",
"fileHash": "e4fd4342",
"needsInterop": false
}
},
@@ -135,57 +135,57 @@
"hls-Q6LDPZPT": {
"file": "hls-Q6LDPZPT.js"
},
"chunk-U7P2NEEE": {
"file": "chunk-U7P2NEEE.js"
},
"chunk-J4JAFMOP": {
"file": "chunk-J4JAFMOP.js"
},
"chunk-YWBEB5PG": {
"file": "chunk-YWBEB5PG.js"
},
"chunk-2VUH7NEY": {
"file": "chunk-2VUH7NEY.js"
"chunk-EQCCHGRT": {
"file": "chunk-EQCCHGRT.js"
},
"chunk-7GZ4CI6Q": {
"file": "chunk-7GZ4CI6Q.js"
},
"chunk-O4L7C4YS": {
"file": "chunk-O4L7C4YS.js"
},
"chunk-OAEA5FZL": {
"file": "chunk-OAEA5FZL.js"
},
"chunk-5ESDTKMP": {
"file": "chunk-5ESDTKMP.js"
},
"chunk-NJ4V5H3P": {
"file": "chunk-NJ4V5H3P.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-GUQHL3N7": {
"file": "chunk-GUQHL3N7.js"
},
"chunk-EQCCHGRT": {
"file": "chunk-EQCCHGRT.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"
}

View File

@@ -1,6 +1,3 @@
import {
clsx_default
} from "./chunk-U7P2NEEE.js";
import {
_extends
} from "./chunk-EQCCHGRT.js";
@@ -10,6 +7,9 @@ import {
import {
require_react
} from "./chunk-WUR7D6NS.js";
import {
clsx_default
} from "./chunk-U7P2NEEE.js";
import {
__commonJS,
__export,

View File

@@ -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,
Activity, Check, Link2, WandSparkles,
type LucideIcon,
} from 'lucide-react';
import {
@@ -17,6 +17,7 @@ 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 ─────────────────────────────────────────────────────────────
@@ -917,7 +918,7 @@ function WarRoom() {
// Tab Bar
// ─────────────────────────────────────────────────────────────────────────────
type TabId = 'studio' | 'command' | 'intelligence' | 'war-room' | 'marketing';
type TabId = 'studio' | 'command' | 'intelligence' | 'war-room' | 'marketing' | 'dream-weaver';
const TABS: Array<{ id: TabId; label: string; icon: LucideIcon }> = [
{ id: 'studio', label: 'The Studio', icon: Clapperboard },
@@ -925,6 +926,7 @@ 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 },
];
// ─────────────────────────────────────────────────────────────────────────────
@@ -940,6 +942,7 @@ export function Catalyst() {
'intelligence': <IntelligenceROI />,
'war-room': <WarRoom />,
'marketing': <CatalystMarketingTab />,
'dream-weaver': <CatalystDreamWeaverTab />,
};
return (
@@ -998,8 +1001,8 @@ export function Catalyst() {
</motion.div>
</AnimatePresence>
{/* Live Optimization Feed — always visible */}
<LiveOptimizationFeed />
{/* Live Optimization Feed — hidden on Dream Weaver because generation has its own status surface. */}
{activeTab !== 'dream-weaver' && <LiveOptimizationFeed />}
</section>
);
}

View File

@@ -0,0 +1,524 @@
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>
);
}

View File

@@ -1,3 +1,5 @@
import { buildVelocityHeaders } from '@/lib/velocitySession';
const rawApiBase = import.meta.env.VITE_API_URL?.trim();
const DEPLOYED_BACKEND_ORIGIN = 'https://velocity.desineuron.in';
@@ -75,10 +77,17 @@ export interface MarketingCampaignSummary {
async function requestJson<T>(path: string): Promise<T> {
const response = await fetch(`${API_URL}${path}`, {
headers: { Accept: 'application/json' },
headers: buildVelocityHeaders(undefined, false),
});
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
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}`,
);
}
return response.json() as Promise<T>;
}

View File

@@ -8,19 +8,19 @@ import type {
Client360Snapshot,
CrmOpportunityCard,
CrmTask,
CrmLeadStageUpdate,
KanbanColumn,
ImportBatchSummary,
ImportProposal,
ImportReviewDecision,
QdScoreEntry,
} from '@/types/crmTypes';
import { VELOCITY_TOKEN_KEY } from '@/lib/velocityPlatformClient';
import { buildVelocityHeaders } from '@/lib/velocitySession';
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? '';
function getAuthHeaders(): Record<string, string> {
const token = localStorage.getItem(VELOCITY_TOKEN_KEY);
return token ? { Authorization: `Bearer ${token}` } : {};
return Object.fromEntries(buildVelocityHeaders(undefined, false).entries());
}
async function apiFetch<T>(path: string, options?: RequestInit): Promise<T> {
@@ -87,6 +87,23 @@ 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?: {
@@ -118,6 +135,23 @@ 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[]> {
@@ -125,6 +159,21 @@ 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<{

View File

@@ -0,0 +1,197 @@
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();
}

View File

@@ -1,10 +1,19 @@
import { API_URL } from '@/lib/api';
export const VELOCITY_TOKEN_KEY = 'velocity-api-token';
import {
buildVelocityHeaders,
setVelocityToken,
} from '@/lib/velocitySession';
export {
VELOCITY_TOKEN_KEY,
clearVelocityToken,
getVelocityToken,
setVelocityToken,
} from '@/lib/velocitySession';
export interface VelocityUserProfile {
user_id: string;
role: string;
tenant_id?: string;
full_name?: string | null;
email?: string | null;
avatar_url?: string | null;
@@ -13,6 +22,7 @@ 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;
@@ -148,18 +158,7 @@ export interface InventoryPropertySummary {
}
function buildHeaders(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;
return buildVelocityHeaders(init, includeJson);
}
async function platformFetch<T>(path: string, init?: RequestInit): Promise<T> {
@@ -182,18 +181,6 @@ 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();
}

View File

@@ -0,0 +1,37 @@
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;
}

View File

@@ -17,7 +17,7 @@ import type {
OracleEnvelope,
CanvasPageRevision,
} from '../types/canvas';
import { VELOCITY_TOKEN_KEY } from '@/lib/velocityPlatformClient';
import { VELOCITY_TOKEN_KEY } from '@/lib/velocitySession';
function getBrowserOrigin(): string {
return typeof window !== 'undefined' ? window.location.origin : '';

View File

@@ -15,7 +15,7 @@ interface MarketingState {
adInsights: AdInsight[];
liveEvents: LiveOptimizationEvent[];
settings: CatalystSettings;
activeTab: 'studio' | 'command' | 'intelligence' | 'war-room' | 'marketing';
activeTab: 'studio' | 'command' | 'intelligence' | 'war-room' | 'marketing' | 'dream-weaver';
// Actions
addCampaign: (campaign: Campaign) => void;

View File

@@ -69,6 +69,7 @@ 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
@@ -109,6 +110,16 @@ 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 {