diff --git a/.Agent Context/Bibels/Desineuron Ops Control Plane Bibel.md b/.Agent Context/Bibels/Desineuron Ops Control Plane Bibel.md new file mode 100644 index 00000000..4a9ff39e --- /dev/null +++ b/.Agent Context/Bibels/Desineuron Ops Control Plane Bibel.md @@ -0,0 +1,382 @@ +# Desineuron Ops Control Plane Bibel + +## Chapter Index + +1. Purpose and Operating Model +2. Architecture Map +3. Linux Control-Plane Stack +4. AWS Machine Profiles +5. Market Data and Pricing Logic +6. S3 Asset Model and Bucket Structure +7. Model Hydration Lifecycle +8. Model Ingest From Linux to S3 +9. Route Management Through the t4g Ingress +10. Daily Operations Guide +11. Launching a GPU Box +12. Hydrating a Model +13. Starting ComfyUI or Another Workload +14. Tracking Session Time and Cost +15. CSV Exports and Reporting +16. Failure Recovery Runbooks +17. Security Model and Access Control +18. Adding a New Model +19. Adding a New Instance Profile +20. Adding a New Route or Service +21. Backup and Restore +22. Validated Live Behaviors +23. Operator Retrieval Commands + +## 1. Purpose and Operating Model + +The Desineuron Ops Control Plane is the persistent Linux-hosted operator surface for AWS infrastructure. It centralizes machine launch, model hydration, workload control, cost estimation, route management, and audit history so the team no longer depends on ad hoc Windows terminals or fragile one-off SSH sessions. + +Core planes: + +- Linux box: control plane +- S3: canonical asset plane +- AWS GPU nodes: ephemeral compute plane +- `t4g.micro` ingress: stable public edge + +Current live endpoint: + +- `https://ops.desineuron.in/login` + +Current canonical S3 bucket: + +- `desineuron-ops-control-plane-819079556187-us-east-1` + +## 2. Architecture Map + +```text +Team + -> ops.desineuron.in + -> Linux control plane + -> ops-web + -> ops-api + -> ops-worker + -> ops-db + -> AWS APIs + -> S3 bucket + -> ingress route helper + -> GPU worker nodes +``` + +## 3. Linux Control-Plane Stack + +- Docker Compose stack under `/opt/desineuron-ops-control-plane` +- PostgreSQL stores machine/session/job/audit state +- API and web share the same FastAPI app +- Worker refreshes markets, machines, and session costs +- systemd keeps the stack persistent after reboot + +Primary Linux service: + +- `desineuron-ops-control-plane.service` + +## 4. AWS Machine Profiles + +Initial curated profiles: + +- `g6-xlarge` +- `g6-2xlarge` +- `g6-4xlarge` +- `g6-12xlarge` + +Each profile contains: + +- instance type +- GPU label +- vCPU / RAM +- intended workloads +- launch config: AMI, subnet, SGs, key, role/profile, root volume + +## 5. Market Data and Pricing Logic + +The control plane collects: + +- instance offerings by region +- on-demand pricing from AWS Pricing API +- latest spot price history from EC2 +- runtime state of all visible machines + +Estimated cost model v1: + +- live instance price signal +- gp3 storage cost estimate +- public IPv4/EIP cost estimate + +## 6. S3 Asset Model and Bucket Structure + +Canonical bucket prefixes: + +- `models/` +- `workflows/` +- `references/` +- `outputs/` +- `manifests/` +- `bootstrap/` + +Models are defined in the `model_catalog` table and hydrated to AWS NVMe on demand. + +## 7. Model Hydration Lifecycle + +1. Operator selects machine and model +2. Worker ensures `s5cmd` exists on target +3. Assets copy from S3 to `/opt/dlami/nvme/models/...` +4. Operation result is logged +5. Cache state is stored per machine + +Hydration verification: + +- if a manifest exists at `manifests/models/.json`, the control plane verifies the expected files are present on the GPU node after copy + +## 8. Model Ingest From Linux to S3 + +The control plane can now ingest a real model directory from the Linux box into S3 without manual bucket prep. + +Source of truth on Linux: + +- `/mnt/ServerStorage/ai-models/models` + +Container mount inside the control plane: + +- `/model-library` + +Operator flow: + +1. enter model key +2. enter human label +3. enter source path relative to the Linux model library root +4. optionally set workload and compatibility tags +5. submit `Upload to S3 + Generate Manifest` + +Result: + +- every file is uploaded under `models//` +- manifest JSON is written to `manifests/models/.json` +- the model catalog entry is upserted in PostgreSQL +- future hydrations can use that manifest for verification + +## 9. Route Management Through the t4g Ingress + +The ingress remains the stable public edge. + +Managed route flow: + +1. control plane writes hostname mapping through `manage_desineuron_routes.py` +2. helper renders managed Caddy snippets +3. Caddy reloads +4. route becomes live + +Static Linux-origin routes still flow through the existing tunnel/nginx path. + +## 10. Daily Operations Guide + +- open `ops.desineuron.in` +- log in with internal ops credentials +- review markets and costs +- launch the required GPU profile +- hydrate the model +- start the workload +- map the route if needed +- export session CSVs for accounting or review + +## 11. Launching a GPU Box + +Use the Launch form in the GUI: + +- choose profile +- choose spot or on-demand +- submit + +Result: + +- instance launches with Desineuron tags +- session row is created +- runtime and cost begin tracking +- if spot capacity is unavailable, the UI records a failed launch job and shows an operator-facing error instead of crashing +- the launcher automatically tries sibling subnets in the same VPC instead of hard-failing on one overloaded AZ + +## 12. Hydrating a Model + +Use the Hydrate form: + +- choose machine +- choose model + +Hydration copies from S3 to the instance NVMe path. + +## 13. Starting ComfyUI or Another Workload + +Use the workload form: + +- choose machine +- choose workload + +Current v1 workload profile: + +- `comfyui` + +## 14. Tracking Session Time and Cost + +Each machine session is tracked in the DB and can be exported to CSV. + +Cost components: + +- compute +- storage +- public IPv4 + +v1 note: + +- machine cost is estimate-based +- instance pricing comes from AWS live data where available +- storage and public IPv4 are blended in as estimated infrastructure cost + +## 15. CSV Exports and Reporting + +CSV export path: + +- `exports/sessions_latest.csv` + +Use this for: + +- session duration review +- estimated expenditure review +- internal ops accounting + +Current export path: + +- `/opt/desineuron-ops-control-plane/exports/sessions_latest.csv` + +The export is also logged in the database as a `csv_exports` record. + +## 16. Failure Recovery Runbooks + +If the worker stops: + +- restart the systemd unit on Linux + +If a GPU node is unhealthy: + +- inspect machine state +- inspect workload status +- stop or terminate the node +- relaunch from a clean profile + +If route mapping fails: + +- inspect the ingress helper +- inspect Caddy reload status +- verify the ops container has SSH access to the ingress node + +If redeploy breaks PostgreSQL permissions: + +- verify `/opt/desineuron-ops-control-plane/data/postgres` is owned by UID/GID `999:999` +- restart `desineuron-ops-control-plane.service` +- never sync runtime directories from repo into the live stack + +## 17. Security Model and Access Control + +- app is intended to be private +- secrets stay on Linux, not in repo +- actions are audited +- AWS workers expose only minimal required ports +- operator accounts can be provisioned as email-style usernames for team access + +Current protected secrets: + +- `/opt/desineuron-ops-control-plane/.env` +- `/opt/desineuron-ops-control-plane/state/desineuron-l4-node.pem` + +## 18. Adding a New Model + +Preferred method: + +1. place the model directory under `/mnt/ServerStorage/ai-models/models` +2. use the `Model Library Ingest` form in the ops console +3. let the control plane upload the files, create the manifest, and upsert the catalog entry + +Fallback manual method: + +1. upload to S3 canonical bucket +2. add catalog entry +3. define expected prefix and optional manifest/checksum + +## 19. Adding a New Instance Profile + +1. add curated profile definition +2. set launch config +3. verify market visibility +4. test launch + +## 20. Adding a New Route or Service + +1. define hostname +2. define target backend +3. add route through GUI or helper +4. reload ingress +5. validate health + +If the route is for a new public hostname: + +6. create the Cloudflare DNS record pointing to `98.87.120.120` +7. keep the record in `DNS only` mode +8. validate TLS issuance on first public request + +## 21. Backup and Restore + +Persist: + +- Postgres data +- `.env` +- exported CSVs +- state directory +- route helper state on ingress + +Restore by: + +- recreating the compose stack +- restoring DB data +- restoring config/env +- validating machine, model, and route state + +## 22. Validated Live Behaviors + +As of the latest implementation pass, the following were validated against the live environment: + +- `ops.desineuron.in` login and dashboard render correctly +- `/api/markets/instances`, `/api/markets/pricing`, `/api/sessions`, `/api/costs`, and `/api/exports/csv` return live data +- a `g6.xlarge` on-demand launch was executed through the control plane and then terminated through the same surface +- a `g6.xlarge` spot launch failure was handled cleanly and recorded as `InsufficientInstanceCapacity` +- managed ingress route upsert/delete was executed successfully through the route helper +- session and audit data now persist because API DB writes are committed per request +- a model ingest smoke test uploaded `ops-smoke-model` from the Linux model library into S3 and generated a manifest + +## 23. Operator Retrieval Commands + +Retrieve the admin password on Linux: + +```bash +sudo sed -n 's/^OPS_ADMIN_PASSWORD=//p' /opt/desineuron-ops-control-plane/.env +``` + +Check stack health: + +```bash +sudo systemctl status desineuron-ops-control-plane.service +sudo docker compose -f /opt/desineuron-ops-control-plane/docker-compose.yml ps +``` + +Inspect recent API logs: + +```bash +sudo docker logs --tail 100 desineuron-ops-api +sudo docker logs --tail 100 desineuron-ops-worker +``` + +Inspect exports: + +```bash +ls -lah /opt/desineuron-ops-control-plane/exports +``` diff --git a/.Agent Context/Sprint 1/Kolkata Builder Intel and Meeting Map - April 2026.md b/.Agent Context/Sprint 1/Kolkata Builder Intel and Meeting Map - April 2026.md new file mode 100644 index 00000000..708cf76f --- /dev/null +++ b/.Agent Context/Sprint 1/Kolkata Builder Intel and Meeting Map - April 2026.md @@ -0,0 +1,796 @@ +# Kolkata Builder Intel and Meeting Map - April 2026 + +Document version: 1.0 +Prepared for: Founder outreach and builder meetings +Document date: 2026-04-10 +Prepared by: Codex + +## 1. Purpose + +This document is not about selling to Rohit Darolia or Get My Ghar. It is a practical builder-intel sheet for meetings with Kolkata-region developers and project teams. The goal is to help you walk into each conversation with: + +- the right project context +- the right commercial frame +- the right product wedge +- the right meeting angle +- the right escalation path after the first conversation + +This is founder-use material. It is designed to help you get meetings, shape the first conversation, and move from "interesting AI demo" to "paid pilot / setup discussion." + +## 2. Core Outreach Principle + +Do not pitch all builders the same way. + +Pitch structure should vary by context: + +- premium project: lead with presentation quality, client handling precision, and shortening high-value sales cycles +- mid-market project: lead with lead quality, rep productivity, faster follow-up, and inventory movement +- multi-project builder: lead with property layer first, then show portfolio unlock after the second project +- Kolkata-first relationship: lead with local proof and anti-SaaS private deployment + +Always keep the opening frame as: + +- private deployment +- project-linked intelligence +- sales-cycle compression +- better control over representatives, leads, and presentation quality + +## 3. What To Sell in the First Meeting + +You are not trying to sell the full vision in the first meeting. You are trying to sell one of these: + +1. A founder-led private pilot for one project +2. A one-property setup with a 30-day implementation window +3. A strategic workshop with the sales / marketing / CRM team using their current live pain points + +The first meeting should not feel like: + +- "buy our software" + +It should feel like: + +- "let us show you how your current project sales engine becomes sharper in 30 days without giving your data away" + +## 4. Builder and Project Intel Map + +## 4.1 Eden Realty - Eden Devprayag + +### Public signal + +- Project: `Eden Devprayag` +- Location signal: Shibpur / Howrah riverfront +- Official project messaging positions it as a premium river-facing development with 3, 4 and 5 BHK inventory and strong connectivity to Kolkata core. +- A recent public report also says Eden launched the project alongside a larger `Rs 5,000 crore` Kolkata riverfront vision. + +### Commercial reading + +This is not a generic affordable housing story. This is premium aspiration plus location-led lifestyle positioning. + +### Likely pain points + +- premium lead qualification +- low tolerance for weak reps or slow follow-up +- project storytelling quality +- investor / premium buyer presentation quality +- cross-channel consistency + +### Best Velocity wedge + +Lead with: + +- premium-property presentation intelligence +- high-intent lead ranking +- representative performance discipline +- project-specific iPad demo and media workflows +- executive visibility on premium lead handling + +### Suggested opening line + +"For a premium riverfront project, the cost of one mishandled serious buyer is too high. Velocity helps your sales team identify, present, and follow through on premium leads with far more control." + +### Best first target roles + +- Project Sales Head +- Channel Sales Head +- Marketing Head +- CRM / digital sales operations lead + +### Source links + +- `https://www.eden-devprayag.com/` +- `https://www.millenniumpost.in/bengal/eden-realty-unveils-eden-devprayag-eyes-rs-5k-cr-kolkata-riverfront-vision-648657` + +## 4.2 Sugam Homes - Sugam Prakriti + +### Public signal + +- Project: `Sugam Prakriti` +- Official positioning highlights South Kolkata / Sonarpur side connectivity and 2/3 BHK residential inventory. +- Sugam publicly positions itself as a major Kolkata residential developer. + +### Commercial reading + +This looks like a broader-addressable, family-housing, mid-market sales motion rather than ultra-luxury. + +### Likely pain points + +- large lead pool with uneven quality +- rep responsiveness +- conversion leakage +- source-to-site-visit drop-off +- repeatable pitch discipline across a broader team + +### Best Velocity wedge + +Lead with: + +- lead triage and prioritization +- follow-up gap detection +- rep productivity monitoring +- campaign/source attribution +- project-level operating control for faster movement + +### Suggested opening line + +"For projects with wider funnel volume, the biggest loss is not lack of leads but poor prioritization and inconsistent follow-up. Velocity fixes that operating layer." + +### Best first target roles + +- Sales Head +- Channel Partner sales lead +- CRM Head +- Marketing operations lead + +### Source links + +- `https://prakriti.sugamhomes.com/` +- `https://sugamhomes.com/` + +## 4.3 Atri Group - Atri Aqua + +### Public signal + +- Project: `Atri Aqua` +- Official messaging emphasizes location access and water-led thematic identity. +- Public material points to South Kolkata / Southern Bypass connectivity. + +### Commercial reading + +This is a positioning-heavy residential project where presentation quality and differentiation matter. + +### Likely pain points + +- standing out against nearby residential competition +- making project identity memorable +- pushing reps beyond generic brochure selling +- balancing volume and project storytelling + +### Best Velocity wedge + +Lead with: + +- AI-assisted presentation layer +- property-specific visual and storytelling workflows +- rep guidance on buyer-fit +- project-level lead intelligence + +### Suggested opening line + +"When multiple projects compete in the same decision space, the winner is often the one that presents and follows up with more precision. Velocity helps your team do that repeatedly." + +### Best first target roles + +- Project Director +- Sales Head +- Marketing lead +- promoter / founder if reachable + +### Source links + +- `https://atrigroup.in/atri-aqua/` +- `https://atrigroup.in/` + +## 4.4 Atri Group - Atri Surya Toron + +### Public signal + +- Project: `Atri Surya Toron` +- Public descriptions place it in Boral / Jayanpur, off Southern Bypass, with a larger multi-tower footprint and broader-market positioning. + +### Commercial reading + +This feels like a stronger operational-sales case than a pure premium-brand case. + +### Likely pain points + +- funnel management +- project-scale rep control +- channel alignment +- inventory prioritization +- site-visit to closure conversion + +### Best Velocity wedge + +Lead with: + +- project sales control room +- rep monitoring +- site-visit follow-up intelligence +- stage-by-stage conversion bottleneck visibility + +### Suggested opening line + +"Large residential projects usually do not lose only because demand is weak. They lose because operating discipline collapses between lead, visit, persuasion, and follow-up." + +### Best first target roles + +- Project Sales Head +- Channel Sales Lead +- CRM lead + +### Source links + +- `https://suryatoron.in/` +- `https://www.wbpcb.gov.in/files/Fr-07-2023-07-55-3011th%20SEACnw%20Minutes.pdf` + +## 4.5 Siddha Group - Siddha Suburbia Bungalow + +### Public signal + +- Project known in the market as bungalow/villa-led premium suburban inventory under Siddha branding. +- Your listed price point places it firmly in high-value territory. + +### Commercial reading + +This is a premium storytelling and premium buyer handling opportunity. + +### Likely pain points + +- serious buyer identification +- premium buyer experience +- project presentation depth +- longer and more consultative conversion cycles + +### Best Velocity wedge + +Lead with: + +- premium lead scoring +- executive-quality presentation workflows +- guided project narrative for high-value inventory +- centralized intelligence on high-intent buyers and rep response quality + +### Suggested opening line + +"At this ticket size, even a small improvement in identifying and handling serious buyers creates disproportionate value." + +### Best first target roles + +- Luxury / premium sales lead +- Group sales head +- marketing head + +### Source links + +- Siddha corporate: `https://www.siddhagroup.com/` + +## 4.6 Merlin Group - Merlin Avana + +### Public signal + +- Project: `Merlin Avana` +- Official project messaging positions it near Tollygunge / Motilal Gupta Road and frames it as premium South Kolkata living. +- Merlin publicly presents itself as a long-established developer with projects across multiple Indian cities. + +### Commercial reading + +Merlin is a brand-sensitive, reputation-aware builder. Meeting quality and framing will matter. + +### Likely pain points + +- premium buyer funnel hygiene +- high-quality digital and in-person presentation +- consistency across sales representatives and channel partners +- executive visibility into project demand quality + +### Best Velocity wedge + +Lead with: + +- premium sales operating system +- channel partner and internal team control +- property-specific intelligent presentation layer +- executive monitoring without losing project-level detail + +### Suggested opening line + +"The stronger the brand, the more damaging weak sales execution becomes. Velocity protects brand-level sales quality at the project layer." + +### Best first target roles + +- Sales Director +- Channel Head +- Marketing Director +- digital transformation / CRM lead if present + +### Source links + +- `https://www.merlinprojects.com/projects/avana/` +- `https://www.merlinaquaville.com/` + +## 4.7 DTC Group - DTC Good Earth + +### Public signal + +- Project: `DTC Good Earth` +- Public messaging frames it as a Madhyamgram / North Kolkata project with balanced lifestyle positioning. +- DTC presents itself as a diversified group with a long business history and real-estate operations. + +### Commercial reading + +This is a practical operating-efficiency pitch, not a glamour-first pitch. + +### Likely pain points + +- rep efficiency +- multi-source lead prioritization +- operational leakage between inquiry and visit +- structured follow-up for a broader market segment + +### Best Velocity wedge + +Lead with: + +- lead operations control room +- conversion funnel visibility +- follow-up discipline +- representative accountability + +### Suggested opening line + +"For a project like this, the difference between average and strong inventory movement is not only brand pull. It is how tightly the sales operation behaves every day." + +### Best first target roles + +- Project Sales Lead +- Channel team lead +- CRM / telecalling operations lead + +### Source links + +- `https://dtcgroup.in/dtc-good-earth/` +- `https://dtcgroup.in/contact/` + +## 4.8 Siddha Group - Siddha Serena + +### Public signal + +- Project: `Siddha Serena` +- Official positioning places it in New Town with 2/3/4 BHK inventory and dual-gate access messaging. +- Siddha publicly presents strong brand heritage and award-backed credibility. + +### Commercial reading + +This is a strong fit for a “modern premium project, structured operating intelligence” pitch. + +### Likely pain points + +- managing premium-but-broad demand bands +- aligning marketing and sales +- rep productivity visibility +- maintaining premium project perception at scale + +### Best Velocity wedge + +Lead with: + +- premium-project funnel discipline +- source-to-closure intelligence +- project presentation personalization +- management monitoring layer + +### Suggested opening line + +"For a branded New Town premium project, the strongest differentiator is often not only product quality but how intelligently the sales engine behaves around the product." + +### Best first target roles + +- Project Sales Head +- Marketing Head +- CRM Head + +### Source links + +- `https://www.siddhagroup.com/projects/siddha-serena/` +- `https://www.siddhagroup.com/` + +## 4.9 Siddha Group - Siddha Sky Waterfront + +### Public signal + +- Project: `Siddha Sky` +- Official messaging positions it as a luxury high-rise experience with skyline differentiation and prestige architecture. + +### Commercial reading + +This is ideal for a premium, reputation-preserving, white-glove pitch. + +### Likely pain points + +- premium buyer experience consistency +- VIP lead handling +- long-cycle decision support +- representation quality across internal and channel teams + +### Best Velocity wedge + +Lead with: + +- premium lead qualification +- executive oversight for high-value buyer handling +- high-impact project presentation flows +- property-specific AI-assisted sales storytelling + +### Suggested opening line + +"At this level of inventory, one mishandled high-intent lead costs far more than the software. The operating layer needs to be premium too." + +### Best first target roles + +- luxury sales lead +- business head +- marketing director + +### Source links + +- `https://www.siddhagroup.com/projects/siddha-sky/` +- `https://www.siddhagroup.com/` + +## 4.10 Godrej Properties - Godrej Blue + +### Public signal + +- Project: `Godrej Blue` +- Official Godrej page places it at BL Saha Road / New Alipore in Kolkata and positions it as premium 3 and 4 BHK inventory. +- Godrej Properties is a nationally recognized developer, so the bar for enterprise credibility is much higher. + +### Commercial reading + +Do not approach this like a simple project pitch. Approach it like an enterprise-grade sales modernization wedge. + +### Likely pain points + +- regional project sales performance visibility +- maintaining brand-standard customer handling +- high-quality project presentation +- internal approval and process discipline + +### Best Velocity wedge + +Lead with: + +- one-project pilot framed as enterprise-ready +- private deployment and data sovereignty +- project-linked sales intelligence +- repeatable rollout model for future properties if the pilot works + +### Suggested opening line + +"We are not asking you to adopt a SaaS tool across the organization. We are proposing a private project-level operating layer that can prove value on one property and later scale across more assets." + +### Best first target roles + +- Regional Sales Head +- Project Sales Head +- Digital / transformation lead +- CRM or sales operations leadership + +### Source links + +- `https://www.godrejproperties.com/landing-page/kolkata/residential/godrej-blue/` +- `https://www.godrejproperties.com/` + +## 4.11 DTC Group - DTC Sojon + +### Public signal + +- Project: `DTC Sojon` +- Public messaging places it in the Joka / Behala side and positions it as a large lifestyle residential project. + +### Commercial reading + +This is a project where structured selling and disciplined team behavior should resonate strongly. + +### Likely pain points + +- volume funnel management +- representative productivity +- site-visit conversion +- competition within the Joka / Behala belt + +### Best Velocity wedge + +Lead with: + +- project sales command center +- lead-to-visit-to-booking analytics +- rep-wise performance clarity +- project-specific AI selling assistance + +### Suggested opening line + +"Projects in a competitive suburban belt do not only need more marketing. They need a tighter operating system between lead capture and booking." + +### Best first target roles + +- Project Head +- Channel Head +- Sales operations / CRM lead + +### Source links + +- `https://dtcprojectjoka.com/` +- `https://sojonjoka.com/` + +## 4.12 Shriram Properties - Shriram Grand City + +### Public signal + +- Project: `Shriram Grand City` +- Public material positions it as a very large township-style development at Uttarpara / Hindmotor scale, often described around `314 acres`. +- Shriram is a listed developer and will expect more operational seriousness. + +### Commercial reading + +This is not only a project pitch. This is a township operating pitch. + +### Likely pain points + +- massive inventory movement over time +- phase-wise sales intelligence +- wide lead funnel and long-cycle nurturing +- channel ecosystem complexity +- monitoring across a large master development + +### Best Velocity wedge + +Lead with: + +- township-scale project monitoring +- phase and cluster-level intelligence +- representative and channel control +- long-cycle conversion management + +### Suggested opening line + +"For a development of this scale, the problem is not just sales. It is maintaining intelligence and discipline across a very large and long-lived inventory engine." + +### Best first target roles + +- Project Sales Director +- business head for east / Kolkata +- CRM / digital sales head +- marketing leadership + +### Source links + +- `https://shriram-grandcity.com/` +- `https://www.shriramproperties.com/sites/default/files/assets/pdf/annual_reports/Annual%20Report%20FY23_LoRes.pdf` + +## 4.13 Godrej Properties - Godrej Elevate + +### Public signal + +- Project: `Elevate at Godrej Se7en` +- Official Godrej page places it in Joka near Diamond Harbour Road / Pailan and positions it as a residential offering starting around your listed band. + +### Commercial reading + +This is a cleaner wedge than Godrej Blue because it can be framed as a project-level pilot before larger enterprise adoption. + +### Likely pain points + +- project-level funnel discipline +- source quality +- rep productivity +- conversion in a developing corridor + +### Best Velocity wedge + +Lead with: + +- one-property pilot +- private deployment +- lead quality scoring +- rep workflow discipline +- future multi-property rollout logic if it succeeds + +### Suggested opening line + +"The cleanest way to prove value is to install Velocity against one project, improve sales operating discipline measurably, and then decide how far to expand." + +### Best first target roles + +- Project Sales Head +- regional digital / CRM lead +- transformation-minded sales leader + +### Source links + +- `https://www.godrejproperties.com/kolkata/residential/elevate-godrej-se7en` +- `https://www.godrejproperties.com/blog/experience-elevate-at-godrej-seven-in-kolkata` + +## 4.14 Ambuja Neotia Group - Ambuja Utpaala + +### Public signal + +- Project: `Utpalaa` +- Official site positions it off EM Bypass near Ruby and frames it as a premium residential project. +- Ambuja Neotia is a major Kolkata corporate house with diversified business interests beyond real estate. + +### Commercial reading + +This is a high-credibility, high-bar conversation. Do not pitch this as a startup tool. Pitch it as a private strategic operating layer. + +### Likely pain points + +- premium project conversion quality +- executive visibility without operational clutter +- cross-team sales coordination +- maintaining a premium buyer experience + +### Best Velocity wedge + +Lead with: + +- private deployment and data control +- premium property sales intelligence +- monitoring layer for leadership +- property-specific generation and presentation capability + +### Suggested opening line + +"For a premium urban project, Velocity is not about more dashboards. It is about giving leadership and sales teams a private operating edge that improves how serious buyers are identified, handled, and converted." + +### Best first target roles + +- Business Head +- Project Sales Head +- Marketing Head +- leadership office / strategic initiatives contact if available + +### Source links + +- `https://utpalaa.com/` +- `https://www.ambujaneotia.com/` + +## 4.15 Srijan Group / Srijan Realty + +### Public signal + +- Srijan publicly positions itself as one of the leading eastern India developers, with presence in Kolkata and beyond. +- Public materials describe the company as having evolved from real-estate marketing roots into development. + +### Commercial reading + +Srijan is especially relevant because the company history itself suggests sensitivity to market movement, buyer behavior, and project commercialization. + +### Likely pain points + +- multi-project management +- balancing project-specific selling with higher-level monitoring +- structured growth from one property operating layer into portfolio intelligence + +### Best Velocity wedge + +Lead with: + +- one-project deployment first +- portfolio intelligence unlocked as more properties are added +- private deployment +- channel and sales monitoring discipline + +### Suggested opening line + +"Velocity is strongest when it starts at the property layer and then grows into portfolio intelligence as more projects enter the operating system. That maps well to how serious builders actually scale." + +### Best first target roles + +- Sales Head +- CRM / operations head +- management / promoter office +- digital transformation or strategic initiatives lead + +### Source links + +- `https://www.srijanrealty.com/` +- `https://www.srijanrealty.com/contact/` + +## 5. Builder Priority Ranking for Meetings + +Suggested meeting priority based on strategic leverage, not just prestige: + +1. `Ambuja Neotia` +2. `Godrej Properties` +3. `Siddha Group` +4. `Srijan Realty` +5. `Merlin Group` +6. `Shriram Properties` +7. `Eden Realty` +8. `Sugam Homes` +9. `DTC Group` +10. `Atri Group` + +Reasoning: + +- the top half gives you stronger brand leverage if meetings convert +- the bottom half may convert faster and become easier pilots +- you want a mix of prestige targets and practical early adopters + +## 6. Recommended Meeting Strategy + +### 6.1 First Meeting Goal + +Do not try to close the full commercial package in meeting one. + +Meeting one goal: + +- secure a second conversation with the actual project or sales operating team + +### 6.2 What To Show + +Show only four things: + +1. Project-level monitoring layer +2. Lead prioritization and rep accountability +3. Property-specific presentation / generation layer +4. Portfolio unlock logic after the second property + +### 6.3 What Not To Show Too Early + +Avoid drowning them in: + +- internal architecture +- agent terminology +- too many workflow variants +- broad platform fantasies +- generic AI claims + +## 7. Questions To Ask in Builder Meetings + +Use these to diagnose pain without sounding generic: + +1. Where do you currently lose the most momentum between lead capture and booking? +2. How do you know which representatives are handling the best leads properly? +3. How do you currently decide which project should be pushed for which buyer profile? +4. How consistent is the project presentation experience across your team and channel partners? +5. If you had one project where sales operations became visibly sharper in 30 days, what would that be worth to you? + +## 8. Recommended Close for Meeting One + +Your close should be: + +"Let us take one project, deploy privately, and prove that your sales engine becomes sharper within a month. If we cannot demonstrate operational improvement, we should not ask you to scale it." + +That is far stronger than: + +"Would you like to buy the platform?" + +## 9. Final Founder Recommendation + +For builders, your winning motion is: + +- one project +- one private deployment +- one measurable improvement story +- one clear expansion path + +Do not try to win on features. Win on: + +- seriousness +- privacy +- project-level value +- operational intelligence +- founder conviction + +If you can get the first meaningful builder to say, "show me this on one project," the real sales motion has begun. diff --git a/.Agent Context/Sprint 1/Project Velocity - The Oracle.md b/.Agent Context/Sprint 1/Project Velocity - The Oracle.md new file mode 100644 index 00000000..9167ab63 --- /dev/null +++ b/.Agent Context/Sprint 1/Project Velocity - The Oracle.md @@ -0,0 +1,2319 @@ +# Project Velocity - The Oracle + +## Master Architecture and Implementation Artifact + +Document version: 1.0 +Prepared for: Sprint 1 execution baseline +Artifact status: Authoritative product and engineering specification +Document date: 2026-04-08 +Repository baseline: `Project_Velocity` at workspace root + +## 0. Topic Index + +| Section | Topic | What the section answers | Primary audience | +| --- | --- | --- | --- | +| 1 | Charter and Baseline Reality | What Oracle is, what exists in the repo today, and what this artifact formally replaces | Product, engineering, founders | +| 2 | System Overview and Goals | What success looks like, who uses Oracle, and how the vertical JSON canvas should behave | Product, design, leadership | +| 3 | Functional Requirements | What users, AI, components, canvases, and collaboration flows must actually do | Product, frontend, backend | +| 4 | Nonfunctional Requirements | How fast, safe, scalable, reliable, and observable Oracle must be in production | Platform, backend, security | +| 5 | System Architecture | How the system is layered, which services exist, and how the data flows end to end | Backend, platform, architecture | +| 6 | Data Model and JSON Canvas Schema | What canonical contracts, entities, schemas, and examples define Oracle state | Backend, data, frontend | +| 7 | Premade Components Catalog and AI Synthesis | How templates are organized, learned, synthesized, validated, cached, and promoted | Frontend, AI, product | +| 8 | Nemoclaw Integration and Data Access | How prompts become plans, plans become safe queries, and AI stewardship is constrained | Backend, AI, data governance | +| 9 | Security, Privacy, and Compliance | How identity, authorization, encryption, retention, redaction, and compliance are enforced | Security, compliance, platform | +| 10 | Testing Strategy and Quality Assurance | How Oracle is validated through unit, integration, replay, performance, and security tests | QA, backend, frontend | +| 11 | Deployment, Operations, and Observability | How Oracle is deployed, monitored, traced, rolled back, and operated in production | Platform, SRE, backend | +| 12 | Data Governance, Provenance, and Lineage | How Oracle proves source-to-visualization lineage and preserves accountability | Compliance, data, leadership | +| 13 | API Contracts and Integration Points | What the public and internal interfaces look like, including errors, websocket messages, and flows | Backend, frontend, integrations | +| 14 | Roadmap, Milestones, and Rollout Plan | How Oracle should move from MVP to enterprise-grade rollout with controlled risk | Leadership, product, program owners | +| 15 | Appendices | Reference glossary, dictionary, samples, and transcript evidence for implementation and review | All stakeholders | +| 16 | Detailed Execution Blueprint | How the current codebase should be transformed into the production Oracle implementation | Frontend, backend, platform | +| 17 | Collaboration and Merge Algorithm Specification | How revisions, forks, order indices, conflicts, and merge resolutions behave deterministically | Backend, frontend, QA | +| 18 | CRM Operating Model and Product Behavior | How Oracle should function as a world-class real-estate CRM grounded in first principles | Product, founders, GTM | +| 19 | Delivery Work Packages and Sequencing | How the work should be partitioned, staffed, ordered, and piloted | Program leads, engineering managers | +| 20 | Production Readiness Exit Criteria | What must be true before Oracle is considered production-ready rather than merely demo-ready | Leadership, platform, QA | +| 21 | Repository Cutover and File Mapping | Which existing files are temporary, which new modules should exist, and how to map the artifact into repo work | Engineering leads, implementers | +| 22 | Success Metrics and Adoption Governance | Which product, operational, and business metrics prove Oracle is valuable after launch | Leadership, product, operations | + +## 1. Charter and Baseline Reality + +The Oracle is the AI operating surface for the Velocity Suite CRM. It is not a chat widget bolted onto a dashboard. It is the product surface where a broker, sales director, marketing operator, or compliance reviewer expresses business intent in natural language and receives executable, auditable, shareable data views on a persistent canvas. The core design objective is to give non-technical operators the speed of AI while preserving the control, provenance, and safety expected from a system that owns customer and revenue-critical data. + +The current repository contains a visual shell for Oracle but not a production Oracle system. The frontend implementation in `app/src/app/oracle/page.tsx` and `app/src/lib/oracleQueryClient.ts` presents a polished UI that renders mock responses across a fixed set of views. That UI is valuable as a style and interaction reference, but its data contract is intentionally temporary and must be retired. The active contract today is centered on `OracleQueryResult`, which returns a single synthetic result payload rather than a persistent branchable canvas. This artifact replaces that contract with a canvas-native architecture. + +The active FastAPI runtime in `backend/main.py` does not mount an Oracle router or CRM router for production Oracle flows. Empty placeholder files exist in `backend/api/routes_oracle.py` and `backend/api/routes_crm.py`, and they should be treated as placeholders rather than compatibility commitments. The backend stack that is real today is FastAPI, asyncpg, PostgreSQL, JWT-based auth, and a Nemoclaw client abstraction already used by Sentinel. That Nemoclaw abstraction currently supports a hosted NVIDIA-compatible path as the primary runtime and a compatible endpoint or local fallback by policy. Oracle should reuse that runtime abstraction instead of binding itself to one model vendor or one inference location. + +This specification assumes the canonical operating model named `Hybrid Sovereign`. Under this model, tenant data, regulated execution, and high-sensitivity query operations remain inside a tenant-controlled VPC or on-premise boundary whenever policy demands it. The reasoning layer remains pluggable. A tenant may run Nemoclaw through the current hosted model path, a private compatible endpoint, or a local model runtime, but Oracle contracts, audit behavior, safety rules, and data access controls remain invariant across those runtime choices. + +The product premise is deliberately real-estate CRM first. Oracle is designed for brokerages, developers, sales galleries, and investor-facing teams that want the discipline of Salesforce, the ease and automation feel of HubSpot, and the branch-review rigor of Jira, without forcing brokers into an IT-admin workflow. The database is AI-operated in the sense that ingestion, enrichment, retrieval planning, and routine record maintenance are AI-managed, while structural schema changes remain human-governed through reviewable proposals and migrations. + +## 2. System Overview and Goals + +### 2.1 Product Vision + +The Oracle is the data intelligence layer of Velocity Suite. It receives natural-language questions such as "show me this week's whale leads by source, highlight the brokers with the highest QD-weighted pipeline, and map investor concentration in Palm Jumeirah," converts that request into a verified retrieval plan, executes the plan against authorized tenant data, chooses or synthesizes suitable visualization components, and persists those components to a vertically scrollable JSON canvas. The output is not a transient answer. It is a durable page revision that becomes part of the user's operating workspace and collaboration history. + +The vertical scrollable JSON canvas is a semantic canvas rather than a freeform pixel board. The canonical structure is an ordered array of component objects enriched with layout hints, data bindings, provenance, access controls, and rendering metadata. The visual renderer materializes that JSON into responsive UI sections that stack vertically and support infinite growth over time. Users do not drag arbitrary rectangles on a blank plane. They append, insert, group, review, fork, and merge structured analytical components backed by typed data contracts. + +Every user receives a unique main Oracle canvas when the account is provisioned. That canvas functions as the user's primary analytical branch. Sharing does not expose the live main branch for direct mutation. Sharing creates an isolated fork for the recipient, preserving the contributor's ability to explore, comment, reorder, and extend the page without silently modifying the origin. Any incorporation of fork changes into the main branch occurs through a merge request workflow with provenance, diff review, and explicit merge records. + +### 2.2 Product Principles + +| Source product | Strongest capability | Oracle translation | +| --- | --- | --- | +| Salesforce | Object model discipline, pipeline rigor, role-aware analytics | Oracle preserves a canonical tenant data model, strict permissions, and operational revenue views. | +| HubSpot | Low-friction operator UX, fast setup, automation feel | Oracle turns natural language into guided actions and appendable canvases without forcing users through report builders. | +| Jira | Branching, review, merge history, accountable change management | Oracle treats canvases as revisioned analytical artifacts with forks, merge requests, and immutable audit trails. | + +The result is a CRM that keeps brokers in flow. It reduces menu depth, centralizes the prompt surface, makes data views durable and shareable, and lets AI do the tedious work of query planning, binding, styling, and data enrichment while preserving human review where it matters. + +### 2.3 Intended Users and Roles + +| Role | Core goal inside Oracle | Authority boundary | +| --- | --- | --- | +| Junior Broker | Ask questions, review personal leads, build personal working canvases, request follow-ups | May read assigned datasets and create private forks but cannot approve merges into team templates or review schema proposals. | +| Senior Broker | Operate team canvases, compare lead pools, request synthesized components, review subordinate work | May approve intra-team merges and tenant-private component promotions. | +| Sales Director | Own pipeline visibility, performance reviews, branch governance, approval queues | May approve merge requests into official canvases and approve tenant-level catalog items. | +| Marketing Operator | Analyze campaign-to-lead flow, audience quality, conversion by source | May access campaign, audience, and attribution datasets allowed by tenant policy. | +| Data Steward | Review AI stewardship proposals, data quality anomalies, lineage evidence | May approve schema proposals, derived fields, connector mappings, and retention exceptions. | +| Compliance Reviewer | Audit prompt history, sensitive queries, deletions, and merge provenance | May access immutable audit and lineage views but not change business data. | +| Platform Admin | Operate infrastructure, tenant policy, connector setup, runtime routing | May manage service configuration but not bypass tenant-scoped data access policy. | + +### 2.4 Integration with Velocity Suite + +Oracle is not isolated from the rest of Velocity Suite. Oracle consumes CRM objects, activity logs, campaign signals, vault engagement, inventory snapshots, and Sentinel scores. Dashboard consumes Oracle aggregates for high-level KPIs. Sentinel enriches Oracle leads with QD scores, reactions, visit evidence, and behavioral tags. Inventory provides structured unit, project, and availability datasets to power pricing, absorption, and buyer-fit views. Catalyst contributes campaign, ad, and audience performance. Settings defines policy, identity, connector, and tenant-level defaults. Oracle becomes the analytical and operational convergence layer where those signals are composed. + +### 2.5 Core Interaction Narrative + +The golden path begins when the user submits a prompt from the Oracle page. The prompt and visible page context are packaged as a `PromptExecution` request. Nemoclaw receives the prompt, the semantic model, user role and tenant policy, page context, and allowed data sources. Nemoclaw returns a typed intent classification, a structured retrieval plan, and a visualization plan. The policy engine validates that plan, ensuring all requested entities, joins, row-level scopes, redaction rules, and connectors are authorized. The Data Access Gateway compiles the validated plan into executable queries against the tenant data store. Results are profiled for shape, density, grain, and statistical suitability. The visualization engine resolves the best component template or synthesizes a new one, binds the result data to the component contract, writes the component into a new page revision, and broadcasts the update over the page WebSocket. The renderer appends the component to the vertically scrollable canvas and preserves the revision in the audit trail. + +If the user shares the page, Oracle creates a fork for the recipient at a specific source revision. If the recipient edits the fork, the original page remains unchanged. If the recipient later opens a merge request, Oracle computes a component-aware diff between the fork head, the fork base, and the target branch head. Conflicts are resolved deterministically when changes do not overlap and escalated for manual review when they do. Every accepted merge creates a new target revision and preserves the source branch, merge reasoning, review comments, and lineage. + +### 2.6 Error Handling, Rollback, and Auditability + +Oracle must never silently fail, silently broaden permissions, or silently overwrite user work. If Nemoclaw produces an invalid plan, the user receives a structured failure artifact that explains whether the failure was caused by ambiguity, authorization, query compilation, data unavailability, or model runtime error. If the runtime cannot meet confidence or policy thresholds, Oracle may request clarification or create a "diagnostic notice" component rather than returning fabricated analytics. + +Rollback is revision-based rather than destructive. Every successful write to a canvas creates an immutable page revision. A rollback action restores a prior state by creating a new revision that points to a previously valid component tree. This preserves full auditability while giving users a familiar undo or restore experience. Each prompt, query, component creation, share, fork, merge, deletion, and rollback event receives an immutable audit record with `tenantId`, `actorId`, `entityType`, `entityId`, `correlationId`, `executionId`, and timestamp. + +## 3. Functional Requirements + +### 3.1 Prompt Authoring and User Experience + +The Oracle prompt surface must support analytical prompts, operational prompts, and mixed prompts. Analytical prompts request facts, comparisons, trends, maps, tables, forecasts, or outlier views. Operational prompts request workflow actions such as creating follow-ups, assigning owners, or annotating a lead segment. Mixed prompts combine both, for example asking for a chart and the creation of tasks for the top three leads shown in that chart. + +Prompt input must preserve the existing premium UI style while evolving its contract. The current Oracle page shell should remain visually recognizable, but the canvas area becomes a persistent vertical page rather than a single view swapper. The right-side conversation rail remains the interaction log. The center canvas becomes scrollable, revisioned, and branch-aware. The top insight banner becomes a page status bar that shows branch state, execution status, and the highest priority insight extracted from the latest successful execution. + +Oracle must support prompt context from four sources: the current conversation transcript, explicit filters chosen by the user, the current canvas component selection, and system context such as the active tenant, role, and time zone. Oracle must also support prompt presets for common brokerage flows such as pipeline health, lead-source quality, project absorption, broker performance, investor geography, follow-up gaps, and QD-weighted opportunity ranking. + +### 3.2 Prompt Interpretation and Retrieval Planning + +Nemoclaw must transform each prompt into a deterministic typed plan. The plan includes the intent class, target business entities, required metrics, required dimensions, requested filters, permissible aggregation grain, requested temporal window, required joins, privacy tier, and preferred visualization semantics. Nemoclaw must return structured JSON only. Freeform model prose is not considered executable output. + +The retrieval plan must be compiled against a tenant semantic model that maps natural-language business terms to canonical datasets and fields. Terms such as "hot lead," "broker performance," "whale," "Palm Jumeirah inventory," or "QD-weighted pipeline" are resolved through a semantic dictionary rather than ad hoc prompting alone. This keeps the system stable when underlying schemas evolve and lets Oracle explain exactly which fields and tables were used. + +If a prompt is ambiguous but still recoverable, Oracle must adopt a default assumption strategy that surfaces the assumption explicitly in the response metadata. If the ambiguity would materially change the answer or cross a policy boundary, Oracle must stop execution and ask for clarification. A prompt that references multiple tenants, forbidden data classes, or undeclared external data sources must fail closed. + +### 3.3 Data Access Orchestration + +The Data Access Gateway must be the sole execution path to tenant data. Nemoclaw does not receive raw connector credentials and does not execute arbitrary SQL directly. Nemoclaw produces an abstract retrieval plan. The policy engine validates the plan. The gateway compiles that plan into parameterized SQL, stored procedures, or connector-specific requests under least-privilege service identities. + +In MVP, the first-class datasets are internal Velocity PostgreSQL datasets for leads, users, inventory, activities, campaigns, vault engagement, audit events, and Sentinel signals. External CRM or data warehouse connectors may exist later, but MVP assumes Oracle is the primary CRM rather than an analytics overlay on another CRM. This keeps the system simple, secure, and aligned with the "walled garden" sales thesis. + +### 3.4 Visualization Generation and Canvas Growth + +Oracle must bind data results to modular component objects. A single prompt may produce zero, one, or many components. Zero-component completion is valid when the prompt performs only an operational action and records a task or note. One-component completion is common for single-metric or focused analytical prompts. Multi-component completion is preferred when the prompt requests both overview and supporting detail, such as a KPI tile followed by a bar chart and a detail table. + +The canvas grows vertically. The default placement mode is `append_after_last_visible_component`, which preserves a chronological narrative of inquiry. Oracle also supports `insert_after_component`, `replace_component`, and `group_under_section`, but those modes must be explicit. Every component records `layout.orderIndex`. The component array order and the persisted `orderIndex` must agree. Gaps in order indices are allowed to simplify concurrent insertions and three-way merges. + +### 3.5 Component Lifecycle + +| Lifecycle state | Meaning | Exit rule | +| --- | --- | --- | +| draft | Component exists only in an in-flight execution and is not visible on a committed page | Exits on validation pass or execution failure | +| active | Component is visible on a committed page revision | Exits on supersede, archive, or deletion | +| superseded | Component remains part of history but is no longer the latest operative version | Exits only through retention expiry | +| archived | Component is hidden from default view but recoverable for audits and replay | Exits only through explicit restore or retention deletion | +| revoked | Component failed policy or security review after creation and must not render again | Exits only through replacement or legal deletion workflow | + +Component editing must preserve lineage. If a user changes a title, threshold, color token, aggregation mode, or filter, Oracle creates a new component version rather than mutating history in place. If a component is cloned from the catalog, its provenance records the original `templateId` and version. If it is synthesized from a prompt, provenance records the `PromptExecution` that created it, the style exemplar set, the model runtime, and all source lineage records. + +### 3.6 Premade Catalog and On-Demand Synthesis + +Oracle ships with a tenant-visible catalog of premade components organized by semantics rather than by chart library name. A broker should think in terms such as "pipeline board," "lead heatmap," "investor map," "follow-up queue," or "project absorption trend," not "bar chart variant 17." Each template carries a semantic profile describing the data shapes it accepts, the audience it serves, the interaction rules it supports, accessibility guarantees, and its style signature. + +When no premade template fits the requested data shape or storytelling need, Oracle may synthesize a new component. Synthesis is constrained by the same schema, security, accessibility, and performance contracts as premade templates. The output of synthesis is a first-class `ComponentTemplate` plus a rendered `CanvasComponent` instance. Auto-promotion is allowed only within the originating tenant and only after automated validation passes. The promotion path does not create a global shared component. Cross-tenant sharing of synthesized components is explicitly disabled in this artifact. + +### 3.7 Sharing, Forking, and Merging + +Oracle uses page-branch collaboration. Every page has one main branch and zero or more fork branches. Sharing a page creates a fork for the recipient based on a concrete source revision. The recipient can annotate, insert, reorder, filter, and synthesize components on the fork without modifying the origin. The system records the fork source page, source branch, source revision, recipient, created time, and sharing scope. + +When the recipient wants to merge changes back, Oracle creates a `MergeRequest`. The merge engine performs a three-way comparison across the target head, source head, and fork base. Adds, deletes, and reorders to disjoint components merge automatically. Concurrent edits to the same component identity, the same query descriptor, or the same layout slot become explicit conflicts. The reviewer sees a component-aware diff rather than raw JSON only. The merge UI must show changes in titles, filters, visualization parameters, redaction settings, and order indices. + +Oracle must preserve provenance across merges. A merged component retains its original source branch, source component id, original prompt execution, and merge request id. Silent overwrite is forbidden. If a conflict is unresolved, the merge request cannot complete. If a merge is abandoned, the fork remains intact and auditable. + +### 3.8 Privacy and Cross-Dataset Enforcement + +Cross-tenant exploration is not enabled in MVP. Oracle may combine multiple datasets only when they belong to the same tenant and the requesting role is authorized to see the resulting joined output. If a prompt attempts to blend sensitive lead data with a dataset outside the actor's scope, the plan validator must reject the request before any data fetch occurs. + +Sensitive prompts must carry privacy tier annotations. For example, a prompt involving phone numbers, medical references, passport identifiers, or financial documents triggers enhanced redaction and narrower logging rules. Data at rest and in transit must remain protected at every stage. Prompt payloads, model outputs, data result sets, component objects, and page revisions are all covered by encryption, access control, and audit logging. + +### 3.9 Failure Behavior + +Oracle must degrade gracefully. If the query returns no rows, the system should render a no-result component with assumptions, applied filters, and suggested follow-up prompts. If the query runtime exceeds latency SLOs but the request is still viable, Oracle may return a pending execution state and stream the component when ready. If a component template fails at render time, Oracle must preserve the page revision but replace the failed renderer with a policy-safe fallback notice and emit an alert for operator review. + +## 4. Nonfunctional Requirements + +### 4.1 Service Level Objectives and Error Budgets + +Oracle SLOs are measured on rolling 30-day windows and exclude planned maintenance windows communicated to tenants at least 72 hours in advance. + +| Capability | Availability target | Monthly error budget | Latency or integrity target | +| --- | --- | --- | --- | +| Authentication and page fetch | 99.95% | 21.9 minutes | `GET` page retrieval p95 <= 400 ms for cached metadata | +| Prompt-to-plan path | 99.9% | 43.8 minutes | Prompt intake validation p95 <= 300 ms and Nemoclaw plan generation p95 <= 2.5 s | +| Analytical query execution | 99.9% | 43.8 minutes | Warm query p95 <= 4 s and cold query p95 <= 8 s | +| Component synthesis | 99.5% | 3 hours 39 minutes | Synthesis completion p95 <= 6 s excluding long-running custom ML jobs | +| Component render after response arrival | 99.9% | 43.8 minutes | First component paint p95 <= 1.5 s | +| Canvas persistence | 99.95% | 21.9 minutes | Append or revision create p95 <= 500 ms | +| Fork creation | 99.95% | 21.9 minutes | Fork create p95 <= 1 s | +| Merge diff generation | 99.9% | 43.8 minutes | Merge diff compute p95 <= 2 s | +| Merge integrity | 99.99% | 4.4 minutes | Zero silent overwrite and deterministic merge replay for every accepted merge | + +### 4.2 Reliability and Data Integrity + +Oracle must provide at-least-once event delivery for internal orchestration and exactly-once visible page revision commits. A user may see retries in the execution timeline, but the committed page history must never show duplicate component insertions for a single successful execution. The write path therefore uses idempotency keys such as `clientRequestId`, `executionId`, and `revisionCommitId`. + +Fork integrity is a first-class reliability requirement. A fork must reference an immutable source revision. The source revision may later receive new commits, but the fork base cannot drift. Merge requests must remain reproducible long after creation by storing both source base and target base commit references. Audit trails must remain queryable for at least the retention period configured by the tenant and longer when legal hold is active. + +### 4.3 Scalability and Frontend Performance + +The frontend must support canvases containing up to 5,000 components while preserving 60 fps scrolling on modern desktop hardware and 45 fps on supported tablets. The renderer must use windowed virtualization, retained measurement caches, incremental hydration for expensive components, and compiled template caching keyed by template id and version. Data-heavy components such as tables and maps must independently virtualize rows or markers rather than forcing the entire canvas to absorb their cost. + +The default implementation choice is a dedicated virtualization adapter in the Oracle frontend backed by a React 19 compatible virtual list engine. Because the current repo does not include a virtualization library, the implementation should introduce one under Oracle ownership rather than forcing generic virtualization behavior into shared layout code. The UI must also preserve stable scroll anchors during component insertion, merge replay, and live collaboration updates. + +### 4.4 Security, Privacy, and Compliance Targets + +Oracle must follow zero-trust principles. Every request is authenticated. Every data fetch is authorized. Every service-to-service hop is mutually authenticated. Every data object is encrypted at rest and in transit. Every secret is stored outside source control. Every model runtime receives least-privilege context only. Every sensitive action is logged immutably. + +Multi-tenant isolation is mandatory. Every persisted entity carries `tenantId`, and every query predicate includes tenant scope. JWTs or external identity assertions are never sufficient on their own; the authorization layer must also resolve the effective tenant, role, page access scope, and data policy. Oracle must be capable of operating in GDPR-, CCPA-, and HIPAA-ready environments, even if healthcare mode is a tenant-specific option rather than the default. + +### 4.5 Observability Targets + +All critical paths must emit structured logs, metrics, and distributed traces. Every prompt execution, data access request, component synthesis job, page revision commit, share event, fork event, merge request, and merge completion must be traceable through a shared `correlationId`. Production dashboards must surface request rates, p50 and p95 latencies, error rates, cache hit ratios, component render times, merge conflict rates, and policy rejection counts. Alerts must fire on SLO burn, merge integrity anomalies, repeated model malformed output, data access denials above baseline, and page render crash spikes. + +### 4.6 Documentation, Training, and Developer Experience + +Oracle is successful only if it is operable by engineers and understandable to commercial teams. Developer setup time for a new engineer should be less than 30 minutes using a documented local profile. Every external contract must be versioned and example-backed. Every service must publish OpenAPI or schema registry entries. Every merge-sensitive algorithm must have deterministic replay fixtures. Operator training material should let a sales director understand private pages, sharing, forks, merges, and audit evidence in under 45 minutes. + +## 5. System Architecture + +### 5.1 Current State in the Repository + +The frontend Oracle experience exists today as a styled page with hard-coded view switching and synthetic payloads. The backend Oracle service does not exist as an active mounted runtime. The system does already have a real FastAPI service, asyncpg-backed PostgreSQL integration, authentication dependencies, audit-friendly Sentinel flows, and a Nemoclaw client abstraction. The correct architectural move is therefore not a greenfield platform rewrite. It is the addition of Oracle-specific services and contracts on top of the current backend and frontend stack. + +### 5.2 Reference Architecture + +```text +Browser / Tablet + -> Oracle Prompt Surface + -> Virtualized Canvas Renderer + -> Branch Banner, Merge Review, Presence Rail + -> Oracle WebSocket Client + +Oracle API Layer + -> Canvas Service + -> Prompt Orchestrator + -> Collaboration Service + -> Component Catalog Service + -> Visualization Renderer Service + -> Identity and Access Gateway + +Execution and Data Layer + -> Policy Engine + -> Data Access Gateway + -> PostgreSQL Core Store + -> Object Storage for exports, snapshots, rendered assets + -> Audit and Event Store + +AI and Runtime Layer + -> Nemoclaw Prompt Planner + -> Style Signature Extractor + -> Component Synthesis Engine + -> Validation and Safety Pipeline + +Integration Layer + -> External IdP + -> Notification channels + -> Future external data connectors + -> Model runtime adapters +``` + +### 5.3 Service Responsibilities + +| Service | Responsibility | Repo alignment | +| --- | --- | --- | +| Canvas Service | Owns page creation, revision commits, component ordering, rollback, and page retrieval | New FastAPI router and service layer on top of current backend | +| Prompt Orchestrator | Accepts prompt requests, builds execution context, calls Nemoclaw, and coordinates validations | Extends current Nemoclaw-backed backend patterns | +| Collaboration Service | Owns sharing, fork creation, merge requests, diff generation, review state, and merge commits | New service and websocket namespace | +| Component Catalog Service | Stores premade templates, synthesized templates, style signatures, validation results, and promotion states | New service backed by PostgreSQL and object storage | +| Data Access Gateway | Compiles validated plans into data queries with tenant isolation and redaction | New service boundary, even if colocated in MVP | +| Visualization Renderer | Converts template plus dataset plus style signature into renderable component JSON and UI metadata | New backend module with typed contracts | +| Identity and Access Gateway | Resolves authenticated actor, tenant scope, role, and page permissions | Reuses existing auth foundation and external IdP integration | + +### 5.4 Layer Details + +The presentation layer extends the existing Oracle UI. The right rail conversation pattern, premium glass styling, and current compositional language remain valuable and should become the default style signature seed set. The central area becomes a virtualized vertical canvas. Each component renders from JSON rather than from a global `view` switch. Real-time collaboration indicators show active viewers, branch status, pending merge requests, and execution progress. + +The application layer is responsible for orchestration rather than ad hoc business logic scattered across controllers. The Prompt Orchestrator assembles context. The Canvas Service persists revisions. The Collaboration Service owns branch semantics. The Component Catalog Service owns reusable visual intelligence. The Data Access Gateway and Visualization Renderer are separate bounded contexts because data authorization and UI rendering safety are different concerns even when implemented in the same runtime. + +The data layer uses raw PostgreSQL as the system of record, consistent with the current backend direction. Operational metadata stays relational. Flexible query descriptors, visualization parameters, style signatures, lineage graphs, and audit payloads use JSONB columns. Large artifacts such as exported page snapshots, generated thumbnails, and template bundles live in object storage. Audit and event records are append-only. + +The AI and ML layer is deliberately constrained. Prompt understanding, retrieval planning, style transfer, and template synthesis all use structured outputs. Nemoclaw never bypasses the Data Access Gateway. Style learning is based on exemplar retrieval and style signatures first, not uncontrolled model fine-tuning. This keeps the system deterministic and reviewable in Sprint 1 while leaving room for richer model adaptation later. + +The integration layer begins with the external identity provider, internal notifications, and future connector adapters. MVP should expose internal Velocity datasets first. Later releases may add data warehouse, ERP, finance, or document-system connectors under the same policy engine. + +### 5.5 End-to-End Data Flow + +When a prompt arrives, the frontend sends the prompt text, conversation context, selected branch, and page context to the Prompt Orchestrator. The orchestrator resolves the authenticated user, tenant, role, active page revision, branch state, and semantic model version. Nemoclaw receives only the contextual summary, schema dictionary, style exemplars, and allowed action surface. Nemoclaw returns a structured plan. The policy engine validates the plan. The Data Access Gateway executes the plan using service credentials with row-level tenant predicates. Results return in typed form to the Visualization Renderer. The renderer chooses a template or calls synthesis. The produced component set is validated. The Canvas Service writes a new revision. The Collaboration Service broadcasts the revision over WebSocket. The frontend applies the revision, preserves scroll anchors, and updates the branch state. + +When a share occurs, the Collaboration Service snapshots the source head as fork base and creates a new branch record and recipient page projection. When a merge request is opened, the service computes a three-way diff. If approved, the Canvas Service writes a merge revision to the target branch. If rejected, the fork persists independently, and the review result remains attached to the merge request record. + +### 5.6 Deployment Topology + +Oracle should deploy as containerized services under the existing FastAPI estate, with the option to start as a modular monolith for MVP and split along service boundaries later. Each tenant is assigned a home region for writes. Reads may be served from regional replicas, but all page revision commits and merge commits route to the tenant home region. PostgreSQL uses logical replication to regional replicas. Object storage uses cross-region replication for snapshots and template artifacts. WebSocket presence and event fanout should use an internal event backbone. The default architecture choice is PostgreSQL transactional outbox plus NATS JetStream for internal event distribution because it offers a clear migration path from a monolith to multi-service fanout without introducing a full-streaming stack too early. + +Release strategy is canary by tenant. Feature flags gate prompt-to-canvas, synthesis, sharing, merge review, and auto-promotion independently. Blue-green or canary deployment is acceptable, but the user-visible rollback unit is always the page revision first, the feature flag second, and the service deployment third. + +### 5.7 Architectural Decision Record Style + +| Decision | Chosen approach | Rationale | Rejected alternative | +| --- | --- | --- | --- | +| Oracle data store | Raw PostgreSQL plus JSONB | Matches current backend, enables strict control and auditability | Supabase-first abstraction | +| Runtime model | Hybrid Sovereign with pluggable Nemoclaw runtime | Preserves data sovereignty and vendor flexibility | Hard-coded hosted LLM dependency | +| Collaboration model | Page branches plus merge requests | Delivers reviewable analytics without live overwrite risk | Unrestricted live co-editing on main | +| Canvas model | Semantic vertical JSON canvas | Aligns with AI generation, persistence, and mergeability | Freeform pixel canvas | +| Style learning | Exemplar retrieval plus style signatures | Deterministic, explainable, easy to govern in MVP | Unbounded fine-tuning-first strategy | +| Eventing | Transactional outbox plus NATS JetStream | Durable and evolvable with moderate complexity | Pure in-process events for all stages | + +## 6. Data Model and JSON Canvas Schema + +### 6.1 Persistence Model + +Oracle stores durable page state in a normalized relational core with JSON projections. `canvas_pages` stores stable page identity and ownership. `canvas_page_revisions` stores immutable revision commits. `canvas_components` stores the latest component projection for fast page reads. `component_templates` stores premade and synthesized templates. `prompt_executions`, `merge_requests`, `fork_records`, `lineage_records`, and `audit_events` store execution, collaboration, provenance, and governance evidence. + +The canonical network contracts are JSON-first. The canonical persistence model is relational plus JSONB. This avoids coupling page rendering directly to SQL tables while still allowing efficient filtering, indexing, and audit queries. + +### 6.2 Canonical Oracle Data Contracts + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.velocity.ai/oracle/v1/oracle-data-contracts.schema.json", + "$defs": { + "AuditEvent": { + "type": "object", + "properties": { + "auditEventId": { "type": "string" }, + "tenantId": { "type": "string" }, + "entityType": { "type": "string" }, + "entityId": { "type": "string" }, + "action": { "type": "string" }, + "actorId": { "type": "string" }, + "actorType": { "type": "string", "enum": ["user", "service", "ai"] }, + "correlationId": { "type": "string" }, + "executionId": { "type": "string" }, + "createdAt": { "type": "string", "format": "date-time" }, + "details": { "type": "object", "additionalProperties": true } + }, + "required": ["auditEventId", "tenantId", "entityType", "entityId", "action", "actorId", "actorType", "correlationId", "createdAt"] + }, + "LineageRecord": { + "type": "object", + "properties": { + "lineageRecordId": { "type": "string" }, + "tenantId": { "type": "string" }, + "sourceKind": { "type": "string", "enum": ["table", "view", "materialization", "prompt", "component", "template", "merge_request"] }, + "sourceId": { "type": "string" }, + "transformationType": { "type": "string" }, + "transformationSpecHash": { "type": "string" }, + "producedKind": { "type": "string" }, + "producedId": { "type": "string" }, + "policySnapshotId": { "type": "string" }, + "createdAt": { "type": "string", "format": "date-time" } + }, + "required": ["lineageRecordId", "tenantId", "sourceKind", "sourceId", "transformationType", "producedKind", "producedId", "createdAt"] + }, + "DataSourceDescriptor": { + "type": "object", + "properties": { + "descriptorId": { "type": "string" }, + "sourceType": { "type": "string", "enum": ["postgres", "warehouse", "api", "materialized_view", "derived_dataset"] }, + "connectorId": { "type": "string" }, + "dataset": { "type": "string" }, + "authContextRef": { "type": "string" }, + "queryTemplate": { "type": "string" }, + "queryParameters": { "type": "object", "additionalProperties": true }, + "rowLimit": { "type": "integer", "minimum": 1 }, + "freshnessSlaSeconds": { "type": "integer", "minimum": 0 }, + "cachePolicy": { + "type": "object", + "properties": { + "mode": { "type": "string", "enum": ["none", "ttl", "revision_scoped"] }, + "ttlSeconds": { "type": "integer", "minimum": 0 } + }, + "required": ["mode"] + }, + "privacyTier": { "type": "string", "enum": ["standard", "restricted", "sensitive"] }, + "lineageRefs": { + "type": "array", + "items": { "type": "string" } + } + }, + "required": ["descriptorId", "sourceType", "dataset", "authContextRef", "queryTemplate", "rowLimit", "privacyTier"] + }, + "PromptExecution": { + "type": "object", + "properties": { + "executionId": { "type": "string" }, + "tenantId": { "type": "string" }, + "pageId": { "type": "string" }, + "branchId": { "type": "string" }, + "actorId": { "type": "string" }, + "prompt": { "type": "string" }, + "intentClass": { "type": "string", "enum": ["analytical", "operational", "mixed"] }, + "status": { "type": "string", "enum": ["received", "planning", "validated", "executing", "completed", "failed", "clarification_required"] }, + "modelRuntime": { "type": "string" }, + "semanticModelVersion": { "type": "string" }, + "retrievalPlan": { "type": "object", "additionalProperties": true }, + "visualizationPlan": { "type": "object", "additionalProperties": true }, + "warnings": { "type": "array", "items": { "type": "string" } }, + "createdAt": { "type": "string", "format": "date-time" }, + "completedAt": { "type": "string", "format": "date-time" } + }, + "required": ["executionId", "tenantId", "pageId", "branchId", "actorId", "prompt", "intentClass", "status", "modelRuntime", "semanticModelVersion", "createdAt"] + }, + "ForkRecord": { + "type": "object", + "properties": { + "forkId": { "type": "string" }, + "sourcePageId": { "type": "string" }, + "sourceBranchId": { "type": "string" }, + "sourceRevision": { "type": "integer" }, + "forkPageId": { "type": "string" }, + "forkBranchId": { "type": "string" }, + "recipientUserId": { "type": "string" }, + "createdBy": { "type": "string" }, + "createdAt": { "type": "string", "format": "date-time" }, + "status": { "type": "string", "enum": ["active", "merged", "closed"] } + }, + "required": ["forkId", "sourcePageId", "sourceBranchId", "sourceRevision", "forkPageId", "forkBranchId", "recipientUserId", "createdBy", "createdAt", "status"] + }, + "MergeRequest": { + "type": "object", + "properties": { + "mergeRequestId": { "type": "string" }, + "tenantId": { "type": "string" }, + "sourcePageId": { "type": "string" }, + "sourceBranchId": { "type": "string" }, + "sourceHeadRevision": { "type": "integer" }, + "targetPageId": { "type": "string" }, + "targetBranchId": { "type": "string" }, + "targetBaseRevision": { "type": "integer" }, + "title": { "type": "string" }, + "description": { "type": "string" }, + "status": { "type": "string", "enum": ["open", "changes_requested", "approved", "merged", "closed"] }, + "conflicts": { + "type": "array", + "items": { "type": "object", "additionalProperties": true } + }, + "createdBy": { "type": "string" }, + "reviewedBy": { "type": "string" }, + "createdAt": { "type": "string", "format": "date-time" }, + "updatedAt": { "type": "string", "format": "date-time" } + }, + "required": ["mergeRequestId", "tenantId", "sourcePageId", "sourceBranchId", "sourceHeadRevision", "targetPageId", "targetBranchId", "targetBaseRevision", "title", "status", "createdBy", "createdAt", "updatedAt"] + }, + "ComponentTemplate": { + "type": "object", + "properties": { + "templateId": { "type": "string" }, + "tenantId": { "type": "string" }, + "name": { "type": "string" }, + "category": { "type": "string" }, + "status": { "type": "string", "enum": ["catalog_active", "tenant_draft", "tenant_active", "archived", "revoked"] }, + "origin": { "type": "string", "enum": ["premade", "synthesized", "cloned"] }, + "version": { "type": "string" }, + "acceptedShapes": { "type": "array", "items": { "type": "string" } }, + "styleSignature": { "type": "object", "additionalProperties": true }, + "validationState": { "type": "object", "additionalProperties": true }, + "provenance": { "type": "object", "additionalProperties": true }, + "createdAt": { "type": "string", "format": "date-time" }, + "updatedAt": { "type": "string", "format": "date-time" } + }, + "required": ["templateId", "tenantId", "name", "category", "status", "origin", "version", "acceptedShapes", "createdAt", "updatedAt"] + }, + "UserProfile": { + "type": "object", + "properties": { + "userId": { "type": "string" }, + "tenantId": { "type": "string" }, + "email": { "type": "string", "format": "email" }, + "displayName": { "type": "string" }, + "role": { "type": "string", "enum": ["junior_broker", "senior_broker", "sales_director", "marketing_operator", "data_steward", "compliance_reviewer", "platform_admin"] }, + "timezone": { "type": "string" }, + "locale": { "type": "string" }, + "defaultPageId": { "type": "string" }, + "canvasPreferences": { + "type": "object", + "properties": { + "defaultDensity": { "type": "string", "enum": ["compact", "comfortable"] }, + "defaultPlacementMode": { "type": "string", "enum": ["append_after_last_visible_component", "insert_after_component"] }, + "showLineageBadges": { "type": "boolean" } + }, + "required": ["defaultDensity", "defaultPlacementMode", "showLineageBadges"] + }, + "policyProfileId": { "type": "string" }, + "createdAt": { "type": "string", "format": "date-time" }, + "updatedAt": { "type": "string", "format": "date-time" } + }, + "required": ["userId", "tenantId", "email", "displayName", "role", "timezone", "locale", "defaultPageId", "canvasPreferences", "policyProfileId", "createdAt", "updatedAt"] + }, + "CanvasComponent": { + "type": "object", + "properties": { + "componentId": { "type": "string" }, + "type": { + "type": "string", + "enum": ["kpiTile", "barChart", "lineChart", "scatterPlot", "geoMap", "table", "pipelineBoard", "timeline", "heatmap", "forecastChart", "activityStream", "customMLVisualization", "errorNotice"] + }, + "title": { "type": "string" }, + "description": { "type": "string" }, + "dataSourceDescriptor": { "$ref": "#/$defs/DataSourceDescriptor" }, + "visualizationParameters": { "type": "object", "additionalProperties": true }, + "dataBindings": { + "type": "object", + "properties": { + "dimensions": { "type": "array", "items": { "type": "string" } }, + "measures": { "type": "array", "items": { "type": "string" } }, + "series": { "type": "array", "items": { "type": "string" } }, + "filters": { "type": "array", "items": { "type": "object", "additionalProperties": true } } + }, + "required": ["dimensions", "measures", "series", "filters"] + }, + "version": { "type": "integer", "minimum": 1 }, + "provenance": { + "type": "object", + "properties": { + "originType": { "type": "string", "enum": ["catalog", "prompt_generated", "cloned", "merged", "edited"] }, + "templateId": { "type": "string" }, + "promptExecutionId": { "type": "string" }, + "sourceComponentId": { "type": "string" }, + "sourceBranchId": { "type": "string" }, + "createdBy": { "type": "string" }, + "createdAt": { "type": "string", "format": "date-time" } + }, + "required": ["originType", "createdBy", "createdAt"] + }, + "renderingHints": { + "type": "object", + "properties": { + "estimatedHeightPx": { "type": "integer", "minimum": 80 }, + "skeletonVariant": { "type": "string" }, + "virtualizationPriority": { "type": "integer", "minimum": 1, "maximum": 10 } + }, + "required": ["estimatedHeightPx", "skeletonVariant", "virtualizationPriority"] + }, + "layout": { + "type": "object", + "properties": { + "orderIndex": { "type": "integer", "minimum": 1 }, + "sectionId": { "type": "string" }, + "widthMode": { "type": "string", "enum": ["full", "half", "third"] }, + "minHeightPx": { "type": "integer", "minimum": 80 }, + "stickyHeader": { "type": "boolean" } + }, + "required": ["orderIndex", "sectionId", "widthMode", "minHeightPx", "stickyHeader"] + }, + "accessControls": { + "type": "object", + "properties": { + "visibilityScope": { "type": "string", "enum": ["private", "shared_fork", "tenant_team"] }, + "allowedRoles": { "type": "array", "items": { "type": "string" } }, + "redactionPolicy": { "type": "string" } + }, + "required": ["visibilityScope", "allowedRoles", "redactionPolicy"] + }, + "styleSignature": { + "type": "object", + "properties": { + "theme": { "type": "string" }, + "paletteToken": { "type": "string" }, + "motionProfile": { "type": "string" }, + "density": { "type": "string" }, + "radiusScale": { "type": "string" }, + "typographyScale": { "type": "string" } + }, + "required": ["theme", "paletteToken", "motionProfile", "density", "radiusScale", "typographyScale"] + }, + "validationState": { + "type": "object", + "properties": { + "schema": { "type": "string", "enum": ["pass", "fail"] }, + "policy": { "type": "string", "enum": ["pass", "fail"] }, + "a11y": { "type": "string", "enum": ["pass", "fail"] }, + "performance": { "type": "string", "enum": ["pass", "fail"] }, + "status": { "type": "string", "enum": ["validated", "rejected", "needs_review"] } + }, + "required": ["schema", "policy", "a11y", "performance", "status"] + }, + "auditLog": { + "type": "array", + "items": { "type": "string" } + } + }, + "required": ["componentId", "type", "title", "dataSourceDescriptor", "visualizationParameters", "dataBindings", "version", "provenance", "renderingHints", "layout", "accessControls", "styleSignature", "validationState", "auditLog"] + }, + "CanvasPage": { + "type": "object", + "properties": { + "pageId": { "type": "string" }, + "tenantId": { "type": "string" }, + "ownerId": { "type": "string" }, + "branchId": { "type": "string" }, + "branchName": { "type": "string" }, + "pageType": { "type": "string", "enum": ["main", "fork"] }, + "title": { "type": "string" }, + "createdAt": { "type": "string", "format": "date-time" }, + "updatedAt": { "type": "string", "format": "date-time" }, + "isShared": { "type": "boolean" }, + "forks": { + "type": "array", + "items": { "$ref": "#/$defs/ForkRecord" } + }, + "mainBranchPointer": { + "type": "object", + "properties": { + "pageId": { "type": "string" }, + "branchId": { "type": "string" }, + "revision": { "type": "integer" } + }, + "required": ["pageId", "branchId", "revision"] + }, + "baseRevision": { "type": "integer", "minimum": 0 }, + "headRevision": { "type": "integer", "minimum": 0 }, + "sharingPolicy": { + "type": "object", + "properties": { + "shareMode": { "type": "string", "enum": ["private", "direct_fork_only"] }, + "allowReshare": { "type": "boolean" }, + "defaultForkVisibility": { "type": "string", "enum": ["private", "team"] } + }, + "required": ["shareMode", "allowReshare", "defaultForkVisibility"] + }, + "presence": { + "type": "object", + "properties": { + "activeViewers": { "type": "integer", "minimum": 0 }, + "activeEditors": { "type": "integer", "minimum": 0 }, + "lastPresenceAt": { "type": "string", "format": "date-time" } + }, + "required": ["activeViewers", "activeEditors", "lastPresenceAt"] + }, + "lineage": { + "type": "array", + "items": { "$ref": "#/$defs/LineageRecord" } + }, + "audit": { + "type": "object", + "properties": { + "lastAuditEventId": { "type": "string" }, + "eventCount": { "type": "integer", "minimum": 0 } + }, + "required": ["lastAuditEventId", "eventCount"] + }, + "components": { + "type": "array", + "items": { "$ref": "#/$defs/CanvasComponent" } + } + }, + "required": ["pageId", "tenantId", "ownerId", "branchId", "branchName", "pageType", "title", "createdAt", "updatedAt", "isShared", "forks", "mainBranchPointer", "baseRevision", "headRevision", "sharingPolicy", "presence", "lineage", "audit", "components"] + } + } +} +``` + +### 6.3 Example Component Object: QD-Weighted Source Comparison + +```json +{ + "componentId": "cmp_01JQWE6F4G5BARWHALESRC", + "type": "barChart", + "title": "Whale Leads by Source This Week", + "description": "Compares QD-weighted whale lead volume across lead sources in the current week.", + "dataSourceDescriptor": { + "descriptorId": "dsd_01JQWE6F4G5SRC", + "sourceType": "postgres", + "connectorId": "velocity-core-postgres", + "dataset": "lead_daily_snapshot", + "authContextRef": "authctx_sales_director_team_scope", + "queryTemplate": "select source, sum(qd_weighted_score) as qd_weighted_volume from lead_daily_snapshot where tenant_id = :tenant_id and lead_class = 'whale' and snapshot_date between :week_start and :week_end group by source order by qd_weighted_volume desc", + "queryParameters": { + "tenant_id": "tenant_binghatti_demo", + "week_start": "2026-04-06", + "week_end": "2026-04-12" + }, + "rowLimit": 20, + "freshnessSlaSeconds": 120, + "cachePolicy": { + "mode": "ttl", + "ttlSeconds": 120 + }, + "privacyTier": "standard", + "lineageRefs": ["lin_01JQWE6F4G5LEADSNAP"] + }, + "visualizationParameters": { + "xAxis": "source", + "yAxis": "qd_weighted_volume", + "sort": "desc", + "showLabels": true, + "colorScale": ["#0EA5E9", "#22D3EE"], + "legend": false + }, + "dataBindings": { + "dimensions": ["source"], + "measures": ["qd_weighted_volume"], + "series": [], + "filters": [ + { + "field": "lead_class", + "operator": "=", + "value": "whale" + } + ] + }, + "version": 1, + "provenance": { + "originType": "prompt_generated", + "templateId": "tpl_bar_source_quality_v3", + "promptExecutionId": "pex_01JQWE6F4G5PROMPTA", + "createdBy": "user_sales_director_001", + "createdAt": "2026-04-08T16:40:22Z" + }, + "renderingHints": { + "estimatedHeightPx": 340, + "skeletonVariant": "chart", + "virtualizationPriority": 8 + }, + "layout": { + "orderIndex": 100, + "sectionId": "sec_market_overview", + "widthMode": "half", + "minHeightPx": 320, + "stickyHeader": false + }, + "accessControls": { + "visibilityScope": "private", + "allowedRoles": ["senior_broker", "sales_director", "marketing_operator"], + "redactionPolicy": "aggregate_only" + }, + "styleSignature": { + "theme": "velocity_glass", + "paletteToken": "ocean_signal", + "motionProfile": "calm_reveal", + "density": "comfortable", + "radiusScale": "lg", + "typographyScale": "balanced" + }, + "validationState": { + "schema": "pass", + "policy": "pass", + "a11y": "pass", + "performance": "pass", + "status": "validated" + }, + "auditLog": ["aud_01JQWE6F4G5CREATE", "aud_01JQWE6F4G5VALIDATE"] +} +``` + +### 6.4 Example Component Object: Investor Geography Map + +```json +{ + "componentId": "cmp_01JQWE7H7G9GEOMAPINV", + "type": "geoMap", + "title": "Investor Interest Density by Dubai District", + "description": "Maps high-intent leads with at least one positive Sentinel spike in the last 30 days.", + "dataSourceDescriptor": { + "descriptorId": "dsd_01JQWE7H7G9MAP", + "sourceType": "derived_dataset", + "connectorId": "velocity-core-postgres", + "dataset": "lead_geo_interest_rollup", + "authContextRef": "authctx_sales_director_team_scope", + "queryTemplate": "select district, lat, lng, lead_count, avg_qd_score from lead_geo_interest_rollup where tenant_id = :tenant_id and activity_window = :window and min_interest = :min_interest", + "queryParameters": { + "tenant_id": "tenant_binghatti_demo", + "window": "30d", + "min_interest": "high" + }, + "rowLimit": 100, + "freshnessSlaSeconds": 300, + "cachePolicy": { + "mode": "ttl", + "ttlSeconds": 300 + }, + "privacyTier": "restricted", + "lineageRefs": ["lin_01JQWE7H7G9ROLLUP", "lin_01JQWE7H7G9SENTINEL"] + }, + "visualizationParameters": { + "mapStyle": "dubai_district_heat", + "intensityField": "lead_count", + "tooltipFields": ["district", "lead_count", "avg_qd_score"], + "interactive": true + }, + "dataBindings": { + "dimensions": ["district"], + "measures": ["lead_count", "avg_qd_score"], + "series": ["district"], + "filters": [ + { + "field": "activity_window", + "operator": "=", + "value": "30d" + } + ] + }, + "version": 1, + "provenance": { + "originType": "catalog", + "templateId": "tpl_geo_investor_heat_v2", + "promptExecutionId": "pex_01JQWE7H7G9PROMPTB", + "createdBy": "user_sales_director_001", + "createdAt": "2026-04-08T16:40:23Z" + }, + "renderingHints": { + "estimatedHeightPx": 420, + "skeletonVariant": "map", + "virtualizationPriority": 9 + }, + "layout": { + "orderIndex": 200, + "sectionId": "sec_market_overview", + "widthMode": "half", + "minHeightPx": 400, + "stickyHeader": false + }, + "accessControls": { + "visibilityScope": "private", + "allowedRoles": ["senior_broker", "sales_director"], + "redactionPolicy": "district_level_only" + }, + "styleSignature": { + "theme": "velocity_glass", + "paletteToken": "aqua_signal", + "motionProfile": "calm_reveal", + "density": "comfortable", + "radiusScale": "lg", + "typographyScale": "balanced" + }, + "validationState": { + "schema": "pass", + "policy": "pass", + "a11y": "pass", + "performance": "pass", + "status": "validated" + }, + "auditLog": ["aud_01JQWE7H7G9CREATE", "aud_01JQWE7H7G9VALIDATE"] +} +``` + +### 6.5 Example Canvas Page with Multiple Components + +```json +{ + "pageId": "page_01JQWE9MAINBROKER", + "tenantId": "tenant_binghatti_demo", + "ownerId": "user_sales_director_001", + "branchId": "branch_main", + "branchName": "main", + "pageType": "main", + "title": "Oracle Home - Pipeline and Investor Signals", + "createdAt": "2026-04-08T16:10:00Z", + "updatedAt": "2026-04-08T16:40:24Z", + "isShared": true, + "forks": [ + { + "forkId": "fork_01JQWE9TEAMSHARE", + "sourcePageId": "page_01JQWE9MAINBROKER", + "sourceBranchId": "branch_main", + "sourceRevision": 18, + "forkPageId": "page_01JQWE9FORKBROKER2", + "forkBranchId": "branch_fork_broker2", + "recipientUserId": "user_senior_broker_014", + "createdBy": "user_sales_director_001", + "createdAt": "2026-04-08T16:41:10Z", + "status": "active" + } + ], + "mainBranchPointer": { + "pageId": "page_01JQWE9MAINBROKER", + "branchId": "branch_main", + "revision": 18 + }, + "baseRevision": 0, + "headRevision": 18, + "sharingPolicy": { + "shareMode": "direct_fork_only", + "allowReshare": false, + "defaultForkVisibility": "private" + }, + "presence": { + "activeViewers": 2, + "activeEditors": 1, + "lastPresenceAt": "2026-04-08T16:40:24Z" + }, + "lineage": [ + { + "lineageRecordId": "lin_01JQWE9PAGE", + "tenantId": "tenant_binghatti_demo", + "sourceKind": "prompt", + "sourceId": "pex_01JQWE6F4G5PROMPTA", + "transformationType": "prompt_to_component_bundle", + "transformationSpecHash": "sha256:promptbundleA", + "producedKind": "page_revision", + "producedId": "page_01JQWE9MAINBROKER:18", + "policySnapshotId": "policy_2026_04_08_v4", + "createdAt": "2026-04-08T16:40:24Z" + } + ], + "audit": { + "lastAuditEventId": "aud_01JQWE9REV18", + "eventCount": 44 + }, + "components": [ + { + "componentId": "cmp_01JQWE6F4G5BARWHALESRC", + "type": "barChart", + "title": "Whale Leads by Source This Week", + "description": "Compares QD-weighted whale lead volume across lead sources in the current week.", + "dataSourceDescriptor": { + "descriptorId": "dsd_01JQWE6F4G5SRC", + "sourceType": "postgres", + "connectorId": "velocity-core-postgres", + "dataset": "lead_daily_snapshot", + "authContextRef": "authctx_sales_director_team_scope", + "queryTemplate": "select source, sum(qd_weighted_score) as qd_weighted_volume from lead_daily_snapshot where tenant_id = :tenant_id and lead_class = 'whale' and snapshot_date between :week_start and :week_end group by source order by qd_weighted_volume desc", + "queryParameters": { + "tenant_id": "tenant_binghatti_demo", + "week_start": "2026-04-06", + "week_end": "2026-04-12" + }, + "rowLimit": 20, + "freshnessSlaSeconds": 120, + "cachePolicy": { "mode": "ttl", "ttlSeconds": 120 }, + "privacyTier": "standard", + "lineageRefs": ["lin_01JQWE6F4G5LEADSNAP"] + }, + "visualizationParameters": { + "xAxis": "source", + "yAxis": "qd_weighted_volume", + "sort": "desc", + "showLabels": true, + "colorScale": ["#0EA5E9", "#22D3EE"], + "legend": false + }, + "dataBindings": { + "dimensions": ["source"], + "measures": ["qd_weighted_volume"], + "series": [], + "filters": [{ "field": "lead_class", "operator": "=", "value": "whale" }] + }, + "version": 1, + "provenance": { + "originType": "prompt_generated", + "templateId": "tpl_bar_source_quality_v3", + "promptExecutionId": "pex_01JQWE6F4G5PROMPTA", + "createdBy": "user_sales_director_001", + "createdAt": "2026-04-08T16:40:22Z" + }, + "renderingHints": { + "estimatedHeightPx": 340, + "skeletonVariant": "chart", + "virtualizationPriority": 8 + }, + "layout": { + "orderIndex": 100, + "sectionId": "sec_market_overview", + "widthMode": "half", + "minHeightPx": 320, + "stickyHeader": false + }, + "accessControls": { + "visibilityScope": "private", + "allowedRoles": ["senior_broker", "sales_director", "marketing_operator"], + "redactionPolicy": "aggregate_only" + }, + "styleSignature": { + "theme": "velocity_glass", + "paletteToken": "ocean_signal", + "motionProfile": "calm_reveal", + "density": "comfortable", + "radiusScale": "lg", + "typographyScale": "balanced" + }, + "validationState": { + "schema": "pass", + "policy": "pass", + "a11y": "pass", + "performance": "pass", + "status": "validated" + }, + "auditLog": ["aud_01JQWE6F4G5CREATE", "aud_01JQWE6F4G5VALIDATE"] + }, + { + "componentId": "cmp_01JQWE7H7G9GEOMAPINV", + "type": "geoMap", + "title": "Investor Interest Density by Dubai District", + "description": "Maps high-intent leads with at least one positive Sentinel spike in the last 30 days.", + "dataSourceDescriptor": { + "descriptorId": "dsd_01JQWE7H7G9MAP", + "sourceType": "derived_dataset", + "connectorId": "velocity-core-postgres", + "dataset": "lead_geo_interest_rollup", + "authContextRef": "authctx_sales_director_team_scope", + "queryTemplate": "select district, lat, lng, lead_count, avg_qd_score from lead_geo_interest_rollup where tenant_id = :tenant_id and activity_window = :window and min_interest = :min_interest", + "queryParameters": { + "tenant_id": "tenant_binghatti_demo", + "window": "30d", + "min_interest": "high" + }, + "rowLimit": 100, + "freshnessSlaSeconds": 300, + "cachePolicy": { "mode": "ttl", "ttlSeconds": 300 }, + "privacyTier": "restricted", + "lineageRefs": ["lin_01JQWE7H7G9ROLLUP", "lin_01JQWE7H7G9SENTINEL"] + }, + "visualizationParameters": { + "mapStyle": "dubai_district_heat", + "intensityField": "lead_count", + "tooltipFields": ["district", "lead_count", "avg_qd_score"], + "interactive": true + }, + "dataBindings": { + "dimensions": ["district"], + "measures": ["lead_count", "avg_qd_score"], + "series": ["district"], + "filters": [{ "field": "activity_window", "operator": "=", "value": "30d" }] + }, + "version": 1, + "provenance": { + "originType": "catalog", + "templateId": "tpl_geo_investor_heat_v2", + "promptExecutionId": "pex_01JQWE7H7G9PROMPTB", + "createdBy": "user_sales_director_001", + "createdAt": "2026-04-08T16:40:23Z" + }, + "renderingHints": { + "estimatedHeightPx": 420, + "skeletonVariant": "map", + "virtualizationPriority": 9 + }, + "layout": { + "orderIndex": 200, + "sectionId": "sec_market_overview", + "widthMode": "half", + "minHeightPx": 400, + "stickyHeader": false + }, + "accessControls": { + "visibilityScope": "private", + "allowedRoles": ["senior_broker", "sales_director"], + "redactionPolicy": "district_level_only" + }, + "styleSignature": { + "theme": "velocity_glass", + "paletteToken": "aqua_signal", + "motionProfile": "calm_reveal", + "density": "comfortable", + "radiusScale": "lg", + "typographyScale": "balanced" + }, + "validationState": { + "schema": "pass", + "policy": "pass", + "a11y": "pass", + "performance": "pass", + "status": "validated" + }, + "auditLog": ["aud_01JQWE7H7G9CREATE", "aud_01JQWE7H7G9VALIDATE"] + } + ] +} +``` + +### 6.6 Prompt-to-Component Transformation Example + +The following example illustrates how Oracle turns a natural-language request into multiple components. + +```json +{ + "prompt": "Show me this week's whale leads by source and map where our high-intent investors are concentrated in Dubai.", + "intentClass": "mixed", + "retrievalPlan": { + "entities": ["lead_daily_snapshot", "lead_geo_interest_rollup"], + "metrics": ["qd_weighted_volume", "lead_count", "avg_qd_score"], + "dimensions": ["source", "district"], + "filters": [ + { "field": "lead_class", "operator": "=", "value": "whale" }, + { "field": "activity_window", "operator": "=", "value": "7d" } + ], + "privacyTier": "restricted", + "resultShape": ["categorical_aggregate", "geospatial_aggregate"] + }, + "visualizationPlan": { + "componentSet": [ + { + "componentType": "barChart", + "templateId": "tpl_bar_source_quality_v3", + "placement": "append" + }, + { + "componentType": "geoMap", + "templateId": "tpl_geo_investor_heat_v2", + "placement": "append" + } + ], + "styleSignature": "velocity_glass:ocean_signal:comfortable" + } +} +``` + +### 6.7 Indexing Keys and Storage Formats + +| Store | Primary key or identity | Secondary indexes | Storage note | +| --- | --- | --- | --- | +| `canvas_pages` | `(tenant_id, page_id)` | `(tenant_id, owner_id, page_type)`, `(tenant_id, branch_id)` | Stable metadata only | +| `canvas_page_revisions` | `(tenant_id, page_id, revision_number)` | `(tenant_id, page_id, created_at desc)` | Immutable revision bodies compressed as JSONB | +| `canvas_components` | `(tenant_id, page_id, branch_id, component_id)` | `(tenant_id, page_id, branch_id, order_index)`, GIN on `data_source_descriptor`, GIN on `style_signature` | Latest projection for fast page reads | +| `component_templates` | `(tenant_id, template_id)` | `(tenant_id, category, status)`, `(tenant_id, style_signature_hash)` | JSONB template metadata plus object storage bundle ref | +| `prompt_executions` | `(tenant_id, execution_id)` | `(tenant_id, page_id, created_at desc)`, `(tenant_id, actor_id, created_at desc)` | Retains request, plan, outcome, warnings | +| `merge_requests` | `(tenant_id, merge_request_id)` | `(tenant_id, target_page_id, status)`, `(tenant_id, source_page_id, status)` | Stores diff metadata and reviewer state | +| `audit_events` | `(tenant_id, audit_event_id)` | `(tenant_id, entity_type, entity_id, created_at desc)`, `(tenant_id, correlation_id)` | Append-only evidence trail | +| `lineage_records` | `(tenant_id, lineage_record_id)` | `(tenant_id, produced_kind, produced_id)`, `(tenant_id, source_kind, source_id)` | Graph edges for provenance and replay | + +## 7. Premade Components Catalog and AI Synthesis + +### 7.1 Catalog Taxonomy + +| Category | Business meaning | Typical component forms | Accessibility and semantics | +| --- | --- | --- | --- | +| Executive overview | Quick status for leaders | KPI tile, compare card, exception notice | Must expose numeric text equivalent and threshold legend | +| Pipeline management | Deal movement and bottlenecks | Pipeline board, stage funnel, stalled-deals table | Must support keyboard navigation and color-independent stage labels | +| Broker performance | Rep activity and outcome quality | Ranked table, trend chart, quota card | Must show exact values in tooltips and tabular fallback | +| Lead quality | Source quality, whale mix, QD trend | Bar chart, stacked chart, score distribution | Must avoid color-only meaning for qualification bands | +| Geographic demand | Location-based concentration | Geo map, district matrix, map plus table | Must provide textual district summaries and aggregated counts | +| Investor timelines | Historical interactions and milestone progress | Timeline, activity stream, sentiment overlay | Must preserve chronological order in screen reader mode | +| Inventory and project analytics | Absorption, pricing, availability | Line chart, heatmap, cohort matrix | Must expose exact numeric labels on focus | +| Operational queues | Follow-ups, approvals, exceptions | Queue table, task board, merge review card | Must support compact dense mode without loss of tab order | + +The premade catalog is seeded from two sources. The first source is purpose-built Oracle templates for the canonical real-estate workflows above. The second source is style and interaction exemplars extracted from the existing Oracle UI and the component reference material already present in the repo. This lets Sprint 1 preserve the visual language the user has already established while greatly expanding functional depth. + +### 7.2 Style Learning and Aesthetic Adaptation + +Oracle does not start with uncontrolled model fine-tuning. In MVP, style learning is retrieval-based. The system ingests exemplar components and extracts a `styleSignature` that captures theme name, palette tokens, motion profile, density, radius scale, typography scale, spacing rhythm, contrast thresholds, and default interaction affordances. The synthesis engine then conditions component generation on that style signature plus a small set of canonical component archetypes. + +This is the correct first-principles choice for Sprint 1. It is explainable, reversible, and easy to validate. It lets Oracle learn the user's aesthetic without introducing a black-box dependency between visual identity and model weights. Phase 2 may add tenant-level adaptation loops that update style priors based on accepted components and rejected components, but the system of record remains the explicit `styleSignature`. + +### 7.3 Synthesis Workflow + +When Nemoclaw requests a component type that has no exact catalog match, the synthesis engine retrieves the closest semantic archetype and the closest style exemplars. It then generates a candidate template in structured form, validates the template schema, simulates render performance, runs accessibility checks, checks security constraints such as redaction and external asset policy, and renders a preview. If validation passes, Oracle persists the template as `tenant_draft`, renders the corresponding component to the canvas, and records the full provenance chain. + +Auto-promotion within the tenant occurs only after successful automated validation and repeated successful runtime use. The default promotion rule is three successful executions across at least two different pages within the same tenant with zero schema, policy, accessibility, or render failures. A sales director or data steward may accelerate promotion manually. No synthesized template may become global in this artifact. + +### 7.4 Versioning, Caching, and Invalidation + +Template versioning follows `major.minor.patch`. A major change alters accepted data bindings, required parameters, or semantics. A minor change alters default styling, interactions, or copy. A patch changes bug fixes or non-breaking defaults. Component instances record the template version used at creation time and do not automatically jump across major versions. + +Client-side template metadata is cached for five minutes by default. Server-side rendered template bundles are cached by `templateId:version:styleSignatureHash`. Cache invalidation occurs on publish, revoke, manual archive, or style signature change. Any revoked template immediately invalidates associated bundles and renders fallback notices for existing components until they are replaced or restored under review. + +### 7.5 Example Synthesis Narrative + +Assume the user asks, "Create a broker influence radar that compares follow-up speed, QD uplift, and conversion probability by team lead." Oracle will not find an exact premade template in Sprint 1. Nemoclaw classifies the request as analytical, identifies a multivariate comparison pattern, and requests a synthesized `customMLVisualization` backed by the nearest archetype for comparative team diagnostics. The style engine pulls the tenant's accepted Oracle chart tokens. The synthesis engine generates a structured component template and validates it. The component renders on the page as a tenant-private draft template. After subsequent successful use, Oracle auto-promotes it into the tenant catalog so future prompts can reuse it directly. + +## 8. Nemoclaw Integration and Data Access + +### 8.1 Nemoclaw's Role + +Nemoclaw is the planner, not the final authority. It understands the user's prompt, maps intent to business entities and metrics, proposes a retrieval plan, proposes a visualization plan, and emits reasoning metadata for audit. It does not obtain direct access to raw connector credentials. It does not commit page revisions. It does not decide policy. Those responsibilities belong to the policy engine, the Data Access Gateway, and the Canvas Service. + +Oracle should use the same runtime abstraction pattern already visible in the current backend's `backend/services/nemoclaw_client.py`. The active production runtime may be a hosted compatible model, a private compatible model, or a local model depending on tenant policy. The Oracle contract stays stable across all three. + +### 8.2 Nemoclaw Request Contract + +```json +{ + "executionId": "pex_01JQX00PLANNER", + "tenantId": "tenant_binghatti_demo", + "actor": { + "userId": "user_sales_director_001", + "role": "sales_director", + "timezone": "Asia/Dubai" + }, + "pageContext": { + "pageId": "page_01JQWE9MAINBROKER", + "branchId": "branch_main", + "headRevision": 18, + "visibleComponentIds": ["cmp_01JQWE6F4G5BARWHALESRC"] + }, + "prompt": "Show me this week's whale leads by source and map where our high-intent investors are concentrated in Dubai.", + "semanticModelVersion": "oracle_semantic_v2026_04_08_01", + "allowedDatasets": [ + "lead_daily_snapshot", + "lead_geo_interest_rollup", + "sentinel_signal_rollup" + ], + "policyConstraints": { + "crossTenantAllowed": false, + "maxRowLimit": 1000, + "piiMode": "redact_unless_explicit" + }, + "styleContext": { + "preferredTheme": "velocity_glass", + "exemplarTemplateIds": [ + "tpl_bar_source_quality_v3", + "tpl_geo_investor_heat_v2" + ] + } +} +``` + +### 8.3 Nemoclaw Response Contract + +```json +{ + "executionId": "pex_01JQX00PLANNER", + "intentClass": "mixed", + "confidence": 0.94, + "retrievalPlan": { + "datasets": [ + { + "dataset": "lead_daily_snapshot", + "grain": "lead_day", + "filters": [ + { "field": "lead_class", "operator": "=", "value": "whale" }, + { "field": "snapshot_date", "operator": "between", "value": ["2026-04-06", "2026-04-12"] } + ], + "dimensions": ["source"], + "measures": ["sum(qd_weighted_score) as qd_weighted_volume"] + }, + { + "dataset": "lead_geo_interest_rollup", + "grain": "district_30d", + "filters": [ + { "field": "min_interest", "operator": "=", "value": "high" } + ], + "dimensions": ["district", "lat", "lng"], + "measures": ["lead_count", "avg_qd_score"] + } + ], + "privacyTier": "restricted" + }, + "visualizationPlan": { + "components": [ + { + "componentType": "barChart", + "templateId": "tpl_bar_source_quality_v3", + "placementMode": "append" + }, + { + "componentType": "geoMap", + "templateId": "tpl_geo_investor_heat_v2", + "placementMode": "append" + } + ] + }, + "warnings": [], + "requiresHumanReview": false +} +``` + +### 8.4 Data Access Gateway Contract + +```json +{ + "executionId": "pex_01JQX00PLANNER", + "tenantId": "tenant_binghatti_demo", + "descriptor": { + "dataset": "lead_daily_snapshot", + "queryTemplate": "select source, sum(qd_weighted_score) as qd_weighted_volume from lead_daily_snapshot where tenant_id = :tenant_id and lead_class = 'whale' and snapshot_date between :week_start and :week_end group by source", + "queryParameters": { + "tenant_id": "tenant_binghatti_demo", + "week_start": "2026-04-06", + "week_end": "2026-04-12" + }, + "authContextRef": "authctx_sales_director_team_scope" + } +} +``` + +```json +{ + "executionId": "pex_01JQX00PLANNER", + "status": "completed", + "resultShape": "categorical_aggregate", + "schema": [ + { "field": "source", "type": "string" }, + { "field": "qd_weighted_volume", "type": "number" } + ], + "rows": [ + { "source": "website", "qd_weighted_volume": 182.4 }, + { "source": "walkin", "qd_weighted_volume": 149.2 }, + { "source": "whatsapp", "qd_weighted_volume": 93.7 } + ], + "lineageRecordIds": ["lin_01JQWE6F4G5LEADSNAP"], + "cacheKey": "cache:tenant_binghatti_demo:lead_daily_snapshot:2026w15" +} +``` + +### 8.5 Hallucination, Non-Determinism, and Data Leakage Mitigation + +Oracle must constrain Nemoclaw through structure, not trust. The prompt planner uses low-temperature structured output. The response is parsed against JSON Schema. The semantic compiler rejects unknown entities, unknown joins, non-parameterized predicates, unsafe field requests, and policy mismatches. The Data Access Gateway executes only validated plans. The Visualization Renderer consumes typed datasets and never infers field names from raw prose. Every execution stores the model runtime id, prompt hash, schema version, plan hash, and validation result so that the same execution can be replayed during debugging or audit. + +Leakage prevention starts before query execution. Nemoclaw receives only the semantic dictionary and visible page context needed for planning. It does not receive all tenant data. Sensitive prompt text is redacted in model logs according to privacy tier. Returned rows are subjected to policy transforms before rendering. When a prompt requests PII without explicit justification or scope, Oracle either redacts those fields or denies the request. + +### 8.6 AI Stewardship Workflow + +Oracle treats AI as the operational steward of records, not as the unchecked owner of schema. Nemoclaw may propose new derived fields, new rollup views, new indexes, new semantic aliases, and new component templates. Those proposals become typed change records reviewed by a data steward or platform admin. A proposal can be accepted, revised, or rejected. Accepted proposals become migrations, materializations, or catalog entries managed under version control and normal deployment controls. This preserves the "AI-owned database" operating model while keeping structural governance accountable and safe. + +## 9. Security, Privacy, and Compliance + +### 9.1 Authentication and Authorization + +Oracle should integrate with an external identity provider for production tenants and retain the current internal JWT scheme only for local or transitional environments. Access tokens should be short-lived. Refresh and session policies should be handled at the identity layer. Every request into Oracle resolves an effective principal that combines identity claims, tenant scope, role, page access, and policy profile. + +Authorization must occur at multiple layers. The API layer enforces page and action permissions. The Data Access Gateway enforces tenant and row scope. The component renderer enforces redaction and visibility scope. The collaboration layer enforces fork and merge rights. A user who can view a page is not automatically authorized to view all datasets referenced by all components on that page. Access must be recalculated at render time and export time. + +### 9.2 Zero-Trust Control Model + +| Control area | Required design | Evidence expectation | +| --- | --- | --- | +| Network trust | Mutual TLS for service-to-service traffic and private service discovery | Certificate rotation records and handshake telemetry | +| Secret handling | Secrets in managed secret store, not in repo or env files committed to git | Secret access audit and rotation policy | +| Data at rest | Envelope encryption with tenant-scoped data keys | Key hierarchy and key rotation logs | +| Data in transit | TLS 1.2+ everywhere and pinned internal trust anchors | Load balancer and service mesh policy evidence | +| Least privilege | Separate service identities for planner, query executor, renderer, and notifier | IAM policy review and quarterly access recertification | +| Runtime hardening | Signed images, patched base images, no privileged containers, dependency scanning | SBOM and vulnerability report | + +### 9.3 Sensitive Data Handling + +Prompts and results must be classified into privacy tiers. `standard` data includes aggregate counts and non-sensitive operational data. `restricted` data includes identifiable customer interaction details that require role-aware visibility. `sensitive` data includes regulated or highly personal data that demands explicit justification, stronger redaction, tighter retention, and narrower logging. + +Redaction policies must be enforced before model planning where possible and certainly before render. Differential privacy options are available only for approved benchmark and research views and are not enabled by default for ordinary broker workflows. Oracle should minimize data movement by preferring aggregates, materialized views, and tokenized identifiers over raw record dumps whenever the prompt does not require record-level access. + +### 9.4 Retention, Deletion, and Immutable Auditing + +Retention must be policy-driven by tenant and data class. Prompt executions and page revisions should default to one year of hot storage and longer archival retention when required. Audit logs should be immutable for the legal retention period. A delete request against user data must propagate through pages, exports, snapshots, and derived datasets according to lineage. When legal hold is active, logical deletion may hide data from operators but cannot physically purge protected records until the hold is lifted. + +Immutable audit logs must capture who asked what, what was planned, what datasets were touched, what component was rendered, who shared the page, who forked it, who approved a merge, and what changed. Audit payloads should store hashes for sensitive prompt bodies when tenant policy forbids storing plain text long term. + +### 9.5 Compliance Mapping + +| Regime | Oracle requirement | Design response | +| --- | --- | --- | +| GDPR | Lawful basis, purpose limitation, minimization, access and deletion rights | Policy-driven retention, lineage-based deletion, consent-aware data access, export and deletion workflows | +| CCPA | Disclosure, deletion, and no unauthorized sharing of personal data | Tenant-scoped access logs, export endpoints, retention enforcement, no cross-tenant data sharing in MVP | +| HIPAA-ready mode | Access controls, encryption, audit logging, minimum necessary access | Sensitive tier handling, mTLS, immutable audit, redaction, role and purpose checks | + +Cross-tenant analytics is not enabled in MVP. Any future cross-tenant analytics must require anonymization, explicit legal basis, tenant opt-in, and a dedicated policy gate outside the normal Oracle flow. + +## 10. Testing Strategy and Quality Assurance + +### 10.1 Test Scope + +Oracle test coverage must span unit, integration, end-to-end, performance, security, accessibility, and chaos testing. The critical path includes prompt interpretation, policy validation, data retrieval, template resolution, component generation, page revision commits, share and fork creation, merge request creation, merge conflict handling, lineage capture, and template auto-promotion. + +| Test layer | What it proves | Target | +| --- | --- | --- | +| Unit tests | Deterministic behavior of plan parsing, policy evaluation, diff logic, ordering, and redaction | >= 90% coverage on prompt planning and access control modules | +| Integration tests | Contracts across orchestrator, query gateway, canvas service, and catalog | >= 85% coverage on collaboration and persistence modules | +| End-to-end tests | Full prompt-to-canvas and share-to-merge user journeys | 100% golden path coverage for documented session flows | +| Performance tests | SLO conformance under concurrency, large canvases, and cache miss conditions | p95 targets validated before release | +| Security tests | Auth bypass, tenant escape, injection, redaction leakage, and secret misuse | Zero high-severity findings before production rollout | +| Accessibility tests | Keyboard, focus, color contrast, screen reader semantics, and reduced-motion behavior | WCAG 2.1 AA conformance for core Oracle flows | +| Chaos tests | Runtime failure resilience for model outages, query timeouts, websocket disconnects, and merge retries | No silent data loss or duplicate page commits | + +### 10.2 Test Data and Mocking Strategy + +CI must use synthetic tenant-isolated datasets seeded with realistic brokerage data including leads, brokers, projects, units, campaign activity, Sentinel scores, and vault events. Nemoclaw must be mocked in CI through deterministic structured outputs so prompt planning tests remain stable. Replay fixtures must exist for ambiguous prompts, policy denials, conflict-heavy merges, and synthesized component promotion. + +Prompt-to-query planning requires deterministic replay fixtures keyed by prompt hash, semantic model version, and policy profile. Merge conflict replays must include concurrent reorder plus edit scenarios, concurrent filter mutation scenarios, and source branch divergence after target changes. These are high-risk behaviors and cannot rely on manual testing alone. + +### 10.3 Acceptance Criteria + +Oracle is acceptable for MVP when a sales director can create a main page, run a prompt that produces at least two components, refresh the page and see the same revision, share the page to a senior broker, watch the senior broker receive an isolated fork, review a merge request, resolve or reject conflicts, and see the merged revision with intact audit history. It is also acceptable only if a junior broker cannot retrieve unauthorized restricted data, if redaction behaves correctly in exports and page render, and if page restore can recreate a prior revision without direct database mutation. + +### 10.4 CI/CD and Release Gating + +| Gate | Requirement | +| --- | --- | +| Contract gate | JSON Schemas and OpenAPI examples validate and remain backward compatible within `v1` rules | +| Quality gate | Coverage targets met and no flaky replay fixtures in critical paths | +| Security gate | No high-severity vulnerabilities or tenant-isolation regressions | +| Performance gate | Prompt, query, render, and persist p95 thresholds within tolerance | +| Accessibility gate | Core flows pass automated checks and manual keyboard review | +| Rollout gate | Canary tenant metrics stable with no SLO burn or merge integrity regression | + +## 11. Deployment, Operations, and Observability + +### 11.1 Deployment Model + +Oracle should deploy as containerized services orchestrated under the same operational umbrella as the existing backend. MVP may run as a modular FastAPI deployment with separate modules enabled behind flags. Later phases may split high-churn or high-scale paths such as synthesis, websocket fanout, and query execution into independent services. Deployment artifacts must be signed, immutable, and tied to revisioned configuration. + +Multi-region deployment uses a home-region write model. The home region owns prompt execution, page revisions, and merge commits for a tenant. Secondary regions serve read replicas, static assets, and failover web entry points. Database replication uses logical replication. Object artifacts use cross-region replication. Event outbox records are replay-safe. + +### 11.2 Monitoring and Dashboards + +Dashboards must expose prompt volume, intent mix, plan validation failures, query latency by dataset, synthesis latency, template cache hit ratio, render crash rate, page size distribution, fork creation rate, merge request throughput, merge conflict rate, merge approval lead time, and policy denial rate. Frontend dashboards must expose canvas hydration time, scroll FPS, component mount time, and websocket reconnect rates. + +### 11.3 Logging and Tracing Standards + +Structured logs must include `timestamp`, `service`, `environment`, `tenantId`, `actorId`, `correlationId`, `executionId`, `pageId`, `branchId`, `severity`, `eventType`, and `message`. Distributed traces must include spans for prompt intake, plan generation, policy validation, query execution, template resolution, synthesis, validation, revision commit, websocket publish, and client apply. Exported page snapshots and merge approvals must include the originating `correlationId` for traceability. + +### 11.4 Rollback and Incident Response + +Rollback occurs at three levels. The first level is page revision rollback, which restores user-visible state. The second level is feature rollback through configuration flags, which can disable synthesis, sharing, or merge execution while preserving read access. The third level is service rollback through deployment tooling. + +| Failure mode | Detection | Immediate response | Durable fix | +| --- | --- | --- | --- | +| Nemoclaw malformed output | Structured output parser failure spike | Route affected executions to fallback clarification or safe failure notice | Tighten schema prompts, add parser test, adjust runtime configuration | +| Data source outage | Query error rate and timeout spike | Fail closed, show diagnostic notice components, switch to stale-but-valid cache only if policy permits | Restore connector, backfill cache, replay failed executions if requested | +| Merge conflict anomaly | Unexpected merge rejection or diff mismatch | Freeze merge completion for affected tenant and preserve forks | Recompute diff, replay fixture, patch merge engine | +| Render crash after template publish | Frontend crash telemetry spike tied to template id | Revoke template version and invalidate cache | Fix template bundle and republish under new version | + +## 12. Data Governance, Provenance, and Lineage + +Oracle must provide a complete lineage path from source data to rendered artifact. A compliance reviewer must be able to answer five questions for any visible component: what prompt created it, what data sources were used, what transformations were applied, what template rendered it, and who later changed or merged it. The lineage model therefore records graph edges, not just flat audit rows. + +The lineage chain begins with source tables or materialized views. It continues through retrieval plans, query executions, result sets, template selection or synthesis, component creation, page revision commits, sharing events, forks, merge requests, and merge commits. Every edge stores timestamps, actor identity, policy snapshot, and transformation type. This allows export evidence, deletion propagation, and historical replay without forensic reconstruction. + +Governance roles are explicit. The sales organization can ask questions and build pages. Data stewards approve schema proposals and semantic model changes. Compliance reviewers audit access and retention. Platform admins operate runtime and connector policy. AI acts as a steward of operational data quality and schema proposals but not as the final authority for structural changes. + +## 13. API Contracts and Integration Points + +### 13.1 Versioning and Error Envelope + +Oracle uses path versioning at `/api/oracle/v1` and may additionally return `X-Oracle-Contract-Version` for debugging and compatibility checks. Pagination uses opaque cursor tokens. Mutation endpoints accept `Idempotency-Key` when the client may retry. + +Errors use a consistent envelope. + +```json +{ + "error": { + "code": "policy_denied", + "message": "The requested prompt references a restricted field outside your scope.", + "retryable": false, + "correlationId": "corr_01JQX999ERR", + "details": { + "field": "passport_number", + "policyProfileId": "policy_team_standard_v4" + } + } +} +``` + +### 13.2 Endpoint Surface + +| Service | Endpoint | Behavior | +| --- | --- | --- | +| Canvas Service | `GET /api/oracle/v1/canvas-pages/{pageId}` | Returns page metadata, current head revision, and current component projection for authorized branch | +| Canvas Service | `POST /api/oracle/v1/canvas-pages/{pageId}/prompts` | Starts a prompt execution and, on success, commits one new page revision | +| Canvas Service | `POST /api/oracle/v1/canvas-pages/{pageId}/rollback` | Creates a new revision restoring a prior revision snapshot | +| Collaboration Service | `POST /api/oracle/v1/canvas-pages/{pageId}/forks` | Creates an isolated fork for a recipient at a fixed source revision | +| Collaboration Service | `POST /api/oracle/v1/merge-requests` | Opens a merge request from fork branch to target branch | +| Collaboration Service | `POST /api/oracle/v1/merge-requests/{mrId}/review` | Approves, rejects, or requests changes with optional conflict resolutions | +| Component Catalog Service | `GET /api/oracle/v1/component-templates` | Lists tenant-visible templates with filters and cursor pagination | +| Component Catalog Service | `POST /api/oracle/v1/component-templates/synthesize` | Synthesizes a tenant-private draft template under policy and validation | +| Identity and Access | `GET /api/oracle/v1/me` | Returns role, tenant, page defaults, and policy profile | +| Realtime | `WS /ws/oracle/canvas/{pageId}` | Streams presence, execution progress, revision commits, and merge updates | + +### 13.3 Prompt Execution Request and Response + +```json +{ + "clientRequestId": "cli_01JQXA111PROMPT", + "branchId": "branch_main", + "prompt": "Show me this week's whale leads by source and map where our high-intent investors are concentrated in Dubai.", + "conversationContext": [ + { + "role": "user", + "content": "Focus on investor-quality leads for the current week." + } + ], + "placementMode": "append_after_last_visible_component" +} +``` + +```json +{ + "executionId": "pex_01JQXA111PROMPT", + "status": "completed", + "pageId": "page_01JQWE9MAINBROKER", + "branchId": "branch_main", + "headRevision": 18, + "componentsCreated": [ + "cmp_01JQWE6F4G5BARWHALESRC", + "cmp_01JQWE7H7G9GEOMAPINV" + ], + "summary": "Created a source comparison chart and an investor density map for high-intent whale leads.", + "warnings": [] +} +``` + +### 13.4 Fork Request and Response + +```json +{ + "recipientUserId": "user_senior_broker_014", + "sourceRevision": 18, + "visibility": "private", + "message": "Review this investor signal page and propose follow-up views." +} +``` + +```json +{ + "forkId": "fork_01JQWE9TEAMSHARE", + "forkPageId": "page_01JQWE9FORKBROKER2", + "forkBranchId": "branch_fork_broker2", + "status": "active", + "sourceRevision": 18 +} +``` + +### 13.5 Merge Request and Review Request + +```json +{ + "sourcePageId": "page_01JQWE9FORKBROKER2", + "sourceBranchId": "branch_fork_broker2", + "targetPageId": "page_01JQWE9MAINBROKER", + "targetBranchId": "branch_main", + "title": "Add broker follow-up queue and reorder investor map", + "description": "Adds one operational queue component and places it under the investor map." +} +``` + +```json +{ + "mergeRequestId": "mr_01JQXB222MERGE", + "status": "open", + "conflicts": [], + "diffSummary": { + "componentsAdded": 1, + "componentsEdited": 0, + "componentsReordered": 1, + "componentsDeleted": 0 + } +} +``` + +```json +{ + "decision": "approve", + "comment": "Queue component is valid. Order change accepted.", + "resolutions": [] +} +``` + +```json +{ + "mergeRequestId": "mr_01JQXB222MERGE", + "status": "merged", + "mergedRevision": 19, + "targetPageId": "page_01JQWE9MAINBROKER", + "targetBranchId": "branch_main" +} +``` + +### 13.6 WebSocket Contract + +The page WebSocket is the realtime backbone for execution progress, revision commits, merge review status, and presence. Messages use typed envelopes such as `presence.updated`, `prompt.execution.progress`, `canvas.revision.committed`, `merge_request.updated`, and `component.validation.revoked`. The client must treat the WebSocket as advisory for live updates and use HTTP as the source of truth after reconnect. + +### 13.7 Sample Session Summary + +The typical session begins with a `POST /api/oracle/v1/canvas-pages/{pageId}/prompts` request that yields two new components on revision 18. The user then shares the page with a senior broker using `POST /forks`. The senior broker edits the fork and opens `POST /merge-requests`. The sales director reviews the diff using `POST /merge-requests/{mrId}/review` and approves the merge. The main page advances to revision 19. The full transcript appears in Appendix 15.4. + +## 14. Roadmap, Milestones, and Rollout Plan + +### 14.1 Delivery Phases + +| Phase | Scope | Exit condition | +| --- | --- | --- | +| MVP | Prompt-to-canvas append, premade catalog, tenant-scoped data access, private sharing, forks, reviewed merges, audit logging, tenant auto-promotion after validation | One pilot tenant can complete the golden path reliably and within SLO | +| Phase 2 | Richer synthesis, exemplar learning loops, more operational actions, stronger governance workflows, broader dataset coverage | Multiple tenants use Oracle as daily operating surface | +| Phase 3 | Advanced compliance packs, broader connector ecosystem, more ML visual forms, optional controlled cross-tenant intelligence | Enterprise tenants can adopt Oracle under stricter regulatory and scale requirements | + +### 14.2 MVP Milestones + +The first milestone is the contract milestone, where `OracleQueryResult` is formally retired and the `CanvasPage` plus `CanvasComponent` contracts become the source of truth. The second milestone is the backend orchestration milestone, where prompt planning, policy validation, query execution, template resolution, and page revision commits work end to end. The third milestone is the collaboration milestone, where page sharing, fork creation, merge diff generation, review, and merge commit work with auditability. The fourth milestone is the catalog milestone, where premade templates and tenant auto-promotion are operational. The fifth milestone is the rollout milestone, where one pilot tenant uses the system behind flags under canary release. + +### 14.3 Resource Assumptions and Constraints + +This plan assumes the current React and FastAPI codebase remains the primary implementation base. It assumes PostgreSQL remains the canonical data store. It assumes Nemoclaw remains a pluggable runtime abstraction. It assumes Oracle remains CRM-first and avoids premature connector sprawl in MVP. It assumes the visual style already established in the repo is preserved rather than replaced. + +### 14.4 Risks and Mitigations + +| Risk | Why it matters | Mitigation | +| --- | --- | --- | +| Prompt planner overreach | Can lead to invalid joins, privacy errors, or unstable outputs | Strict semantic compiler, policy gate, structured outputs, replay fixtures | +| Merge complexity | Component-aware diffs can become subtle under reorder and edit concurrency | Deterministic three-way merge, explicit conflict classes, merge replay tests | +| Template synthesis instability | Could create visually inconsistent or slow components | Exemplar-based style signatures, strict validation pipeline, tenant-only auto-promotion | +| Canvas performance decay | Large pages can become unusable | Virtualization, retained measurement caches, template-level performance budgets | +| Governance drift | AI-owned operations can become unsafe without human structure review | AI stewardship proposals with human approval for structural changes | + +## 15. Appendices + +### 15.1 Glossary + +| Term | Meaning | +| --- | --- | +| Oracle | The AI-driven CRM canvas module inside Velocity Suite | +| Canvas Page | A revisioned ordered JSON page containing renderable data components | +| Component Template | A reusable visualization recipe that can render one or more compatible data shapes | +| Prompt Execution | The durable record of one prompt request from intake through completion or failure | +| Hybrid Sovereign | Deployment model where data execution stays within tenant-controlled boundaries when policy requires it while model runtime remains pluggable | +| Fork | An isolated page branch created from a specific source revision | +| Merge Request | A reviewed proposal to apply fork changes into a target branch | +| Style Signature | Explicit visual token profile extracted from accepted Oracle exemplars | +| AI Stewardship | Operating model where AI manages records and proposes structural change while humans approve migrations | +| QD | Quantum Dynamics score produced by Sentinel and consumable by Oracle | + +### 15.2 Data Dictionary + +| Schema | Field | Meaning | +| --- | --- | --- | +| CanvasPage | `pageId` | Stable page identity | +| CanvasPage | `ownerId` | User who owns the canonical page | +| CanvasPage | `branchId` | Current branch identity | +| CanvasPage | `mainBranchPointer` | Reference to the main page and revision for fork tracking | +| CanvasPage | `baseRevision` | Revision from which the current branch was created | +| CanvasPage | `headRevision` | Latest committed revision on the branch | +| CanvasPage | `forks` | Active and historical fork records | +| CanvasPage | `components` | Ordered renderable component array | +| CanvasComponent | `componentId` | Stable component identity across revisions | +| CanvasComponent | `type` | Renderable component family | +| CanvasComponent | `dataSourceDescriptor` | Authorized query and source metadata | +| CanvasComponent | `visualizationParameters` | Display, interaction, and chart settings | +| CanvasComponent | `dataBindings` | Dimensions, measures, and filter mapping | +| CanvasComponent | `provenance` | Origin, prompt execution, and source lineage | +| CanvasComponent | `styleSignature` | Visual language tokens | +| CanvasComponent | `validationState` | Schema, policy, accessibility, and performance checks | +| UserProfile | `defaultPageId` | User's main Oracle page | +| PromptExecution | `retrievalPlan` | Typed query plan before execution | +| PromptExecution | `visualizationPlan` | Planned component set and placement | +| ComponentTemplate | `acceptedShapes` | Data shapes supported by the template | +| ForkRecord | `sourceRevision` | Immutable source snapshot used to create the fork | +| MergeRequest | `targetBaseRevision` | Target revision used for three-way diff | +| LineageRecord | `transformationType` | Named transformation connecting source to produced entity | +| AuditEvent | `correlationId` | Shared request trace identity across services | + +### 15.3 Sample Component JSON Object + +```json +{ + "componentId": "cmp_01JQWE6F4G5BARWHALESRC", + "type": "barChart", + "title": "Whale Leads by Source This Week", + "dataSourceDescriptor": { + "descriptorId": "dsd_01JQWE6F4G5SRC", + "sourceType": "postgres", + "connectorId": "velocity-core-postgres", + "dataset": "lead_daily_snapshot", + "authContextRef": "authctx_sales_director_team_scope", + "queryTemplate": "select source, sum(qd_weighted_score) as qd_weighted_volume from lead_daily_snapshot where tenant_id = :tenant_id and lead_class = 'whale' group by source", + "queryParameters": { + "tenant_id": "tenant_binghatti_demo" + }, + "rowLimit": 20, + "privacyTier": "standard" + }, + "visualizationParameters": { + "xAxis": "source", + "yAxis": "qd_weighted_volume" + }, + "dataBindings": { + "dimensions": ["source"], + "measures": ["qd_weighted_volume"], + "series": [], + "filters": [] + }, + "version": 1, + "provenance": { + "originType": "prompt_generated", + "templateId": "tpl_bar_source_quality_v3", + "promptExecutionId": "pex_01JQWE6F4G5PROMPTA", + "createdBy": "user_sales_director_001", + "createdAt": "2026-04-08T16:40:22Z" + }, + "renderingHints": { + "estimatedHeightPx": 340, + "skeletonVariant": "chart", + "virtualizationPriority": 8 + }, + "layout": { + "orderIndex": 100, + "sectionId": "sec_market_overview", + "widthMode": "half", + "minHeightPx": 320, + "stickyHeader": false + }, + "accessControls": { + "visibilityScope": "private", + "allowedRoles": ["senior_broker", "sales_director"], + "redactionPolicy": "aggregate_only" + }, + "styleSignature": { + "theme": "velocity_glass", + "paletteToken": "ocean_signal", + "motionProfile": "calm_reveal", + "density": "comfortable", + "radiusScale": "lg", + "typographyScale": "balanced" + }, + "validationState": { + "schema": "pass", + "policy": "pass", + "a11y": "pass", + "performance": "pass", + "status": "validated" + }, + "auditLog": ["aud_01JQWE6F4G5CREATE"] +} +``` + +### 15.4 Sample Canvas Page JSON Object + +```json +{ + "pageId": "page_01JQWE9MAINBROKER", + "tenantId": "tenant_binghatti_demo", + "ownerId": "user_sales_director_001", + "branchId": "branch_main", + "branchName": "main", + "pageType": "main", + "title": "Oracle Home - Pipeline and Investor Signals", + "createdAt": "2026-04-08T16:10:00Z", + "updatedAt": "2026-04-08T16:40:24Z", + "isShared": true, + "forks": [], + "mainBranchPointer": { + "pageId": "page_01JQWE9MAINBROKER", + "branchId": "branch_main", + "revision": 18 + }, + "baseRevision": 0, + "headRevision": 18, + "sharingPolicy": { + "shareMode": "direct_fork_only", + "allowReshare": false, + "defaultForkVisibility": "private" + }, + "presence": { + "activeViewers": 2, + "activeEditors": 1, + "lastPresenceAt": "2026-04-08T16:40:24Z" + }, + "lineage": [], + "audit": { + "lastAuditEventId": "aud_01JQWE9REV18", + "eventCount": 44 + }, + "components": [ + { + "componentId": "cmp_01JQWE6F4G5BARWHALESRC", + "type": "barChart", + "title": "Whale Leads by Source This Week", + "dataSourceDescriptor": { + "descriptorId": "dsd_01JQWE6F4G5SRC", + "sourceType": "postgres", + "connectorId": "velocity-core-postgres", + "dataset": "lead_daily_snapshot", + "authContextRef": "authctx_sales_director_team_scope", + "queryTemplate": "select source, sum(qd_weighted_score) as qd_weighted_volume from lead_daily_snapshot where tenant_id = :tenant_id and lead_class = 'whale' group by source", + "queryParameters": { + "tenant_id": "tenant_binghatti_demo" + }, + "rowLimit": 20, + "privacyTier": "standard" + }, + "visualizationParameters": { + "xAxis": "source", + "yAxis": "qd_weighted_volume" + }, + "dataBindings": { + "dimensions": ["source"], + "measures": ["qd_weighted_volume"], + "series": [], + "filters": [] + }, + "version": 1, + "provenance": { + "originType": "prompt_generated", + "templateId": "tpl_bar_source_quality_v3", + "promptExecutionId": "pex_01JQWE6F4G5PROMPTA", + "createdBy": "user_sales_director_001", + "createdAt": "2026-04-08T16:40:22Z" + }, + "renderingHints": { + "estimatedHeightPx": 340, + "skeletonVariant": "chart", + "virtualizationPriority": 8 + }, + "layout": { + "orderIndex": 100, + "sectionId": "sec_market_overview", + "widthMode": "half", + "minHeightPx": 320, + "stickyHeader": false + }, + "accessControls": { + "visibilityScope": "private", + "allowedRoles": ["senior_broker", "sales_director"], + "redactionPolicy": "aggregate_only" + }, + "styleSignature": { + "theme": "velocity_glass", + "paletteToken": "ocean_signal", + "motionProfile": "calm_reveal", + "density": "comfortable", + "radiusScale": "lg", + "typographyScale": "balanced" + }, + "validationState": { + "schema": "pass", + "policy": "pass", + "a11y": "pass", + "performance": "pass", + "status": "validated" + }, + "auditLog": ["aud_01JQWE6F4G5CREATE"] + } + ] +} +``` + +### 15.5 Sample Session Transcript + +```text +2026-04-08T16:40:21Z user_sales_director_001 opens page_01JQWE9MAINBROKER branch_main revision 17 +2026-04-08T16:40:21Z user submits prompt: "Show me this week's whale leads by source and map where our high-intent investors are concentrated in Dubai." +2026-04-08T16:40:21Z Prompt Orchestrator creates execution pex_01JQXA111PROMPT with correlation corr_01JQXA111 +2026-04-08T16:40:21Z Nemoclaw receives semantic model version oracle_semantic_v2026_04_08_01 and allowed datasets lead_daily_snapshot, lead_geo_interest_rollup, sentinel_signal_rollup +2026-04-08T16:40:22Z Nemoclaw returns structured retrieval plan and visualization plan with barChart and geoMap +2026-04-08T16:40:22Z Policy Engine validates no cross-tenant joins, restricted fields redacted, max row limits satisfied +2026-04-08T16:40:22Z Data Access Gateway executes aggregate source query and district interest rollup query +2026-04-08T16:40:23Z Visualization Renderer binds result set A to tpl_bar_source_quality_v3 and result set B to tpl_geo_investor_heat_v2 +2026-04-08T16:40:23Z Validation pipeline passes schema, policy, accessibility, and performance checks for both components +2026-04-08T16:40:24Z Canvas Service commits revision 18 with components cmp_01JQWE6F4G5BARWHALESRC and cmp_01JQWE7H7G9GEOMAPINV +2026-04-08T16:40:24Z WebSocket emits canvas.revision.committed to all page viewers +2026-04-08T16:41:10Z user_sales_director_001 shares page revision 18 with user_senior_broker_014 +2026-04-08T16:41:10Z Collaboration Service creates fork page_01JQWE9FORKBROKER2 branch_fork_broker2 from branch_main@18 +2026-04-08T16:49:55Z user_senior_broker_014 adds a follow-up queue component and reorders the investor map below it on the fork +2026-04-08T16:50:30Z user_senior_broker_014 opens merge request mr_01JQXB222MERGE targeting page_01JQWE9MAINBROKER branch_main +2026-04-08T16:50:30Z Merge engine computes three-way diff with zero conflicts and identifies one component add plus one reorder +2026-04-08T16:52:02Z user_sales_director_001 approves merge request with comment "Queue component is valid. Order change accepted." +2026-04-08T16:52:03Z Canvas Service writes merge revision 19 to branch_main and preserves fork provenance +2026-04-08T16:52:03Z Audit trail records prompt execution, share, fork, merge request, approval, and merged revision under correlation corr_01JQXA111 +``` + +## 16. Detailed Execution Blueprint + +### 16.1 Why This Annex Exists + +The first fifteen sections define the product and system contract. This annex translates that contract into an implementation blueprint that can be assigned directly to frontend, backend, data, platform, and QA workstreams. The aim here is not to restate the architecture at a higher level. The aim is to eliminate ambiguity in how the existing Velocity codebase should evolve from a polished placeholder Oracle UI into the production Oracle system described above. + +The guiding rule for implementation is that the current Oracle page remains the visual seed, not the data contract. The production system should preserve the premium visual shell that already exists in the repository, but every state transition, persistence rule, collaboration action, and data fetch path must move from mocked single-response behavior into revisioned canvas behavior. + +### 16.2 Frontend Build Blueprint + +The Oracle frontend should be implemented as a domain-owned vertical slice inside the existing React and Vite application. The current `app/src/app/oracle/page.tsx` should be refactored into an orchestration page rather than a monolithic view switcher. The visual primitives that already work, such as glass panels, prompt rail patterns, header rhythm, and premium motion, should be preserved. The internal architecture should be reorganized around page state, branch state, component registry, execution timeline, and collaboration overlays. + +The page should be split into five conceptual regions. The first region is the branch and execution bar at the top, which shows the page title, branch identity, revision number, unsynced local changes if any, current execution status, and share or merge affordances. The second region is the canvas viewport, which is a virtualized vertically scrollable component tree backed by the current page revision. The third region is the prompt and conversation rail, which preserves the current Oracle interaction language while now reflecting durable `PromptExecution` history. The fourth region is the secondary collaboration surface for presence, merge notices, and review comments. The fifth region is a transient overlay system for template preview, component inspector, rollback confirmation, and conflict resolution. + +The frontend should maintain three distinct state planes. The first plane is remote canonical state, which represents the page revision, component projection, branch metadata, merge request state, and template catalog data fetched from the backend. The second plane is execution state, which includes in-flight prompt executions, pending websocket events, optimistic placeholders, and temporary layout measurements. The third plane is UI-local state, which includes input text, selected component, collapsed groups, visible review drawer, and viewport state. This separation matters because optimistic feedback is useful for responsiveness, but only committed revisions may become durable user-visible history. + +The component registry should become the central rendering mechanism. Each `CanvasComponent.type` resolves to a renderer implementation that accepts a typed component object and a standardized render context. The render context includes viewport information, tenant style tokens, redaction flags, formatter utilities, and event hooks for drill-down, edit, clone, fork, and export. The registry must support lazy loading for expensive renderers such as maps, large tables, or custom ML visualizations. + +The virtualized canvas must preserve scroll stability during inserts and realtime updates. When a new component is appended, the viewport should remain stable if the user is reading older content and should auto-scroll only when the user is already near the bottom or explicitly opts into follow mode. During merge replay or rollback, the renderer should diff component identity and order indices so that only affected regions remount. Stable keys must use `componentId` plus `version`, not array index. + +The prompt rail should preserve conversational continuity, but it should stop being the place where results render. Instead, each prompt turn links to the revision it produced and lists the components created or updated by that execution. A user should be able to click from a prompt turn to the first component created by that turn, review the assumptions Nemoclaw made, inspect the datasets touched, and open the audit trail for the execution. + +The collaboration surface should expose branch identity clearly because this product borrows Jira-like merge semantics. The UI should make it impossible for a user to forget whether they are editing `main` or a fork. The share action must explain that recipients receive a fork, not live edit access. The merge review surface must explain adds, deletes, reorders, parameter edits, and access-control changes using domain terms rather than raw JSON only. + +The recommended frontend module split is shown below. + +| Frontend module | Responsibility | Notes | +| --- | --- | --- | +| `oracle/page.tsx` | Page orchestration and route entry | Preserves shell, delegates all data and render logic | +| `oracle/components/CanvasViewport.tsx` | Virtualized vertical canvas renderer | Owns measurement cache and viewport anchoring | +| `oracle/components/PromptRail.tsx` | Prompt input, history, execution state | Replaces current single-result assumptions | +| `oracle/components/BranchBar.tsx` | Branch identity, share, merge, revision affordances | Must remain visible at all times | +| `oracle/components/ComponentRegistry.tsx` | Resolver from `type` to renderer implementation | Supports lazy loading and fallback notice rendering | +| `oracle/components/review/MergeReviewDrawer.tsx` | Merge diff review and conflict resolution UI | Domain-specific conflict explanations | +| `oracle/hooks/useOraclePage.ts` | Fetch and subscribe to page state | Owns page hydration lifecycle | +| `oracle/hooks/useOracleExecution.ts` | Prompt submit and execution progress handling | Owns optimistic placeholders and status tracking | +| `oracle/hooks/useOracleSocket.ts` | WebSocket lifecycle and event reconciliation | Must recover from reconnect cleanly | +| `oracle/lib/oracleApiClient.ts` | Typed network client | Replaces the mock query client | + +React 19 features should be used deliberately. `startTransition` is appropriate when applying large revision updates so the prompt rail and branch bar remain responsive. `useDeferredValue` is appropriate for local filter controls on large tables or maps. Event handlers that depend on current state but should not cause unnecessary re-subscription, such as websocket message handlers, should use `useEffectEvent` when the codebase conventions allow it. The goal is not novelty. The goal is to keep the canvas interactive under real payload size. + +### 16.3 Backend Build Blueprint + +The backend should adopt Oracle as a first-class domain inside the current FastAPI runtime. The existing `backend/main.py` should mount a versioned Oracle router explicitly. The empty placeholder Oracle and CRM routes should not be incrementally patched into production shape. They should be superseded by a `v1` Oracle API surface that follows the contracts in Sections 6 and 13. + +The Oracle backend should be organized into bounded modules instead of one large controller. The Prompt Orchestrator owns prompt intake, semantic context assembly, Nemoclaw invocation, and orchestration state. The Canvas Service owns page creation, revision commits, component ordering, rollback, and retrieval. The Collaboration Service owns share, fork, merge request, review, and merge commit behavior. The Catalog Service owns premade template retrieval, synthesized template persistence, validation records, and promotion decisions. The Data Access Gateway owns query compilation and execution. The Policy Service owns authorization, privacy tier enforcement, and redaction policy. The Audit and Lineage Service owns immutable event recording and lineage edge persistence. + +The recommended backend module split is shown below. + +| Backend module | Responsibility | Expected runtime behavior | +| --- | --- | --- | +| `backend/oracle/router_v1.py` | Public HTTP and WebSocket surface | Mounted in `main.py` under `/api/oracle/v1` and `/ws/oracle/...` | +| `backend/oracle/prompt_orchestrator.py` | Prompt intake through validated execution plan | Stateless orchestrator over durable execution records | +| `backend/oracle/canvas_service.py` | Page creation, revision writes, rollback, retrieval | Must guarantee exactly one visible revision per commit | +| `backend/oracle/collaboration_service.py` | Fork, merge request, review, merge replay | Owns three-way merge rules | +| `backend/oracle/catalog_service.py` | Template query, synthesis persistence, promotion | Must enforce tenant-only auto-promotion | +| `backend/oracle/data_access_gateway.py` | Plan compilation and data execution | Must never execute unvalidated plan fragments | +| `backend/oracle/policy_service.py` | Tenant scope, row scope, privacy tier, redaction | Invoked before query and before render | +| `backend/oracle/renderer_service.py` | Component instance assembly and renderer metadata | Returns structured component objects only | +| `backend/oracle/audit_service.py` | Immutable event storage and query helpers | Shared by all mutation paths | +| `backend/oracle/lineage_service.py` | Lineage edge writes and replay helpers | Shared by execution and merge flows | +| `backend/oracle/semantic_model.py` | Business vocabulary and dataset mapping | Versioned independently from database schema | + +The backend write path should always use transaction boundaries that correspond to user-visible guarantees. A prompt execution may involve several internal steps, but the user-visible success event is the revision commit. Therefore a component set must be validated before the final commit transaction opens, and the commit transaction must atomically write the page revision, component projection updates, audit events, and lineage edges. Events for websocket fanout should be emitted through a transactional outbox rather than directly from inside the business transaction so that failures are replayable. + +### 16.4 Physical PostgreSQL Schema Mapping + +The logical contracts from Section 6 should map to a concrete relational schema. The following physical schema is the recommended baseline for Sprint 1. Types are shown in PostgreSQL form because the existing backend already uses raw PostgreSQL and asyncpg. + +```sql +create table if not exists oracle_canvas_pages ( + tenant_id uuid not null, + page_id uuid primary key, + owner_id uuid not null references users_and_roles(id), + branch_id uuid not null, + branch_name text not null, + page_type text not null check (page_type in ('main', 'fork')), + title text not null, + source_page_id uuid, + source_branch_id uuid, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + is_shared boolean not null default false, + base_revision integer not null default 0, + head_revision integer not null default 0, + sharing_policy jsonb not null default '{}'::jsonb, + presence_snapshot jsonb not null default '{}'::jsonb, + audit_summary jsonb not null default '{}'::jsonb, + unique (tenant_id, page_id), + unique (tenant_id, branch_id) +); + +create index if not exists idx_oracle_pages_owner + on oracle_canvas_pages (tenant_id, owner_id, updated_at desc); +``` + +```sql +create table if not exists oracle_canvas_page_revisions ( + tenant_id uuid not null, + page_id uuid not null references oracle_canvas_pages(page_id) on delete cascade, + branch_id uuid not null, + revision_number integer not null, + parent_revision integer, + commit_kind text not null check (commit_kind in ('prompt', 'merge', 'rollback', 'manual_edit')), + committed_by uuid not null references users_and_roles(id), + prompt_execution_id uuid, + merge_request_id uuid, + revision_payload jsonb not null, + created_at timestamptz not null default now(), + primary key (tenant_id, page_id, revision_number) +); + +create index if not exists idx_oracle_page_revisions_branch + on oracle_canvas_page_revisions (tenant_id, branch_id, revision_number desc); +``` + +```sql +create table if not exists oracle_canvas_components ( + tenant_id uuid not null, + page_id uuid not null references oracle_canvas_pages(page_id) on delete cascade, + branch_id uuid not null, + component_id uuid not null, + version integer not null, + order_index integer not null, + section_id text not null, + component_type text not null, + component_payload jsonb not null, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + primary key (tenant_id, page_id, branch_id, component_id) +); + +create index if not exists idx_oracle_components_page_order + on oracle_canvas_components (tenant_id, page_id, branch_id, order_index); + +create index if not exists idx_oracle_components_payload_gin + on oracle_canvas_components using gin (component_payload jsonb_path_ops); +``` + +```sql +create table if not exists oracle_prompt_executions ( + tenant_id uuid not null, + execution_id uuid primary key, + page_id uuid not null references oracle_canvas_pages(page_id) on delete cascade, + branch_id uuid not null, + actor_id uuid not null references users_and_roles(id), + prompt text not null, + prompt_hash text not null, + intent_class text not null, + status text not null, + model_runtime text not null, + semantic_model_version text not null, + retrieval_plan jsonb, + visualization_plan jsonb, + warnings jsonb not null default '[]'::jsonb, + result_summary jsonb, + created_at timestamptz not null default now(), + completed_at timestamptz +); + +create index if not exists idx_oracle_prompt_executions_page + on oracle_prompt_executions (tenant_id, page_id, created_at desc); +``` + +```sql +create table if not exists oracle_component_templates ( + tenant_id uuid not null, + template_id uuid primary key, + name text not null, + category text not null, + origin text not null, + status text not null, + version text not null, + style_signature_hash text not null, + accepted_shapes jsonb not null, + template_payload jsonb not null, + validation_state jsonb not null, + provenance jsonb not null, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); + +create index if not exists idx_oracle_component_templates_category + on oracle_component_templates (tenant_id, category, status); +``` + +```sql +create table if not exists oracle_forks ( + tenant_id uuid not null, + fork_id uuid primary key, + source_page_id uuid not null references oracle_canvas_pages(page_id), + source_branch_id uuid not null, + source_revision integer not null, + fork_page_id uuid not null references oracle_canvas_pages(page_id), + fork_branch_id uuid not null, + recipient_user_id uuid not null references users_and_roles(id), + created_by uuid not null references users_and_roles(id), + status text not null, + created_at timestamptz not null default now() +); + +create table if not exists oracle_merge_requests ( + tenant_id uuid not null, + merge_request_id uuid primary key, + source_page_id uuid not null references oracle_canvas_pages(page_id), + source_branch_id uuid not null, + source_head_revision integer not null, + target_page_id uuid not null references oracle_canvas_pages(page_id), + target_branch_id uuid not null, + target_base_revision integer not null, + title text not null, + description text, + status text not null, + diff_payload jsonb not null, + conflicts_payload jsonb not null default '[]'::jsonb, + created_by uuid not null references users_and_roles(id), + reviewed_by uuid references users_and_roles(id), + created_at timestamptz not null default now(), + updated_at timestamptz not null default now() +); +``` + +```sql +create table if not exists oracle_lineage_records ( + tenant_id uuid not null, + lineage_record_id uuid primary key, + source_kind text not null, + source_id text not null, + transformation_type text not null, + transformation_spec_hash text, + produced_kind text not null, + produced_id text not null, + policy_snapshot_id text, + created_at timestamptz not null default now() +); + +create table if not exists oracle_audit_events ( + tenant_id uuid not null, + audit_event_id uuid primary key, + entity_type text not null, + entity_id text not null, + action text not null, + actor_id uuid not null references users_and_roles(id), + actor_type text not null, + correlation_id text not null, + execution_id uuid, + details jsonb not null default '{}'::jsonb, + created_at timestamptz not null default now() +); + +create index if not exists idx_oracle_audit_entity + on oracle_audit_events (tenant_id, entity_type, entity_id, created_at desc); +``` + +This schema is intentionally revision-friendly. `oracle_canvas_page_revisions` holds immutable snapshots for replay and rollback. `oracle_canvas_components` holds the latest projection for fast page fetch. `oracle_merge_requests` persists full diff payloads so that review decisions remain reproducible even after target branches advance. `oracle_audit_events` and `oracle_lineage_records` remain append-only. + +### 16.5 Event and WebSocket Blueprint + +The Oracle realtime layer should use the page WebSocket as a consumer-facing transport only. Internal fanout should happen over the event backbone. The page socket therefore subscribes to page-level events already committed or safely staged, not raw in-transaction state. + +The minimum event catalog for Sprint 1 should include `oracle.prompt.received`, `oracle.prompt.validated`, `oracle.prompt.failed`, `oracle.page.revision.committed`, `oracle.page.rollback.committed`, `oracle.fork.created`, `oracle.merge_request.opened`, `oracle.merge_request.updated`, `oracle.merge_request.merged`, `oracle.component.template.promoted`, and `oracle.presence.updated`. Every event must carry `tenantId`, `pageId` where relevant, `branchId` where relevant, `correlationId`, and a monotonic event timestamp. Client reconciliation should prefer revision numbers over event order whenever there is disagreement. + +## 17. Collaboration and Merge Algorithm Specification + +### 17.1 Revision Model + +Oracle pages are revisioned append-only objects. A branch has exactly one current head revision and zero or more prior revisions. A write request must always declare the branch head it believes it is modifying. If the declared base head does not match the actual current head at commit time, Oracle does not silently overwrite. For direct prompt executions on `main`, the system should automatically rebase if the new execution only appends components at the end and touches no existing component. For edits, reorders, or merge commits, the system should require either a safe automatic rebase or explicit user-visible conflict handling. + +Revision numbers are monotonic integers scoped to a page branch. Revision numbers never decrement and are never reused. Rollback is represented as a new revision with `commit_kind = 'rollback'` and a payload equal to or derived from a previously valid revision. The branch history therefore remains explainable without hidden mutation. + +### 17.2 Three-Way Merge Behavior + +Oracle merge behavior should be deterministic and component-aware. The merge base is the fork source revision. The merge source is the fork head. The merge target is the current target head at merge time. The diff engine compares the component identities, order indices, layout sections, visualization parameters, data bindings, and access controls across the three versions. + +The merge engine should classify changes before resolving them. If the source adds a new component and the target does not touch the same order window, the merge is automatic. If the source reorders components and the target edits unrelated components, the merge is automatic if the final order can be represented without overlapping slot ownership. If both source and target edit the same component's title but no other field, the merge becomes a text conflict. If both modify filters, data descriptors, access controls, or layout slots on the same component, the merge becomes a structured conflict that requires reviewer selection. + +The conflict classes are defined below. + +| Conflict class | Trigger | Default behavior | +| --- | --- | --- | +| `component_content_conflict` | Source and target change the same component field set | Manual review required | +| `query_descriptor_conflict` | Source and target modify the same data source descriptor or filter semantics | Manual review required | +| `layout_slot_conflict` | Source and target claim the same order window or section positioning in incompatible ways | Manual review required | +| `access_policy_conflict` | Source and target change role visibility or redaction policy differently | Manual review required | +| `delete_edit_conflict` | One side deletes a component while the other edits it | Manual review required | +| `safe_append` | Source adds only new components at free order indices | Auto-merge | +| `safe_reorder` | Reorders are compatible after normalization | Auto-merge | + +### 17.3 Order Index Strategy + +Order indices should be stored as integers with gaps, not as dense 1..N sequences rewritten on every insert. The recommended initial spacing is increments of 100. This allows local inserts and merge-time reorders with minimal rewrite pressure. When gaps become too tight within a section, Oracle may normalize order indices during a maintenance revision that preserves visible order but rewrites indices. That normalization revision should be treated as a first-class page commit and audited. + +### 17.4 Merge Review UX Contract + +The merge review UI must not force reviewers to inspect raw JSON to understand common changes. For component additions, the UI should show the rendered component preview plus the prompt or source branch explanation. For reorders, it should show before and after page position. For edits, it should show field-level change cards grouped by display text, filters, measure definitions, visibility, and layout. For access changes, it should show a security warning surface because those are high-risk changes even when visually small. + +### 17.5 Conflict Resolution Serialization + +When a reviewer resolves a conflict, the choice must be serialized as part of the merge decision. This makes the merge replayable and auditable. A resolution record should state which conflict id was resolved, whether the source or target version won or whether a manual composite was created, and the exact resolved payload hash. This is critical for legal traceability and for deterministic replay in CI. + +An example resolution payload is shown below. + +```json +{ + "mergeRequestId": "mr_01JQXB222MERGE", + "resolutions": [ + { + "conflictId": "conf_01JQXB222LAYOUT", + "resolutionType": "manual_composite", + "resolvedPayloadHash": "sha256:resolvedLayoutA", + "comment": "Accepted source follow-up queue but retained target map placement in section two." + } + ] +} +``` + +## 18. CRM Operating Model and Product Behavior + +### 18.1 First-Principles CRM Design + +The best CRM in this category is not the one with the most menus. It is the one that minimizes duplicate work, makes ownership explicit, preserves institutional memory, and helps a broker move a lead forward at the right moment. Oracle should therefore follow five first-principles rules. + +The first rule is that every lead action should have one canonical home. If a broker asks Oracle to create a task, tag a lead, or change ownership, that action should update the underlying record system directly and then appear everywhere else from the same event source. There should be no parallel note pad, shadow task list, or sidecar spreadsheet workflow. + +The second rule is that analysis should become action without context loss. A chart that identifies the top five under-served whale leads should let the user create follow-ups, assign brokers, or open a fork for team review directly from the component. Oracle should not force the user to leave insight space and reopen the same data in another module to act on it. + +The third rule is that branch review should replace version confusion. Teams often circulate screenshots and exported PDFs because shared analytics are fragile. Oracle replaces that with durable page branches, merge requests, and revision history so that decisions are debated on live artifacts rather than stale exports. + +The fourth rule is that AI should remove clerical effort, not remove accountability. AI may ingest notes, summarize visits, propose tasks, suggest schema extensions, and compose component layouts, but ownership of sensitive merges, role access, and structural change remains explicit and reviewable. + +The fifth rule is that the CRM must remain operable for brokers under time pressure. The default workflows should be fast, language-based, and forgiving. The product should always feel simpler than Salesforce while remaining more trustworthy than a generic chat front end. + +### 18.2 Core Business Entities + +The CRM model should revolve around a concise set of entities tailored to real-estate sales instead of generic B2B deal abstractions alone. The principal entities are `Lead`, `HouseholdOrInvestor`, `BrokerUser`, `Project`, `Unit`, `VisitSession`, `SentinelSignalRollup`, `CampaignTouch`, `Task`, `OfferOrReservation`, `VaultShare`, `CanvasPage`, and `MergeRequest`. The reason for keeping this set small is to reduce semantic drift between the operational database and the AI planner. Every extra entity the model cannot explain cleanly becomes a long-term source of hallucination and reporting inconsistency. + +The recommended operational sequence is lead capture, enrichment, qualification, assignment, engagement, visit evidence, follow-up, offer progression, reservation or close, and post-close relationship management. Oracle components should map naturally onto this lifecycle. Pipeline boards, visit timelines, unit-fit matrices, campaign quality views, and broker follow-up queues should feel like native product surfaces rather than reporting afterthoughts. + +### 18.3 Persona Journeys + +For a developer sales gallery, the key user is the sales director who wants to know which investor profiles are reacting to which projects, which brokers are moving high-value leads fast enough, and which units need narrative repositioning. Oracle should let this user open a main page for a project launch, compare lead source quality, map investor concentration, inspect Sentinel reaction patterns, share the page to a senior broker, and accept a merge that adds an operational call queue. + +For a brokerage principal, the key user is the owner or operations manager who fears lead leakage and inconsistent follow-up. Oracle should make lead ownership visible, document every assignment and branch of reasoning, and prevent data wandering into rogue spreadsheets. The principal should be able to ask for pipeline health by broker, see which leads have gone cold despite strong prior sentiment, and publish an official team canvas that brokers can fork but not overwrite. + +For a boutique founder, the key user is a small-team operator who needs leverage more than administrative breadth. Oracle should help this user look enterprise-grade without requiring a data team. The default catalog should therefore include project overview, investor geography, follow-up gaps, unit-fit suggestions, and campaign quality views that work out of the box on a small but growing lead base. + +### 18.4 Edge Cases the CRM Must Handle + +Oracle must handle duplicated leads arriving from multiple channels without producing conflicting canvases or inconsistent ownership. It must handle leads who visit a gallery without giving full identity until later. It must handle retroactive data corrections from brokers or stewards without corrupting historical page revisions. It must handle data retention requests without breaking audit integrity. It must handle large campaigns that create bursts of low-quality leads without degrading the responsiveness of whale-level analysis. + +It must also handle a social and organizational edge case: multiple brokers forming competing interpretations of the same data. That is exactly why the branch and merge model matters. The product should encourage experimentation on forks and clarity on mainline, rather than forcing teams into a fake live consensus that silently overwrites dissenting work. + +## 19. Delivery Work Packages and Sequencing + +### 19.1 Recommended Build Order + +The implementation should proceed in a sequence that unlocks user-visible value early while de-risking the hard parts before pilot rollout. The correct order is contract foundation first, then persistence and prompt orchestration, then canvas rendering, then collaboration, then synthesis and hardening. + +| Work package | Primary outcome | Dependency | Acceptance signal | +| --- | --- | --- | --- | +| WP1 Contract foundation | JSON Schemas, API contracts, semantic model baseline, Oracle router skeleton | None | Mock client fully replaced with typed contract stubs | +| WP2 Persistence and page core | Page tables, revision tables, component projection, page fetch and rollback | WP1 | Page can persist and replay deterministic revisions | +| WP3 Prompt orchestration | Prompt execution records, Nemoclaw structured planning, policy validation, query gateway | WP1 and WP2 | Prompt can create committed components on a page | +| WP4 Frontend canvas | Virtualized canvas, component registry, prompt rail refactor, branch bar | WP2 and WP3 | User can see persisted multi-component page revisions | +| WP5 Collaboration | Share, fork, merge request, review UI, merge commit flow | WP2 and WP4 | Team can complete the share to merge golden path | +| WP6 Catalog and synthesis | Premade catalog API, synthesis persistence, validation pipeline, tenant auto-promotion | WP3 and WP4 | New template can be synthesized and reused safely | +| WP7 Hardening and pilot ops | SLO tuning, observability, security review, canary controls, incident playbooks | All prior packages | Pilot tenant can run daily workload under guardrails | + +### 19.2 Team Responsibilities + +The frontend team owns the page shell refactor, canvas viewport, prompt rail, branch bar, merge review UI, component registry, and performance instrumentation. The backend team owns Oracle routing, orchestration, persistence, query gateway, collaboration engine, and websocket behavior. The data team or backend data owner owns semantic model versioning, derived rollups, indexes, and stewardship proposal flow. The platform owner owns deployment, secret handling, runtime policies, monitoring, and canary controls. QA owns replay fixtures, conflict scenarios, accessibility verification, and pilot acceptance gates. + +### 19.3 Sprint-to-Pilot Timeline + +The recommended timeline assumes one sprint for contract and persistence foundation, one sprint for prompt-to-canvas flow, one sprint for collaboration and merge review, and one sprint for synthesis and hardening. If the team needs to compress delivery, synthesis should compress last, not first. The highest-value MVP is a reliable prompt-to-canvas system with branch review, not a flashy synthesis engine with weak governance. + +### 19.4 Pilot Rollout Pattern + +The first pilot should use one design-led or process-disciplined tenant rather than the largest possible tenant. The ideal pilot tenant is large enough to exercise the collaboration model and small enough to support close observation. The pilot should start with private pages and direct-fork sharing only. Team-wide branch promotion, auto-promotion thresholds, and heavier operational actions should be enabled after the first two weeks of stable usage and after merge integrity metrics remain healthy. + +The pilot should include structured office hours with the sales director and at least one senior broker so that confusing merge behavior, ambiguous prompt patterns, and missing catalog templates are captured early. Oracle will live or die on whether the team trusts it as a daily operating surface. That trust is built by clean revisions, clear explanations, and predictable merge behavior more than by raw model cleverness. + +## 20. Production Readiness Exit Criteria + +Oracle should not be considered production-ready merely because the UI is complete or the first prompt works. The system is production-ready only when the product, platform, and governance layers all pass together. + +The product layer is ready when a sales director can use Oracle to ask a real business question, receive a durable multi-component page, share it to a teammate as a fork, review a merge request, and rely on the page as a repeatable operational artifact. The platform layer is ready when prompt, query, render, persist, and websocket metrics remain inside the SLO envelope under pilot load and when rollback is proven in rehearsal. The governance layer is ready when audit, lineage, retention, access control, and stewardship proposal flows have all been exercised with real operators. + +The following table defines the final go-live bar. + +| Area | Exit requirement | +| --- | --- | +| Contracts | All `v1` schemas and OpenAPI examples frozen and published | +| Persistence | Revision replay, rollback, and merge replay proven in automated tests | +| Security | Tenant isolation and access control penetration checks show no high-severity gaps | +| Observability | Dashboards, alerts, trace correlation, and incident playbooks operational | +| Performance | Prompt, render, and canvas scroll SLOs met on pilot-like datasets | +| Collaboration | Share, fork, merge, reject, and rollback all pass deterministic end-to-end tests | +| Catalog | At least the baseline premade catalog is live and synthesis auto-promotion is bounded to tenant scope | +| Governance | Data steward can review and approve an AI schema proposal without manual database patching | + +### 20.1 Immediate Next Implementation Moves + +The first concrete move after accepting this artifact should be to freeze the current mock Oracle query client as deprecated and create the typed Oracle API client that speaks in page and execution contracts. The second move should be to add Oracle page tables and revision tables to PostgreSQL. The third move should be to mount the new Oracle router in `backend/main.py`. Only after those three foundations exist should the team invest in merge review UI or template synthesis. That order keeps the product grounded in durable state rather than another generation of temporary mocks. + +## 21. Repository Cutover and File Mapping + +### 21.1 Purpose of the Cutover Map + +The Oracle artifact is intentionally specific to this repository rather than written as a generic platform brief. This section translates the design into concrete repo movement so the implementation team can see which current files are visual references, which files are temporary scaffolding, which runtime entry points need modification, and which new modules should be introduced first. + +The current Oracle frontend should be treated as a design shell and interaction seed. The current Oracle backend placeholders should be treated as disposable stubs. The Sentinel work already present in the repo should be treated as the closest backend precedent for audited AI-assisted flows, especially around websocket usage, asyncpg integration, and Nemoclaw client patterns. + +### 21.2 Existing File Roles + +| Existing path | Current role | Oracle cutover decision | +| --- | --- | --- | +| `app/src/app/oracle/page.tsx` | Premium Oracle UI shell with mock view switching | Keep visual language, refactor into page orchestration and virtualized canvas host | +| `app/src/lib/oracleQueryClient.ts` | Temporary single-response mock client | Deprecate and replace with typed `oracleApiClient` speaking page and execution contracts | +| `backend/main.py` | Active FastAPI entrypoint without mounted Oracle runtime | Extend to mount Oracle `v1` router and Oracle websocket namespace | +| `backend/api/routes_oracle.py` | Empty placeholder | Do not preserve as contract surface; replace with new domain router | +| `backend/api/routes_crm.py` | Empty placeholder | Treat as unused placeholder unless later repurposed intentionally | +| `backend/services/nemoclaw_client.py` | Active structured AI runtime abstraction | Reuse for Oracle planning path with Oracle-specific prompt wrappers and validation | +| `backend/db/schema.sql` | Current PostgreSQL schema baseline focused on Sentinel and auth-adjacent data | Extend with Oracle tables through migrations rather than ad hoc inline edits | +| `app/src/store/useStore.ts` | General app state store with mocked Oracle data assumptions | Reduce Oracle-specific mock state and move Oracle remote state into domain hooks or slice modules | + +### 21.3 Recommended New File and Module Additions + +| Proposed path | Responsibility | +| --- | --- | +| `app/src/oracle/components/CanvasViewport.tsx` | Virtualized canvas renderer and scroll-anchor control | +| `app/src/oracle/components/BranchBar.tsx` | Branch identity, revision status, share and merge entrypoints | +| `app/src/oracle/components/PromptRail.tsx` | Prompt input, execution history, and turn-to-revision linking | +| `app/src/oracle/components/review/MergeReviewDrawer.tsx` | Merge diff and conflict resolution UI | +| `app/src/oracle/lib/oracleApiClient.ts` | Typed Oracle HTTP and websocket client | +| `app/src/oracle/hooks/useOraclePage.ts` | Page hydration, branch refresh, optimistic reconciliation | +| `app/src/oracle/hooks/useOracleExecution.ts` | Prompt submission and execution progress | +| `backend/oracle/router_v1.py` | Mounted Oracle API and websocket surface | +| `backend/oracle/prompt_orchestrator.py` | Prompt intake through validated plan production | +| `backend/oracle/canvas_service.py` | Page, revision, rollback, and component projection writes | +| `backend/oracle/collaboration_service.py` | Share, fork, merge request, diff, merge commit | +| `backend/oracle/catalog_service.py` | Template list, synthesis persistence, validation and promotion | +| `backend/oracle/data_access_gateway.py` | Plan-to-query compilation and execution | +| `backend/oracle/policy_service.py` | Tenant scope, role scope, privacy tier, redaction | +| `backend/oracle/audit_service.py` | Oracle-specific immutable audit writes and queries | +| `backend/oracle/lineage_service.py` | Oracle lineage edge persistence and replay | +| `backend/oracle/semantic_model.py` | Business vocabulary and dataset alias layer | +| `backend/tests/oracle/` | Contract, execution, merge, and replay test suite | + +### 21.4 Implementation Cutover Sequence in the Repo + +The first repo change should introduce Oracle domain modules without deleting the current UI. The second should route the current page through the new API client while still rendering a minimal premade component set. The third should remove direct dependence on `OracleQueryResult` from the page. The fourth should add collaboration and revision history. Only then should the team delete the legacy mock query client entirely. This sequence keeps the UI visually stable while moving the data plane underneath it. + +The migration rule should be that temporary compatibility shims are allowed only when they preserve forward movement toward the page-and-revision contract. No new code should be added against the deprecated single-result Oracle contract after the Oracle `v1` API client exists. + +## 22. Success Metrics and Adoption Governance + +### 22.1 Why Oracle Needs Post-Launch Governance + +A sophisticated CRM artifact is not successful because it can answer prompts. It is successful because operators trust it enough to replace lower-trust behaviors such as screenshots, spreadsheets, duplicated notes, and ad hoc dashboard exports. Oracle therefore needs a post-launch success model that measures not only runtime health but operator adoption, merge hygiene, decision velocity, and revenue relevance. + +### 22.2 Product and Usage KPIs + +| Metric | Definition | Initial target | +| --- | --- | --- | +| Weekly active Oracle operators | Unique tenant users who execute at least one prompt or review one merge request in a week | 70% of licensed pilot users by week 4 | +| Prompt-to-committed-page rate | Share of prompt executions that result in at least one committed component revision | >= 75% in pilot after initial tuning | +| Reuse rate of committed components | Share of new prompts that reuse existing catalog templates instead of requiring synthesis | >= 60% by end of pilot | +| Fork-to-merge completion rate | Share of opened merge requests that reach approved or closed-with-decision state | >= 85% within 5 business days | +| Median time from insight to action | Time from prompt completion to task creation, assignment, or follow-up action | <= 10 minutes for action-oriented workflows | +| Canvas revisit rate | Share of committed pages reopened within 7 days | >= 50% for team-owned pages | + +### 22.3 Operational and Governance KPIs + +| Metric | Definition | Initial target | +| --- | --- | --- | +| Policy denial precision | Share of denied requests later confirmed as correctly blocked | >= 95% | +| Merge integrity incidents | Count of silent overwrite, lost component, or non-replayable merge outcomes | 0 | +| Template validation escape rate | Share of promoted templates later revoked due to missed validation failure | < 1% | +| Audit completeness | Share of mutating Oracle actions with full correlation, actor, and entity metadata | 100% | +| Lineage completeness | Share of rendered components with valid lineage links to source data and prompt execution | >= 99.5% | +| Stewardship turnaround time | Median time for a data steward to approve or reject an AI schema proposal | <= 3 business days | + +### 22.4 Business Outcome KPIs + +Oracle should also be evaluated on business impact rather than product usage alone. For developer and brokerage deployments, the important business signals are improved follow-up speed, reduced lead leakage, faster identification of high-intent investors, improved broker prioritization, and stronger operational confidence in AI-assisted reporting. The recommended business scorecard should track response time to high-value leads, conversion movement among whale or high-intent segments, percentage of team decisions backed by shared Oracle pages instead of exported decks, and reduction in duplicate manual reporting effort. + +### 22.5 Governance Cadence + +The recommended governance cadence is weekly during pilot and biweekly after stabilization. The weekly review should include one product owner, one engineering lead, one platform or SRE lead, one data steward, and one business operator from the tenant side. The review agenda should inspect adoption metrics, merge behavior, denied prompts, slow components, repeated synthesis failures, schema proposal queue, and any evidence that brokers are still falling back to external spreadsheets or screenshots for core workflows. + +The most important governance rule is that Oracle should evolve through evidence. New catalog categories, new synthesis heuristics, and new access policies should be driven by observed operator patterns, not by abstract feature accumulation. This keeps the CRM simple, functional, and aligned with the original goal of building the most effective real-estate operating system rather than the most complicated one. diff --git a/.Agent Context/Sprint 1/Software Requirements Specification (SRS)_ The Sentinel _Quantum Dynamics_ Engine & Active Pipeline.md b/.Agent Context/Sprint 1/Software Requirements Specification (SRS)_ The Sentinel _Quantum Dynamics_ Engine & Active Pipeline.md new file mode 100644 index 00000000..463385ed --- /dev/null +++ b/.Agent Context/Sprint 1/Software Requirements Specification (SRS)_ The Sentinel _Quantum Dynamics_ Engine & Active Pipeline.md @@ -0,0 +1,239 @@ +### Software Requirements Specification (SRS): The Sentinel Quantum Dynamics Engine & Active Pipeline + +Updated: April 2, 2026 +Status: Current truth after code implementation and AWS/NVMe deployment recovery + +#### 1. Purpose + +The Sentinel is the biometric and engagement intelligence module inside Project Velocity. It combines local perception capture, broker-facing live session controls, scene-aware biometric scoring, trackable asset notifications, and automated lead enrichment into one operational pipeline. + +The current implementation is no longer a purely planned architecture. The backend, frontend session flow, PostgreSQL schema, CCTV ingestion path, scene upload path, notification center, and test harness all exist in the repository. The live backend deployment has also been rebuilt on the AWS NVMe-backed node. + +#### 2. Current Architectural Truth + +The architecture now operates as four cooperating layers: + +1. Perception Layer + MediaPipe Face Landmarker runs in the frontend via `app/src/hooks/useMediapipeFaceLandmarker.ts`. The browser captures webcam frames locally and emits compact blend-shape packets rather than raw video. + +2. Application Layer + FastAPI runs from `backend/main.py`. It exposes Sentinel WebSocket endpoints, Vault endpoints, Scenes upload endpoints, CCTV ingestion endpoints, and authentication. + +3. Data Layer + PostgreSQL is the system of record. The core schema is in `backend/db/schema.sql` and `backend/db/schema_addendum.sql`. Session, CCTV, vault, consent, scene maps, and omnichannel logs are persisted here. + +4. Reasoning Layer + NemoClaw prompt logic is implemented in `backend/services/nemoclaw_client.py`. The current production-truth inference path is NVIDIA-hosted OpenAI-compatible chat completions, not local Ollama. OpenShell and Ollama still exist on the node as related runtime infrastructure, but the FastAPI backend currently uses NVIDIA as the primary scoring path. + +#### 3. Current Runtime Topology + +AWS node public IP as of April 2, 2026: `54.152.236.10` + +Ports: + +- `22` SSH +- `443` nginx TLS reverse proxy +- `127.0.0.1:8001` FastAPI/Uvicorn backend +- `127.0.0.1:5432` PostgreSQL +- `8080` OpenShell gateway / NemoClaw gateway bootstrap target +- `11434` local Ollama runtime + +Storage and runtime paths: + +- Backend release root: `/opt/dlami/nvme/velocity/current` +- Backend environment file: `/opt/dlami/nvme/velocity/env` +- Backend virtualenv: `/opt/dlami/nvme/velocity/venv` +- Backend TLS cert/key: `/opt/dlami/nvme/velocity/tls/velocity.crt` and `/opt/dlami/nvme/velocity/tls/velocity.key` +- Prompt directory: `/opt/dlami/nvme/nemoclaw/prompts` +- PostgreSQL data directory: `/opt/dlami/nvme/pgdata/14/velocity` +- Asset directory: `/opt/dlami/nvme/assets` +- Marketing-video directory: `/opt/dlami/nvme/assets/videos` +- Marketing-video catalog: `/opt/dlami/nvme/assets/videos/catalog.json` + +Systemd units: + +- `velocity-backend.service` +- `postgresql@14-velocity.service` +- `nginx` +- `nemoclaw-velocity.service` + +#### 4. Frontend Truth + +Implemented frontend components: + +- `app/src/components/modules/Sentinel.tsx` + Provides the Sentinel subtab structure with `Overview` and `Live Session`. + +- `app/src/components/modules/sentinel/SentinelLiveSession.tsx` + Implements the live workflow with: + `select-video -> select-mode -> select-lead -> session -> summary` + It now also includes scene CSV upload in the video-management flow, searchable assigned-mode lead selection, and hover-preview marketing video cards sourced from the backend catalog. + +- `app/src/components/modules/sentinel/PerceptionPlayer.tsx` + Plays the video, opens webcam capture, emits biometric packets every 500ms, receives QD updates, and closes the session through the backend on video end. + +- `app/src/components/layout/NotificationCenter.tsx` + Displays live notifications from the backend for vault opens, QD spikes, and lead-tagging events. + +- `app/src/hooks/useVelocitySocket.ts` + Uses the correct backend WebSocket namespace: + `/api/sentinel/ws/notifications` + `/api/sentinel/ws/perception` + +#### 5. Backend Truth + +Implemented backend routers: + +- `backend/routers/sentinel.py` + Handles: + `/api/sentinel/ws/notifications` + `/api/sentinel/ws/perception` + `/api/sentinel/consent` + `/api/sentinel/session/complete` + `/api/sentinel/tag-lead` + `/api/sentinel/qd-score/{lead_id}` + +- `backend/routers/vault.py` + Handles trackable asset delivery and `WS_ASSET_OPENED` broadcast. + +- `backend/routers/scenes.py` + Handles: + `POST /api/scenes/upload` + `GET /api/scenes/{video_asset_id}` + +- `backend/routers/videos.py` + Handles: + `GET /api/videos/marketing` + Returns catalog-driven or directory-scanned marketing videos for the live-session picker. + +- `backend/routers/cctv.py` + Handles: + `POST /api/cctv/event` + `POST /api/cctv/finalize-auto-mode` + +Implemented backend services: + +- `backend/services/nemoclaw_client.py` + NVIDIA API primary inference client, scene-aware QD scoring, lead tagging, CCTV visitor profiling, and malformed JSON recovery. + +- `backend/services/auto_mode_matcher.py` + Post-session matching for auto mode using plate history, tag overlap, and fallback auto-lead creation. + +- `backend/db/pool.py` + asyncpg connection pool for FastAPI. + +#### 6. Database Truth + +The following tables are part of the current truth: + +- `users_and_roles` +- `leads_intelligence` +- `velocity_vault_assets` +- `omnichannel_logs` +- `consent_log` +- `video_scene_maps` +- `perception_sessions` +- `cctv_events` + +The addendum schema is no longer optional planning material. It is part of the required production schema. + +#### 6A. Marketing Video Asset Truth + +The Sentinel live-session picker is no longer hardcoded to mock cards. The current truth is: + +- The frontend requests `GET /api/videos/marketing` +- The backend reads `/opt/dlami/nvme/assets/videos/catalog.json` when present +- The backend falls back to recursive video discovery inside `/opt/dlami/nvme/assets/videos` +- The selected asset is streamed through `/assets/videos/` +- The four current walkthrough assets are: + - `eden-devprayag.mp4` + - `sugam-prakriti.mp4` + - `atri-aqua.mp4` + - `atri-surya-toron.mp4` + +This matters because adding more walkthrough assets is now a backend-content operation, not a frontend rebuild requirement. + +#### 7. NemoClaw / Reasoning Truth + +The original plan assumed local agent-contained reasoning through OpenShell plus local Ollama. That is no longer the active scoring path. + +Current truth: + +- The backend client uses NVIDIA-hosted OpenAI-compatible completions as primary inference. +- Current primary model: `nvidia/nemotron-3-super-120b-a12b` +- Current fallback model: `nvidia/llama-3.3-nemotron-super-49b-v1` +- Local Ollama remains installed on `11434` but is not the active backend primary. +- OpenShell gateway remains present on `8080` and is bootstrapped by `nemoclaw-velocity.service`. + +Reason for this change: + +- The local Ollama path was not reliably producing the strict JSON contract needed for QD scoring. +- The NVIDIA path is OpenAI-compatible and easier to integrate directly from FastAPI. +- The NVIDIA path is now what the code and live environment are configured to use. + +#### 8. Milestone Truth + +Milestone 1: Complete +Database schema, auth dependencies, store/types, and live PostgreSQL deployment exist. + +Milestone 2: Complete +Vault generation, notification ingestion, and frontend notification center are implemented. + +Milestone 3: Complete +Perception pipeline frontend, live session flow, and packet emission are implemented. + +Milestone 4: Complete +NemoClaw prompt system and backend scoring/tagging routes are implemented. The inference truth changed from local Ollama-first planning to NVIDIA-primary production use. + +Milestone 5: Code complete, operational feed integration still pending +CCTV router, OCR bridge script, and auto-mode matcher exist. What remains is wiring a real ONVIF/RTSP/OCR producer into `/api/cctv/event`. + +Milestone 6: Complete +Scene upload backend, scene lookup, frontend upload UI, and scene-aware packet correlation are implemented. + +Milestone 7: Complete +NVMe-backed backend deployment, env file, PostgreSQL, systemd, nginx TLS, and prompt placement are in place. + +Milestone 8: Complete at repo test level, still desirable to run a broker-facing live acceptance pass +Backend pytest tests and frontend Playwright tests exist and pass locally. + +#### 9. Known Risks + +1. NVIDIA model JSON consistency + The NVIDIA model is reachable and active, but it does not always honor strict JSON perfectly for the full QD prompt. The client now includes malformed-response recovery, but this remains the main live inference risk. + +2. Dynamic public IP churn + The node public IP changed during execution. The system still needs either an Elastic IP or a stable DNS record. + +3. Self-signed TLS + nginx currently serves TLS, but a trusted certificate path is still preferable for production rollout. + +4. External CCTV source not yet hard-connected + The ingest API exists, but a real external bridge process still needs to be pointed at it in production. + +5. Video catalog hygiene + If operators copy MP4s to NVMe without updating `catalog.json`, the fallback scan will keep the assets visible, but broker-facing order and labels may become inconsistent. + +#### 10. Acceptance Criteria + +The Sentinel is considered aligned with current truth when all of the following hold: + +- `https://54.152.236.10/health` returns backend status `ok` +- PostgreSQL is live on NVMe and all eight tables exist +- `velocity-backend.service` is active +- `nginx` is active on `443` +- `nemoclaw-velocity.service` is active +- `POST /api/scenes/upload` accepts a valid CSV +- `POST /api/cctv/event` persists a CCTV event +- `POST /api/sentinel/session/complete` finalizes assigned or auto sessions +- `GET /api/videos/marketing` returns the current marketing-video catalog +- Live QD updates are visible in the frontend through the Sentinel WebSocket path +- Playwright tests and backend tests pass + +#### 11. Appendices + +Related documents: + +- `velocity_status_report.md` +- `nemoclaw_setup_truth.md` +- `The Sentinel Bibel.md` diff --git a/.Agent Context/Sprint 1/The Sentinel Bibel.md b/.Agent Context/Sprint 1/The Sentinel Bibel.md new file mode 100644 index 00000000..9e3376de --- /dev/null +++ b/.Agent Context/Sprint 1/The Sentinel Bibel.md @@ -0,0 +1,277 @@ +# The Sentinel Bibel + +Updated: April 2, 2026 + +## 1. What The Sentinel Is + +The Sentinel is the biometric, session-intelligence, and attention-scoring engine inside Project Velocity. It exists to help brokers understand how a prospect reacts to a guided property walkthrough in real time, without streaming raw webcam video to a remote backend. + +It fuses: + +- browser-side facial blend-shape capture +- scene-aware timestamp correlation +- CRM context +- asset-open notifications +- CCTV-assisted auto-mode enrichment +- lead tagging and session finalization + +The output is not just a number. The output is operational intelligence. + +## 2. Core Principles + +### Local-first perception + +The webcam is processed in the browser through MediaPipe. The backend receives only compact biometric packets, not raw video. + +### NVMe-first deployment + +Runtime data, prompts, PostgreSQL, certificates, and backend release assets belong on `/opt/dlami/nvme`, not the root volume. + +### PostgreSQL is the source of truth + +No Supabase database runtime is used for the Sentinel workflow. Session, consent, vault, CCTV, and sentiment history all land in PostgreSQL. + +### Broker-facing intelligence must be real time + +The system pushes QD updates and vault-open signals back through WebSockets so the broker can act during or immediately after the session. + +### Architecture follows truth, not plan nostalgia + +The original plan centered more heavily on local Ollama/OpenShell inference. The current deployed truth is NVIDIA-primary reasoning with OpenShell/Ollama still present as neighboring infrastructure. + +## 3. How The Sentinel Works + +### Assigned Mode + +1. Broker selects a marketing video +2. Broker selects `Assigned Mode` +3. Broker selects an existing lead +4. Prospect watches the video while the browser emits biometric packets +5. Backend scores QD using scene context plus CRM context +6. Lead score updates and notifications are visible in real time + +### Auto Mode + +1. Broker selects a marketing video +2. Broker selects `Auto Mode` +3. Session runs without a pre-bound lead +4. Browser emits biometric packets tied to a session UUID +5. CCTV events enrich the same session through `/api/cctv/event` +6. Session closes through `/api/sentinel/session/complete` +7. Auto-mode matcher links an existing lead or creates a new one + +## 4. The Files That Matter + +### Frontend + +`app/src/components/modules/Sentinel.tsx` +Sentinel shell and tab structure. + +`app/src/components/modules/sentinel/SentinelLiveSession.tsx` +The broker-facing session wizard. This is the orchestration UI. + +`app/src/components/modules/sentinel/PerceptionPlayer.tsx` +Video playback, webcam capture, packet emission, and session closeout. + +`app/src/hooks/useMediapipeFaceLandmarker.ts` +MediaPipe bootstrap and frame detection. + +`app/src/hooks/useVelocitySocket.ts` +Notification and perception WebSocket connection logic. + +`app/src/components/layout/NotificationCenter.tsx` +Top-bar operational notification panel. + +### Backend + +`backend/main.py` +FastAPI entrypoint and router registration. + +`backend/routers/sentinel.py` +Perception WebSocket, notifications, consent, session completion, tag-lead route, QD score endpoint. + +`backend/routers/vault.py` +Trackable links and vault-open broadcast flow. + +`backend/routers/scenes.py` +Scene CSV upload and scene retrieval. + +`backend/routers/videos.py` +Marketing-video catalog loading and fallback filesystem discovery. + +`backend/routers/cctv.py` +CCTV event ingestion and auto-mode finalization. + +`backend/services/nemoclaw_client.py` +NVIDIA-primary prompt client and response parsing. + +`backend/services/auto_mode_matcher.py` +Auto-mode session-to-lead attribution logic. + +`backend/db/schema.sql` +Core Sentinel relational model. + +`backend/db/schema_addendum.sql` +Scene maps, perception sessions, CCTV events. + +## 5. The Tables That Matter + +`users_and_roles` +RBAC and broker identity. + +`leads_intelligence` +Lead record, tags, QD score, broker assignment. + +`velocity_vault_assets` +Trackable asset share records. + +`omnichannel_logs` +Event history and sentiment spikes. + +`consent_log` +Biometric consent actions. + +`video_scene_maps` +Scene labels and timestamp ranges for each marketing video. + +`perception_sessions` +Assigned or auto-mode session records. + +`cctv_events` +OCR/vehicle/visitor-derived session evidence. + +## 6. Ports and Paths + +Public/backend-facing truth: + +- `22` SSH +- `443` nginx TLS +- `127.0.0.1:8001` Uvicorn +- `127.0.0.1:5432` PostgreSQL +- `8080` OpenShell gateway +- `11434` Ollama + +Paths: + +- `/opt/dlami/nvme/velocity/current` +- `/opt/dlami/nvme/velocity/env` +- `/opt/dlami/nvme/velocity/venv` +- `/opt/dlami/nvme/velocity/tls` +- `/opt/dlami/nvme/nemoclaw/prompts` +- `/opt/dlami/nvme/pgdata/14/velocity` +- `/opt/dlami/nvme/assets` +- `/opt/dlami/nvme/assets/videos` +- `/opt/dlami/nvme/assets/videos/catalog.json` + +## 7. What Is True About NemoClaw + +The Sentinel still conceptually uses NemoClaw as its reasoning layer, but the active truth is: + +- The backend uses NVIDIA-hosted completions as primary inference +- The active primary model is `nvidia/nemotron-3-super-120b-a12b` +- The backend prompt layer is implemented in repo code, not hidden inside a separate agent black box +- OpenShell and Ollama still exist on the node, but they are not the active primary scoring path + +This distinction matters. If someone assumes the live QD score is coming from local Ollama, they are wrong. + +## 8. Event Flows + +### QD update flow + +Frontend packet +-> `/api/sentinel/ws/perception` +-> scene lookup in `video_scene_maps` +-> QD scoring in `nemoclaw_client.py` +-> `perception_sessions` and `omnichannel_logs` +-> QD broadcast +-> frontend QD ring and notifications + +### Marketing video flow + +Frontend live-session picker +-> `GET /api/videos/marketing` +-> catalog read from `/opt/dlami/nvme/assets/videos/catalog.json` +-> fallback recursive scan of `/opt/dlami/nvme/assets/videos` +-> MP4 served through `/assets/videos/...` +-> hover-preview card and full `PerceptionPlayer` playback + +### Vault flow + +Broker generates velocity link +-> prospect opens `/vault/{tracking_hash}` +-> backend logs `WS_ASSET_OPENED` +-> notification broadcast +-> NotificationCenter shows the event + +### Auto-mode CCTV flow + +OCR bridge posts to `/api/cctv/event` +-> backend profiles plate/vehicle via `cctv_profiler` +-> `cctv_events` row is written +-> session evidence is updated +-> session closes +-> `auto_mode_matcher.py` links or creates lead + +## 9. What Can Break + +### Structured output from the model + +The NVIDIA path is reachable, but model output is not always perfect JSON under the full prompt. This is the main runtime fragility. + +### Wrong WebSocket path + +The correct Sentinel socket namespace is `/api/sentinel/ws/...`. Any client using `/ws/...` directly will miss the router prefix and fail. + +### Root-volume drift + +If deployment scripts start writing runtime state back to root instead of NVMe, disk-pressure regressions will return. + +### Video catalog drift + +If new walkthrough files are copied to NVMe without catalog updates, the system still loads them, but the presentation order and labels can become unreliable. + +### Dynamic public IP assumptions + +If external scripts hardcode an old public IP, they will fail. Current truth must be rechecked whenever AWS reassigns the address. + +## 10. What “Done” Actually Means + +The Sentinel is done at the engineering level when: + +- all milestone code exists +- backend services start cleanly on NVMe-backed runtime +- PostgreSQL schema is live +- frontend build succeeds +- backend tests pass +- Playwright tests pass +- health endpoint is live over TLS + +The Sentinel is done at the operational level only when: + +- a stable DNS or Elastic IP is assigned +- trusted TLS is installed +- production CCTV/OCR source is wired in +- a live broker walkthrough has been accepted on the deployed system + +## 11. Commands Worth Remembering + +```bash +curl -k https://54.152.236.10/health +sudo systemctl status velocity-backend.service +sudo systemctl status postgresql@14-velocity.service +sudo systemctl status nginx +sudo systemctl status nemoclaw-velocity.service +sudo -u postgres psql -d velocity -c '\dt' +``` + +## 12. Final Rule + +Do not maintain two truths. + +If deployment reality changes, update: + +- the SRS +- `nemoclaw_setup_truth.md` +- this Bibel + +before anyone else builds on stale assumptions. diff --git a/.Agent Context/Sprint 1/Velocity Kolkata Customer 0 Strategy - Get My Ghar.md b/.Agent Context/Sprint 1/Velocity Kolkata Customer 0 Strategy - Get My Ghar.md new file mode 100644 index 00000000..6a3f292c --- /dev/null +++ b/.Agent Context/Sprint 1/Velocity Kolkata Customer 0 Strategy - Get My Ghar.md @@ -0,0 +1,503 @@ +# Velocity Kolkata Customer 0 Strategy - Get My Ghar + +Document version: 1.0 +Prepared for: Founder sales, product architecture, GTM execution +Document date: 2026-04-10 +Primary audience: Sagnik, Sayan, Abantika + +## 1. Executive Summary + +Rohit Darolia and Get My Ghar should be treated as `Customer 0` for the Tier 3 city-channel deployment of Velocity Suite in Kolkata. This is not just a sales opportunity. This is the founding field deployment that lets Velocity prove three things at once: + +- the product can operate as a city-level sales operating system for a top channel partner +- the same deployment can showcase multiple builder inventories without diluting data privacy +- the CP deployment becomes the wedge into Tier 2 and Tier 1 builder relationships across India + +The strategic rule is simple: + +- Kolkata Tier 3 should be exclusive to Get My Ghar during the pilot and early rollout window +- builders in Kolkata and outside Kolkata should see Velocity first through live proof, not a pitch deck +- the demo must show property-level intelligence today and portfolio-level logic as the obvious next paid unlock + +The business objective is not only to close Get My Ghar. The objective is to use one high-trust CP deployment to unlock builder conversations with groups such as Ambuja Neotia, Godrej, Siddha, Merlin, Eden, Sugam, DTC, Srijan, and others. + +## 2. Business Context + +### 2.1 Commercial Thesis + +Velocity is not being positioned as SaaS. It is being positioned as a private AI sales operating system that gives builders and channel partners a competitive edge while keeping data under their control. + +Revenue model: + +- Initial setup fee +- Monthly maintenance and innovation fee +- Inventory-linked performance fee + +Commercial logic: + +- every property is a high-value commercial unit +- every property deserves a serious setup because the sales cycle and inventory value justify it +- second and later properties unlock portfolio-level intelligence rather than requiring a separate codebase + +### 2.2 Role of Get My Ghar + +Get My Ghar is strategically useful for five reasons: + +- high-trust founder access through Rohit +- live inventory across multiple builders +- enough operational complexity to prove the product is real +- direct builder relationships that compress outbound sales effort +- Kolkata exclusivity can motivate partnership behavior instead of passive curiosity + +### 2.3 Why Kolkata Matters First + +Kolkata is the ideal proving ground because: + +- founder access is stronger +- Rohit can become an active design partner +- success in one city creates a referenceable operating model +- the same pattern can later be repeated city by city in Bangalore, Gurgaon, Pune, Mumbai, then Dubai and Abu Dhabi + +## 3. Product Positioning + +### 3.1 What Velocity Is + +Velocity is a private sales operating system for premium real estate inventory. It combines: + +- CRM operating layer +- AI reasoning layer +- inventory intelligence +- media and generation layer +- iPad-assisted project presentation flows +- monitoring and performance layer + +### 3.2 What Velocity Is Not + +Velocity is not: + +- just a CRM +- just a lead-management tool +- just a ComfyUI demo +- just a dashboard +- generic builder software + +The product promise is: + +- faster movement of inventory +- shorter sales cycle +- sharper broker and representative decision-making +- better presentation quality +- better follow-up precision +- private ownership of customer and inventory data + +### 3.3 Packaging Logic + +Velocity should be explained as one platform with scope-based unlocks: + +- `Property Layer` +- `Portfolio Layer` +- `Enterprise Control Layer` + +For the Get My Ghar demo, the visible focus should be: + +- property-linked intelligence +- city-channel workflows +- cross-builder inventory intelligence within Kolkata + +For builders, the visible focus should shift to: + +- project-specific deployment +- property-specific generation +- multi-property portfolio monitoring after the second project + +## 4. Customer 0 Architecture + +### 4.1 Core Model + +For Get My Ghar, Velocity should be framed as a `Kolkata City Deployment`. + +This deployment is: + +- one private city operating environment +- one city-specific sales intelligence layer +- one deployment containing multiple builder properties in Kolkata +- one team operating surface for CP workflows + +This is not a company-wide pan-India deployment on day one. It is a Kolkata operating system. + +### 4.2 Functional Layers for the Demo + +#### A. Monitoring Layer + +Shows: + +- lead inflow by property +- source quality +- pipeline health +- broker activity +- follow-up gaps +- inventory movement signals +- QD-weighted lead prioritization + +#### B. Interaction Layer + +Shows: + +- lead cards and enrichment +- follow-up and assignment workflows +- property comparison views +- client interaction memory +- page-level or project-level insights +- chat-to-action operational flows + +#### C. Generation Layer + +Shows: + +- property-linked AI visual generation +- iPad-friendly presentation assets +- project-specific walkthrough content +- agent-facing creative variation workflows + +Important principle: + +- generation should remain property-linked +- monitoring can aggregate across properties +- portfolio intelligence can be explained as a future unlock rather than the initial visible center + +### 4.3 Deployment Architecture for Customer 0 + +Recommended architecture for the demo and first paid deployment: + +```text +Get My Ghar Kolkata Deployment + -> Private app environment + -> PostgreSQL + -> object/model storage + -> Velocity web app + -> iPad presentation surfaces + -> AI reasoning service + -> media/generation worker + -> monitoring dashboards + -> project/property data namespaces +``` + +Operationally: + +- one deployment +- many Kolkata properties +- property-level isolation in the data model +- city-level aggregate dashboards for Rohit and management + +### 4.4 Data Model View + +For Get My Ghar, the key top-level entities are: + +- City +- Builder +- Property +- Unit / inventory type +- Lead +- Broker / sales representative +- Interaction +- Presentation asset +- Visit / response signal +- Sales stage + +The hierarchy should be: + +```text +Kolkata + -> Builder + -> Property + -> Inventory + -> Leads + -> Interactions + -> Media / generation assets +``` + +This lets the same city deployment prove: + +- property-specific workflows +- builder-specific views +- city-level channel-partner intelligence + +## 5. Demo Property Map + +The following properties should be used as the first Kolkata showcase inventory set: + +| # | Property | Builder | URL | Starting Price | +| --- | --- | --- | --- | --- | +| 1 | Eden Devprayag | Eden Realty | `https://getmyghar.com/properties/eden-devprayag/` | `₹ 1.50 Cr Onwards` | +| 2 | Sugam Prakriti | Sugam Homes | `https://getmyghar.com/properties/sugam-prakriti/` | `₹ 35 Lakhs Onwards` | +| 3 | Atri Aqua | Atri Group | `https://getmyghar.com/properties/atri-aqua/` | `₹ 32.50 Lakhs Onwards` | +| 4 | Atri Surya Toron | Atri Group | `https://getmyghar.com/properties/atri-surya-toron/` | `₹ 23 Lakhs Onwards` | +| 5 | Siddha Suburbia Bungalow | Siddha Group | `https://getmyghar.com/properties/siddha-suburbia-bungalow/` | `₹ 96 Lacs Onwards` | +| 6 | Merlin Avana | Merlin Group | `https://getmyghar.com/properties/merlin-avana/` | `₹ 78 Lakhs Onwards` | +| 7 | DTC Good Earth | DTC Group | `https://getmyghar.com/properties/dtc-good-earth/` | `₹ 42 Lakhs Onwards` | +| 8 | Siddha Serena | Siddha Group | `https://getmyghar.com/properties/siddha-serena/` | `₹ 69 Lakhs Onwards` | +| 9 | Siddha Sky Waterfront | Siddha Group | `https://getmyghar.com/properties/siddha-sky/` | `₹ 1.40 Cr Onwards` | +| 10 | Godrej Blue | Godrej Properties Ltd | `https://getmyghar.com/properties/godrej-blue/` | `₹ 2.57 Cr Onwards` | +| 11 | DTC Sojon | DTC Group | `https://getmyghar.com/properties/dtc-sojon/` | `₹ 48 Lakhs Onwards` | +| 12 | Shriram Grand City | Shriram Properties Limited | `https://getmyghar.com/properties/shriram-grand-city/` | `₹ 22 Lakhs Onwards` | +| 13 | Godrej Elevate | Godrej Properties Ltd | `https://getmyghar.com/properties/godrej-elevate/` | `₹ 56 Lacs Onwards` | +| 14 | Ambuja Utpaala | Ambuja Neotia Group | `https://getmyghar.com/properties/ambuja-utpalaa/` | `₹ 2.17 Cr Onwards` | +| 15 | Srijan Group Properties | Srijan Group | `TBD per property selection` | `TBD` | + +### 5.1 Why This Property Mix Works + +This list is commercially useful because it spans: + +- affordable to premium price bands +- multiple major builder brands +- repeat builder presence, which helps show cross-property insight +- obvious buyer segmentation possibilities +- obvious premium property storytelling use cases + +This means the demo can show: + +- low-ticket, high-volume movement logic +- mid-market comparison logic +- premium property white-glove selling logic + +## 6. Demo Narrative for Rohit + +The demo must not feel like software browsing. It must feel like Rohit is seeing his future operating advantage. + +### 6.1 Narrative Arc + +#### Part 1: Control + +Show Rohit that Kolkata inventory can be monitored from one surface: + +- which projects are active +- which representatives are following up properly +- which inventory is getting traction +- where interest is stalling + +#### Part 2: Precision + +Show that Velocity helps decide: + +- which lead deserves immediate attention +- which property should be pushed for a given buyer profile +- where the broker team is leaking momentum +- how presentations can be tailored faster + +#### Part 3: Advantage + +Show that Get My Ghar can become the smartest CP operation in Kolkata: + +- faster inventory movement +- better project pitching +- stronger builder reporting +- stronger broker accountability +- more premium operational image + +#### Part 4: Expansion + +Show that this same deployment pattern becomes: + +- the Kolkata CP operating system today +- the proof point used to win builders tomorrow + +## 7. Selling Strategy + +### 7.1 Strategic Goal + +Close Get My Ghar as the founding Kolkata Tier 3 partner while using the deployment as the live demonstration environment for Tier 2 and Tier 1 builder sales. + +### 7.2 Positioning for Rohit + +Pitch to Rohit as: + +- exclusive Kolkata CP partner during the pilot period +- early strategic design partner +- operator with first access to AI-enhanced city inventory workflows +- trusted launch ally, not just a paying customer + +What he gets emotionally: + +- status +- exclusivity +- first-mover advantage +- influence on product shape + +What he gets commercially: + +- sharper conversion machinery +- better builder reporting +- a differentiated operating layer that smaller CPs cannot match + +### 7.3 Positioning for Builders + +Pitch to builders as: + +- the system already trusted by a top CP in Kolkata +- a private, anti-SaaS deployment model +- project-specific or portfolio-specific deployment +- a system that reduces time-to-sale, improves presentation quality, and creates better operational discipline + +For builders, the main proof is not your code. The main proof is: + +- a live CP deployment operating on real properties +- a known operator they trust +- a founder-led product with industry insight and technical depth + +### 7.4 Recommended Founder Sales Sequence + +1. Close Rohit as Customer 0. +2. Use his live Kolkata deployment as proof. +3. Use Rohit's introductions to open Tier 2 and Tier 1 conversations. +4. Lead with live use cases, not feature lists. +5. Sell one-month beta setup as a founder-stage privileged engagement. +6. Use signed setup fees to buy time and runway for Sayan and Abantika. + +## 8. Commercial Strategy + +### 8.1 Get My Ghar Commercial Frame + +Since this is a founding deployment, the pitch should be: + +- one-month setup window +- founder-led implementation +- priority roadmap influence +- Kolkata exclusivity on the Tier 3 city-channel side for the agreed early term + +Possible structure: + +- discounted or specially structured setup fee relative to future city deployments +- formal monthly maintenance agreement +- clear success metrics tied to operational usage and inventory movement + +### 8.2 Builder Commercial Frame + +Builders should be sold on: + +- one property as the initial operating unit +- serious setup fee because every premium property justifies it +- second property unlocks portfolio visibility and cross-property intelligence +- monthly maintenance keeps them current +- inventory-linked fee aligns your upside with their sales velocity + +### 8.3 Why This Is Profitable + +This model is profitable because: + +- every deployment is high-value +- support remains standardized if the architecture stays modular +- second-property expansion is easier than first deployment +- references compound +- one CP deployment can source multiple builder deals + +## 9. Competitive Strategy + +### 9.1 Strategic Moat + +Your moat is not one model or one workflow. It is the combination of: + +- real-estate operating understanding +- anti-SaaS private deployment stance +- AI reasoning plus sales workflow integration +- generation and presentation layer linked to actual inventory +- founder-led speed in a market where incumbents move slowly + +### 9.2 Why Others Will Struggle + +Most competitors will fall into one of these traps: + +- generic CRM with no project intelligence +- marketing demo with no operational depth +- AI toy with no deployment discipline +- enterprise software with no founder velocity + +Velocity wins if it becomes the real operational layer, not just a shiny demo. + +## 10. Founding Team Execution Logic + +This GTM motion is also your hiring and runway bridge. + +Desired outcome: + +- Get My Ghar signs +- setup fee lands +- monthly maintenance starts +- founder confidence increases +- that lets you pull Sayan out of TCS +- that lets Abantika operate more fully in the venture + +This is not just sales revenue. This is team-conversion capital. + +## 11. Risks and Controls + +### 11.1 Risks + +- demo looks impressive but not operationally credible +- too much is promised before install discipline exists +- exclusivity is given without enough commercial commitment +- founders get dragged into excessive custom work +- builders interpret beta status as product weakness rather than founder access + +### 11.2 Controls + +- keep the demo grounded in real operational flows +- define exactly what is included in the one-month setup +- keep architecture modular +- avoid bespoke one-off branches per client +- position beta as founder-led privileged access, not instability + +## 12. Immediate Action Plan + +### 12.1 Demo Build + +Build the demo around these views: + +- Kolkata inventory overview +- builder-by-builder property map +- price-band segmentation +- property-specific lead prioritization +- representative performance +- follow-up leakage +- premium property AI presentation layer +- iPad property pitch experience + +### 12.2 Sales Material + +Prepare: + +- founder narrative deck +- one-page commercial sheet +- one-page architecture sheet +- one-page privacy and anti-SaaS argument +- one-page rollout timeline + +### 12.3 Meeting Goal + +The objective of the Rohit meeting is: + +- not “what do you think?” +- but “let’s make this your Kolkata operating edge and launch it together” + +## 13. Recommended Founder Script + +Use this logic in conversation: + +"We are not building software for everyone and then trying to fit you into it. We are building the sales operating system that should exist for premium real-estate movement. Kolkata is the first city where we want to prove it properly. You are one of the few people who can make that proof real at city scale. We want Get My Ghar to become the founding Kolkata partner, and once that engine is live, we use that proof to walk into the builders together." + +## 14. Final Recommendation + +Treat Rohit and Get My Ghar as: + +- founding Kolkata partner +- city-channel Customer 0 +- proof engine for builder acquisition +- reference deployment for the anti-SaaS sales thesis + +The right next move is not to overbuild. It is to build the smallest high-conviction live operating demo that proves: + +- city-level CP control +- project-level intelligence +- premium-property sales enhancement +- clear path from CP deployment to builder deployment + +If this works, Kolkata becomes the template. If Kolkata becomes the template, the rest of India becomes a rollout problem, not a product-definition problem. diff --git a/.Agent Context/Sprint 1/nemoclaw_setup_truth.md b/.Agent Context/Sprint 1/nemoclaw_setup_truth.md new file mode 100644 index 00000000..20a1b4a7 --- /dev/null +++ b/.Agent Context/Sprint 1/nemoclaw_setup_truth.md @@ -0,0 +1,327 @@ +# NemoClaw Setup Truth + +Updated: April 2, 2026 + +## 1. Purpose + +This document records the actual NemoClaw-related deployment state for Project Velocity. It explains what exists, where it exists, why it exists, which ports are involved, and how the reasoning path works today. + +This is not the original intended architecture. This is the current operational truth. + +## 2. High-Level Summary + +Project Velocity uses the term "NemoClaw" for the reasoning and prompt layer attached to the Sentinel QD Engine. In practice, this is now split into two different concerns: + +1. Prompted reasoning used by the FastAPI backend +2. OpenShell / gateway infrastructure that remains installed on the AWS node + +The active FastAPI inference path is NVIDIA-hosted OpenAI-compatible chat completions. + +The OpenShell gateway and Ollama are still installed and running as adjacent infrastructure, but they are not the active primary scoring path used by `backend/services/nemoclaw_client.py`. + +## 3. Node and Network Truth + +AWS region: `us-east-1` +Current public IP: `54.152.236.10` +SSH user: `ubuntu` + +### Port Map + +`22` +SSH access to the AWS node. + +`443` +nginx TLS reverse proxy. Public entry point for the backend. + +`127.0.0.1:8001` +FastAPI/Uvicorn backend. Not directly public. + +`127.0.0.1:5432` +PostgreSQL. Local-only. + +`8080` +OpenShell/NemoClaw gateway target. Internal service path for gateway bootstrap and sandbox-related flows. + +`11434` +Local Ollama runtime. Installed and reachable on the node, but not the current primary backend scoring path. + +`/api/videos/marketing` +Backend catalog endpoint for Sentinel live-session marketing videos. + +## 4. File and Directory Layout + +### NVMe-backed runtime directories + +`/opt/dlami/nvme/velocity/current` +Active backend code. + +`/opt/dlami/nvme/velocity/env` +Environment file used by `velocity-backend.service`. + +`/opt/dlami/nvme/velocity/venv` +Python virtual environment for the backend. + +`/opt/dlami/nvme/velocity/tls` +TLS cert and key used by nginx. + +`/opt/dlami/nvme/nemoclaw/prompts` +Prompt files used by the backend reasoning client. + +`/opt/dlami/nvme/assets/videos` +Runtime marketing-video directory served by FastAPI static assets. + +`/opt/dlami/nvme/assets/videos/catalog.json` +Optional checked catalog that controls video ordering, labels, and display metadata for the live-session picker. + +`/opt/dlami/nvme/pgdata/14/velocity` +PostgreSQL 14 data directory. + +### Repo paths + +`backend/services/nemoclaw_client.py` +Primary reasoning client used by the FastAPI backend. + +`backend/routers/videos.py` +Marketing-video catalog endpoint for the Sentinel live-session picker. + +`backend/config/marketing_videos.catalog.json` +Checked source catalog for the four current property walkthrough videos. + +`backend/nemoclaw_prompts/qd_calculator.md` +QD scoring prompt. + +`backend/nemoclaw_prompts/lead_tagger.md` +Lead enrichment prompt. + +`backend/nemoclaw_prompts/cctv_profiler.md` +CCTV vehicle and plate profiling prompt. + +`backend/scripts/nemoclaw_deploy.sh` +Historical deployment/bootstrap script for OpenShell/Ollama-style setup. Useful as reference, but no longer fully aligned with the active NVIDIA-primary truth. + +## 5. Services + +### `velocity-backend.service` + +Purpose: +Runs FastAPI/Uvicorn from the NVMe release tree. + +Why it exists: +Provides the production API and WebSocket layer for Sentinel, Vault, Scenes, CCTV, and Auth. + +Key behavior: +- Reads `/opt/dlami/nvme/velocity/env` +- Starts `uvicorn backend.main:app --host 127.0.0.1 --port 8001` + +### `nemoclaw-velocity.service` + +Purpose: +Bootstraps the OpenShell/NemoClaw gateway state. + +Why it exists: +Keeps the local gateway selection and related tooling available on the node even though FastAPI currently scores against NVIDIA directly. + +Current truth: +- Implemented as a non-blocking `oneshot` systemd unit +- Leaves the service in `active (exited)` when successful + +### `nginx` + +Purpose: +TLS reverse proxy for the backend. + +Why it exists: +Exposes the backend on `443`, terminates TLS, and forwards both HTTP and WebSocket traffic to Uvicorn. + +### `postgresql@14-velocity.service` + +Purpose: +Owns the NVMe-backed PostgreSQL cluster. + +Why it exists: +The Sentinel and Vault flows persist state in PostgreSQL, not Supabase. + +## 6. Environment Variables + +Active variables relevant to NemoClaw reasoning: + +`NVIDIA_API_KEY` +Used by the backend to authenticate against NVIDIA hosted completions. + +`NVIDIA_BASE_URL` +Set to `https://integrate.api.nvidia.com/v1` + +`NVIDIA_MODEL` +Set to `nvidia/nemotron-3-super-120b-a12b` + +`NVIDIA_FALLBACK_MODEL` +Set to `nvidia/llama-3.3-nemotron-super-49b-v1` + +`ALLOW_LOCAL_FALLBACK` +Currently `false` + +`NEMOCLAW_PROMPT_DIR` +Set to `/opt/dlami/nvme/nemoclaw/prompts` + +Historical-but-not-primary variables: + +`OLLAMA_BASE_URL` +Still relevant if local fallback is re-enabled. + +`NEMOCLAW_BASE_URL` +No longer the primary path for backend scoring. + +## 7. Inference Flow + +### Current backend inference flow + +1. Frontend emits biometric packet over `/api/sentinel/ws/perception` +2. `backend/routers/sentinel.py` receives the packet +3. Scene context is resolved from `video_scene_maps` if `video_asset_id` and `video_ts_ms` are present +4. `backend/services/nemoclaw_client.py` builds an OpenAI-compatible messages payload +5. The backend calls NVIDIA hosted completions using `nvidia/nemotron-3-super-120b-a12b` +6. The result updates QD score state and is broadcast back over WebSocket + +### Current lead-tagging flow + +1. Broker or system calls `/api/sentinel/tag-lead` +2. `tag_lead()` uses the NVIDIA path +3. Lead tags are updated in `leads_intelligence` +4. `LEAD_TAGGED` is broadcast to notifications + +### Current CCTV flow + +1. OCR/bridge posts to `/api/cctv/event` +2. `profile_cctv_visitor()` uses the NVIDIA path +3. `cctv_events` row is written +4. Session evidence is updated +5. Session can later be finalized through auto-mode matching + +### Current live-session video flow + +1. Frontend calls `GET /api/videos/marketing` +2. Backend reads `/opt/dlami/nvme/assets/videos/catalog.json` if present +3. Backend falls back to scanning `/opt/dlami/nvme/assets/videos` recursively for playable files if the catalog is missing or incomplete +4. FastAPI serves the MP4 files through `/assets/videos/...` +5. `SentinelLiveSession.tsx` renders smaller preview cards that autoplay in 3-second bursts on hover and advance 10 seconds between bursts +6. `PerceptionPlayer.tsx` plays the selected asset through the same `/assets/videos/...` path + +## 8. OpenShell and Ollama Truth + +OpenShell and Ollama still matter, but in a narrower way than originally planned. + +### Ollama + +Location: +Runs locally on port `11434` + +Why it still exists: +- Historical deployment compatibility +- Potential local fallback if NVIDIA is disabled +- OpenShell-related infrastructure expectations + +### OpenShell gateway + +Location: +Gateway target on port `8080` + +Why it still exists: +- NemoClaw sandbox bootstrap +- Local gateway control path +- Operational continuity for the previously onboarded sandbox + +What it is not: +- It is not the current primary inference path for backend scoring + +## 9. Prompts + +Prompt source-of-truth in repo: + +- `backend/nemoclaw_prompts/qd_calculator.md` +- `backend/nemoclaw_prompts/lead_tagger.md` +- `backend/nemoclaw_prompts/cctv_profiler.md` + +Prompt runtime location on node: + +- `/opt/dlami/nvme/nemoclaw/prompts/qd_calculator.md` +- `/opt/dlami/nvme/nemoclaw/prompts/lead_tagger.md` +- `/opt/dlami/nvme/nemoclaw/prompts/cctv_profiler.md` + +Why copied to NVMe: +- Keeps runtime prompts off the root volume +- Aligns with the NVMe-first deployment strategy +- Prevents storage-eviction regressions + +## 10. Known Operational Risks + +### JSON compliance risk + +The NVIDIA model sometimes returns malformed or partially malformed JSON for the full QD prompt. The backend now includes partial-response recovery, but this is the biggest remaining correctness risk. + +### Dynamic IP risk + +The public IP has changed during execution. A stable Elastic IP or DNS entry is still recommended. + +### Trust-chain risk + +nginx TLS exists, but a production-trusted certificate should replace self-signed cert material. + +### External producer gap + +The OCR bridge script exists, but a production ONVIF/RTSP/OCR producer still needs to be pointed at the ingestion endpoint. + +### Catalog drift risk + +If new property videos are copied to NVMe without updating `catalog.json`, they will still be discoverable through directory scanning, but order, title, and display color may drift from the intended broker-facing presentation. + +## 11. Validation Commands + +Health: + +```bash +curl -k https://54.152.236.10/health +curl -k https://54.152.236.10/api/videos/marketing +``` + +Backend service: + +```bash +sudo systemctl status velocity-backend.service +``` + +Gateway bootstrap: + +```bash +sudo systemctl status nemoclaw-velocity.service +``` + +PostgreSQL: + +```bash +sudo systemctl status postgresql@14-velocity.service +sudo -u postgres psql -d velocity -c '\dt' +``` + +Local inference health from backend env: + +```bash +source /opt/dlami/nvme/velocity/env +PYTHONPATH=/opt/dlami/nvme/velocity/current /opt/dlami/nvme/velocity/venv/bin/python - <<'PY' +import asyncio, json +from backend.services.nemoclaw_client import health_check +print(asyncio.run(health_check())) +PY +``` + +## 12. What to Update If the Truth Changes + +Update this document whenever any of the following change: + +- Public IP or DNS target +- Primary inference provider +- Primary model +- Prompt directory +- nginx port or TLS behavior +- OpenShell gateway port +- service unit names +- NVMe runtime paths diff --git a/.Agent Context/Sprint 1/sentinel_execution_tracker_2026-04-02.md b/.Agent Context/Sprint 1/sentinel_execution_tracker_2026-04-02.md new file mode 100644 index 00000000..a64526b8 --- /dev/null +++ b/.Agent Context/Sprint 1/sentinel_execution_tracker_2026-04-02.md @@ -0,0 +1,27 @@ +# Sentinel QD Engine Execution Tracker + +Updated: April 2, 2026 + +| Milestone | Scope | Current Status | Completed Truth | What Is Left | +|---|---|---|---|---| +| 1 | Database & Auth | Complete | Types, store slice, schema, addendum schema, asyncpg pool, RBAC dependencies, FastAPI entrypoint, NVMe PostgreSQL deployment, and schema application are done. | Nothing required to satisfy the original milestone. | +| 2 | Velocity Link & Notifications | Complete | Vault link generation, vault-open logging, notification socket hook, notification center UI, and App wiring are implemented. | A live broker acceptance pass on the deployed frontend is still useful, but the milestone itself is complete. | +| 3 | Perception Pipeline | Complete | Landmark encoder, MediaPipe hook, PerceptionPlayer, Sentinel live session wizard, and Sentinel subtab split are implemented. | No code gap remains for the milestone definition. | +| 4 | NemoClaw Sandbox | Complete with architecture change | Prompt files, Sentinel router, backend reasoning client, AWS gateway bootstrap, and active env configuration are in place. The live reasoning path is now NVIDIA-primary instead of local Ollama-primary. | The only remaining improvement is better structured-output robustness from the NVIDIA model. | +| 5 | CCTV & Auto Mode Integration | Code complete, external producer hookup pending | `backend/routers/cctv.py`, `backend/scripts/cctv_ocr_bridge.py`, `backend/services/auto_mode_matcher.py`, and prompt placement to NVMe are done. | A real ONVIF/RTSP/OCR producer still needs to post to `/api/cctv/event` in production. | +| 6 | Video Scene CSV Upload | Complete | `backend/routers/scenes.py`, scene-aware backend lookup, frontend scene CSV upload UI, and scene-linked session packets are implemented. | Optional broker training and a production CSV content pass remain operational tasks, not engineering blockers. | +| 7 | AWS Deployment | Complete | NVMe-backed backend tree, env file, PostgreSQL on NVMe, schema execution, `velocity-backend.service`, `nemoclaw-velocity.service`, nginx TLS proxy, and backend health recovery on the current node IP are complete. | Replace dynamic IP and self-signed TLS with stable production networking and certificate management. | +| 8 | Integration Tests | Complete at repo level | Backend NemoClaw scoring test, backend vault notification flow test, Playwright live-session tests, and Playwright notification-center test are implemented and passing locally. | A live deployed end-to-end acceptance run through the broker-facing UI is still recommended before declaring production signoff. | + +## Residual Risks + +1. NVIDIA structured output remains imperfect on the full QD prompt. The client now includes malformed-response recovery, but this is still the main inference correctness risk. +2. The AWS public IP is dynamic. The system should move to an Elastic IP or a stable DNS record. +3. CCTV ingestion is implemented at the API boundary, but the production camera/OCR producer is not yet hard-wired. +4. TLS is functional, but the deployment still needs a trusted certificate chain for full production readiness. + +## April 2 Addendum + +1. The live-session video picker now loads catalog-backed marketing videos from `GET /api/videos/marketing` instead of hardcoded mock cards. +2. The assigned-mode lead selection step now includes client-side search. +3. The current four walkthrough assets are intended to live under `/opt/dlami/nvme/assets/videos` with broker-facing metadata in `catalog.json`. diff --git a/.Agent Context/Sprint 1/velocity_status_report.md b/.Agent Context/Sprint 1/velocity_status_report.md new file mode 100644 index 00000000..89424026 --- /dev/null +++ b/.Agent Context/Sprint 1/velocity_status_report.md @@ -0,0 +1,71 @@ +# Project Velocity: Sentinel QD Engine Status Report +*Generated: April 1, 2026* + +This artifact provides a comprehensive overview of the current status of the **Sentinel Quantum Dynamics (QD) Engine**. It maps the architectural decisions and milestones directly to the codebase, detailing what has been completed, deployed, and what remains to be done. + +--- + +## 1. Architectural Adjustments & Realities +During the NemoClaw deployment (Milestone 4 & 7), several key architectural realities emerged that forced an adjustment to the original design: + +1. **Inference Pipeline Switch (NVIDIA API vs. Ollama)** + - **Original Plan**: Route traffic through the OpenShell mTLS gateway locally holding `qwen3.5:27b`. + - **The Reality**: The `qwen3.5:27b` model under Ollama currently runs in an extended "think mode" (chain-of-thought) which exhausts token limits before outputting JSON, causing timeouts. The OpenShell Gateway also expects internal sandbox client certificates (mTLS). + - **The Pivot**: We have updated `nemoclaw_client.py` to use the **NVIDIA API directly** (`llama-3.3-nemotron-super-49b-v1`) as the primary inference engine since it is OpenAI-compatible, fast, and reliable. Ollama remains a fallback (with a known TODO to fix the `think` parameter parsing). + +2. **Storage Eviction Issues Resolved** + - **The Reality**: k3s and Docker were exhausting the root `/dev/root` volume, triggering disk pressure evictions and crashing OpenShell pods. + - **The Pivot**: We migrated the 103GB model cache and the `/var/lib/rancher` k3s storage to the 3.4TB `/opt/dlami/nvme` partition, restoring disk health and allowing NemoClaw to successfully "onboard" and enter a "Ready" state. + +3. **Port Collisions** + - **The Reality**: Both the existing `velocity-oracle` and the OpenShell gateway wanted port `8080`. + - **The Pivot**: We migrated `velocity-oracle` to bind to port **`8081`**. + +--- + +## 2. Codebase Map: What is Done + +### ✅ **Milestone 1: Database & Auth** +- **Types:** `app/src/types/index.ts` (BiometricPacket, QDScore, Notifications) +- **State:** `app/src/store/useStore.ts` (Notification state management) +- **Schema:** `backend/db/schema.sql`, `backend/db/schema_addendum.sql` +- **Pool Data:** `backend/db/pool.py`, `backend/auth/dependencies.py` +- **Entrypoint:** `backend/main.py` (FastAPI lifecycle and router integration) + +### ✅ **Milestone 2: Velocity Link & Notifications** +- **Routers:** `backend/routers/vault.py` +- **Hooks:** `app/src/hooks/useVelocitySocket.ts` +- **UI:** `app/src/components/layout/NotificationCenter.tsx` + `App.tsx` wiring. + +### ✅ **Milestone 3: Perception Pipeline (Frontend)** +- **WASM Encoders:** `app/src/utils/landmarkPacketEncoder.ts` +- **Vision Feed:** `app/src/hooks/useMediapipeFaceLandmarker.ts` +- **Live UI:** `app/src/components/modules/sentinel/PerceptionPlayer.tsx` + `SentinelLiveSession.tsx` + +### ✅ **Milestone 4 & 7: NemoClaw Sandbox & AWS Env** +- **Client Logic:** `backend/services/nemoclaw_client.py` (Now pointed to NVIDIA API) +- **Prompts:** Uploaded to NVMe (`cctv_profiler.md`, `lead_tagger.md`, `qd_calculator.md`) +- **System Service:** `nemoclaw-velocity.service` created and enabled. +- **Environment:** `/opt/dlami/nvme/velocity/env` written. +- **NemoClaw Onboarding:** Succeeded; the sandbox is completely ready (`Phase: Ready`). + +--- + +## 3. Pending Workflow: What is Left + +### ⏳ **Database Initialization (Finalizing Milestone 1/7)** +While the backend code and schema files exist, the AWS database instance itself has not been booted. +- **Action Required:** Instantiate PostgreSQL on `/opt/dlami/nvme/pgdata` and execute `schema.sql` and `schema_addendum.sql`. +- **Action Required:** Point FastAPI to the PostgreSQL instance. + +### ⏳ **Milestone 5: CCTV & Auto Mode Integration** +- **Action Required:** Build `backend/routers/cctv.py` to ingest frames. +- **Action Required:** Build OCR bridging logic to pass plates and cars to the NemoClaw prompt (`cctv_profiler.md`) and into the DB. +- **Action Required:** Build `backend/services/auto_mode_matcher.py` (matching CCTV feed data to post-session lead attribution). + +### ⏳ **Milestone 6: Video Scene CSV Upload** +- **Action Required:** Implement `backend/routers/scenes.py` (/api/scenes/upload endpoint). +- **Action Required:** Integrate the scene context into the frontend `PerceptionPlayer` to correlate timestamped video annotations with real-time biometric feeds. + +### ⏳ **Milestone 8: End-to-End Integration Testing** +- **Action Required:** E2E testing of the full pipeline (Frontend → FastAPI → PostgreSQL/NemoClaw → Frontend Notifications). diff --git a/.Agent Context/Tech/MediaPipe-1906.08172v1.pdf b/.Agent Context/Tech/MediaPipe-1906.08172v1.pdf new file mode 100644 index 00000000..02aefa46 Binary files /dev/null and b/.Agent Context/Tech/MediaPipe-1906.08172v1.pdf differ diff --git a/.Agent Context/Tech/NewTitle_May1_MediaPipe_CVPR_CV4ARVR_Workshop_2019.pdf b/.Agent Context/Tech/NewTitle_May1_MediaPipe_CVPR_CV4ARVR_Workshop_2019.pdf new file mode 100644 index 00000000..e907baa0 Binary files /dev/null and b/.Agent Context/Tech/NewTitle_May1_MediaPipe_CVPR_CV4ARVR_Workshop_2019.pdf differ diff --git a/.Agent Context/Tech/README-MediaPipe.md b/.Agent Context/Tech/README-MediaPipe.md new file mode 100644 index 00000000..e4f5dd18 --- /dev/null +++ b/.Agent Context/Tech/README-MediaPipe.md @@ -0,0 +1,158 @@ +--- +layout: forward +target: https://developers.google.com/mediapipe +title: Home +nav_order: 1 +--- + +---- + +**Attention:** *We have moved to +[https://developers.google.com/mediapipe](https://developers.google.com/mediapipe) +as the primary developer documentation site for MediaPipe as of April 3, 2023.* + +![MediaPipe](https://developers.google.com/static/mediapipe/images/home/hero_01_1920.png) + +**Attention**: MediaPipe Solutions Preview is an early release. [Learn +more](https://developers.google.com/mediapipe/solutions/about#notice). + +**On-device machine learning for everyone** + +Delight your customers with innovative machine learning features. MediaPipe +contains everything that you need to customize and deploy to mobile (Android, +iOS), web, desktop, edge devices, and IoT, effortlessly. + +* [See demos](https://goo.gle/mediapipe-studio) +* [Learn more](https://developers.google.com/mediapipe/solutions) + +## Get started + +You can get started with MediaPipe Solutions by by checking out any of the +developer guides for +[vision](https://developers.google.com/mediapipe/solutions/vision/object_detector), +[text](https://developers.google.com/mediapipe/solutions/text/text_classifier), +and +[audio](https://developers.google.com/mediapipe/solutions/audio/audio_classifier) +tasks. If you need help setting up a development environment for use with +MediaPipe Tasks, check out the setup guides for +[Android](https://developers.google.com/mediapipe/solutions/setup_android), [web +apps](https://developers.google.com/mediapipe/solutions/setup_web), and +[Python](https://developers.google.com/mediapipe/solutions/setup_python). + +## Solutions + +MediaPipe Solutions provides a suite of libraries and tools for you to quickly +apply artificial intelligence (AI) and machine learning (ML) techniques in your +applications. You can plug these solutions into your applications immediately, +customize them to your needs, and use them across multiple development +platforms. MediaPipe Solutions is part of the MediaPipe [open source +project](https://github.com/google/mediapipe), so you can further customize the +solutions code to meet your application needs. + +These libraries and resources provide the core functionality for each MediaPipe +Solution: + +* **MediaPipe Tasks**: Cross-platform APIs and libraries for deploying + solutions. [Learn + more](https://developers.google.com/mediapipe/solutions/tasks). +* **MediaPipe models**: Pre-trained, ready-to-run models for use with each + solution. + +These tools let you customize and evaluate solutions: + +* **MediaPipe Model Maker**: Customize models for solutions with your data. + [Learn more](https://developers.google.com/mediapipe/solutions/model_maker). +* **MediaPipe Studio**: Visualize, evaluate, and benchmark solutions in your + browser. [Learn + more](https://developers.google.com/mediapipe/solutions/studio). + +### Legacy solutions + +We have ended support for [these MediaPipe Legacy Solutions](https://developers.google.com/mediapipe/solutions/guide#legacy) +as of March 1, 2023. All other MediaPipe Legacy Solutions will be upgraded to +a new MediaPipe Solution. See the [Solutions guide](https://developers.google.com/mediapipe/solutions/guide#legacy) +for details. The [code repository](https://github.com/google/mediapipe/tree/master/mediapipe) +and prebuilt binaries for all MediaPipe Legacy Solutions will continue to be +provided on an as-is basis. + +For more on the legacy solutions, see the [documentation](https://github.com/google/mediapipe/tree/master/docs/solutions). + +## Framework + +To start using MediaPipe Framework, [install MediaPipe +Framework](https://developers.google.com/mediapipe/framework/getting_started/install) +and start building example applications in C++, Android, and iOS. + +[MediaPipe Framework](https://developers.google.com/mediapipe/framework) is the +low-level component used to build efficient on-device machine learning +pipelines, similar to the premade MediaPipe Solutions. + +Before using MediaPipe Framework, familiarize yourself with the following key +[Framework +concepts](https://developers.google.com/mediapipe/framework/framework_concepts/overview.md): + +* [Packets](https://developers.google.com/mediapipe/framework/framework_concepts/packets.md) +* [Graphs](https://developers.google.com/mediapipe/framework/framework_concepts/graphs.md) +* [Calculators](https://developers.google.com/mediapipe/framework/framework_concepts/calculators.md) + +## Community + +* [Slack community](https://mediapipe.page.link/joinslack) for MediaPipe + users. +* [Discuss](https://groups.google.com/forum/#!forum/mediapipe) - General + community discussion around MediaPipe. +* [Awesome MediaPipe](https://mediapipe.page.link/awesome-mediapipe) - A + curated list of awesome MediaPipe related frameworks, libraries and + software. + +## Contributing + +We welcome contributions. Please follow these +[guidelines](https://github.com/google/mediapipe/blob/master/CONTRIBUTING.md). + +We use GitHub issues for tracking requests and bugs. Please post questions to +the MediaPipe Stack Overflow with a `mediapipe` tag. + +## Resources + +### Publications + +* [Bringing artworks to life with AR](https://developers.googleblog.com/2021/07/bringing-artworks-to-life-with-ar.html) + in Google Developers Blog +* [Prosthesis control via Mirru App using MediaPipe hand tracking](https://developers.googleblog.com/2021/05/control-your-mirru-prosthesis-with-mediapipe-hand-tracking.html) + in Google Developers Blog +* [SignAll SDK: Sign language interface using MediaPipe is now available for + developers](https://developers.googleblog.com/2021/04/signall-sdk-sign-language-interface-using-mediapipe-now-available.html) + in Google Developers Blog +* [MediaPipe Holistic - Simultaneous Face, Hand and Pose Prediction, on + Device](https://ai.googleblog.com/2020/12/mediapipe-holistic-simultaneous-face.html) + in Google AI Blog +* [Background Features in Google Meet, Powered by Web ML](https://ai.googleblog.com/2020/10/background-features-in-google-meet.html) + in Google AI Blog +* [MediaPipe 3D Face Transform](https://developers.googleblog.com/2020/09/mediapipe-3d-face-transform.html) + in Google Developers Blog +* [Instant Motion Tracking With MediaPipe](https://developers.googleblog.com/2020/08/instant-motion-tracking-with-mediapipe.html) + in Google Developers Blog +* [BlazePose - On-device Real-time Body Pose Tracking](https://ai.googleblog.com/2020/08/on-device-real-time-body-pose-tracking.html) + in Google AI Blog +* [MediaPipe Iris: Real-time Eye Tracking and Depth Estimation](https://ai.googleblog.com/2020/08/mediapipe-iris-real-time-iris-tracking.html) + in Google AI Blog +* [MediaPipe KNIFT: Template-based feature matching](https://developers.googleblog.com/2020/04/mediapipe-knift-template-based-feature-matching.html) + in Google Developers Blog +* [Alfred Camera: Smart camera features using MediaPipe](https://developers.googleblog.com/2020/03/alfred-camera-smart-camera-features-using-mediapipe.html) + in Google Developers Blog +* [Real-Time 3D Object Detection on Mobile Devices with MediaPipe](https://ai.googleblog.com/2020/03/real-time-3d-object-detection-on-mobile.html) + in Google AI Blog +* [AutoFlip: An Open Source Framework for Intelligent Video Reframing](https://ai.googleblog.com/2020/02/autoflip-open-source-framework-for.html) + in Google AI Blog +* [MediaPipe on the Web](https://developers.googleblog.com/2020/01/mediapipe-on-web.html) + in Google Developers Blog +* [Object Detection and Tracking using MediaPipe](https://developers.googleblog.com/2019/12/object-detection-and-tracking-using-mediapipe.html) + in Google Developers Blog +* [On-Device, Real-Time Hand Tracking with MediaPipe](https://ai.googleblog.com/2019/08/on-device-real-time-hand-tracking-with.html) + in Google AI Blog +* [MediaPipe: A Framework for Building Perception Pipelines](https://arxiv.org/abs/1906.08172) + +### Videos + +* [YouTube Channel](https://www.youtube.com/c/MediaPipe) diff --git a/.gitignore b/.gitignore index c861d4a6..6e182514 100644 --- a/.gitignore +++ b/.gitignore @@ -160,6 +160,8 @@ docker-compose.override.yml *.seed *.pid.lock *.pem +*.mp4 +*.zip models/ comfy_engine/test_outputs/ \ No newline at end of file diff --git a/3.0.0 b/3.0.0 new file mode 100644 index 00000000..e69de29b diff --git a/app/dist/index.html b/app/dist/index.html index 870fc272..3e662a8f 100644 --- a/app/dist/index.html +++ b/app/dist/index.html @@ -1,13 +1,17 @@ - - - - Velocity WebOS - - - - -
- - + + + + + Velocity WebOS + + + + + +
+ + + + \ No newline at end of file diff --git a/app/node_modules/.package-lock.json b/app/node_modules/.package-lock.json index 6c6e3e0c..41358b5f 100644 --- a/app/node_modules/.package-lock.json +++ b/app/node_modules/.package-lock.json @@ -1835,18 +1835,18 @@ "react": ">=16.8.0" } }, - "node_modules/@esbuild/darwin-arm64": { + "node_modules/@esbuild/win32-x64": { "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { "node": ">=18" @@ -2217,6 +2217,22 @@ "node": ">= 8" } }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -3922,18 +3938,32 @@ "dev": true, "license": "MIT" }, - "node_modules/@rollup/rollup-darwin-arm64": { + "node_modules/@rollup/rollup-win32-x64-gnu": { "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", - "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" ] }, "node_modules/@standard-schema/utils": { @@ -5776,21 +5806,6 @@ } } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -6603,6 +6618,38 @@ "node": ">= 6" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", diff --git a/app/node_modules/.tmp/tsconfig.app.tsbuildinfo b/app/node_modules/.tmp/tsconfig.app.tsbuildinfo index 51fe1201..fee76f26 100644 --- a/app/node_modules/.tmp/tsconfig.app.tsbuildinfo +++ b/app/node_modules/.tmp/tsconfig.app.tsbuildinfo @@ -1 +1,5 @@ -{"root":["../../src/app.tsx","../../src/main.tsx","../../src/app/oracle/page.tsx","../../src/components/layout/loginscreen.tsx","../../src/components/layout/sidebar.tsx","../../src/components/modules/catalyst.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/oracle/leadinspector.tsx","../../src/components/oracle/pipelineview.tsx","../../src/components/oracle/mockleads.ts","../../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/lib/oraclequeryclient.ts","../../src/lib/utils.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/lib/oracledemodata.ts","../../src/oracle/types/canvas.ts","../../src/store/usecurrencystore.ts","../../src/store/usemarketingstore.ts","../../src/store/usestore.ts","../../src/types/crm.ts","../../src/types/index.ts","../../src/utils/curvegenerator.ts"],"version":"5.9.3"} \ No newline at end of file +<<<<<<< HEAD +{"root":["../../src/app.tsx","../../src/main.tsx","../../src/app/oracle/page.tsx","../../src/components/layout/loginscreen.tsx","../../src/components/layout/sidebar.tsx","../../src/components/modules/catalyst.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/oracle/leadinspector.tsx","../../src/components/oracle/pipelineview.tsx","../../src/components/oracle/mockleads.ts","../../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/lib/oraclequeryclient.ts","../../src/lib/utils.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/lib/oracledemodata.ts","../../src/oracle/types/canvas.ts","../../src/store/usecurrencystore.ts","../../src/store/usemarketingstore.ts","../../src/store/usestore.ts","../../src/types/crm.ts","../../src/types/index.ts","../../src/utils/curvegenerator.ts"],"version":"5.9.3"} +======= +{"root":["../../src/app.tsx","../../src/global.d.ts","../../src/main.tsx","../../src/app/oracle/page.tsx","../../src/components/layout/loginscreen.tsx","../../src/components/layout/notificationcenter.tsx","../../src/components/layout/sidebar.tsx","../../src/components/modules/catalyst.tsx","../../src/components/modules/dashboard.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/oracle/mockleads.ts","../../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/usemediapipefacelandmarker.ts","../../src/hooks/usevelocitysocket.ts","../../src/lib/api.ts","../../src/lib/oraclequeryclient.ts","../../src/lib/utils.ts","../../src/store/usemarketingstore.ts","../../src/store/usestore.ts","../../src/types/crm.ts","../../src/types/index.ts","../../src/utils/curvegenerator.ts","../../src/utils/landmarkpacketencoder.ts"],"version":"5.9.3"} +>>>>>>> feat/#15 diff --git a/app/node_modules/.vite/deps/@radix-ui_react-avatar.js b/app/node_modules/.vite/deps/@radix-ui_react-avatar.js index fe29feb1..44bf9534 100644 --- a/app/node_modules/.vite/deps/@radix-ui_react-avatar.js +++ b/app/node_modules/.vite/deps/@radix-ui_react-avatar.js @@ -1,24 +1,36 @@ "use client"; import { +<<<<<<< HEAD createSlot } from "./chunk-YWBEB5PG.js"; import { require_shim } from "./chunk-TXHHHGR3.js"; import { +======= +>>>>>>> feat/#15 useCallbackRef, useLayoutEffect2 -} from "./chunk-23FVUG5N.js"; -import "./chunk-2VUH7NEY.js"; +} from "./chunk-GRXJTWBV.js"; import { +<<<<<<< HEAD +======= + require_shim +} from "./chunk-642Z5WD3.js"; +import { +>>>>>>> feat/#15 require_react_dom -} from "./chunk-YF4B4G2L.js"; +} from "./chunk-YLZ34CCM.js"; +import { + createSlot +} from "./chunk-5HUACAZ7.js"; +import "./chunk-HPBHRBIF.js"; import { require_jsx_runtime -} from "./chunk-2YVA4HRZ.js"; +} from "./chunk-USXRE7Q2.js"; import { require_react -} from "./chunk-WUR7D6NS.js"; +} from "./chunk-ZNKPWGXJ.js"; import { __toESM } from "./chunk-G3PMV62Z.js"; diff --git a/app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js b/app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js index 630b6bd3..da57abf3 100644 --- a/app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js +++ b/app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js @@ -2,20 +2,20 @@ import { useCallbackRef, useLayoutEffect2 -} from "./chunk-23FVUG5N.js"; +} from "./chunk-GRXJTWBV.js"; +import { + require_react_dom +} from "./chunk-YLZ34CCM.js"; import { composeRefs, useComposedRefs -} from "./chunk-2VUH7NEY.js"; -import { - require_react_dom -} from "./chunk-YF4B4G2L.js"; +} from "./chunk-HPBHRBIF.js"; import { require_jsx_runtime -} from "./chunk-2YVA4HRZ.js"; +} from "./chunk-USXRE7Q2.js"; import { require_react -} from "./chunk-WUR7D6NS.js"; +} from "./chunk-ZNKPWGXJ.js"; import { __toESM } from "./chunk-G3PMV62Z.js"; diff --git a/app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js.map b/app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js.map index bede1ace..6158e38c 100644 --- a/app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js.map +++ b/app/node_modules/.vite/deps/@radix-ui_react-dropdown-menu.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../@radix-ui/react-dropdown-menu/src/dropdown-menu.tsx", "../../@radix-ui/primitive/src/primitive.tsx", "../../@radix-ui/react-context/src/create-context.tsx", "../../@radix-ui/react-use-controllable-state/src/use-controllable-state.tsx", "../../@radix-ui/react-use-controllable-state/src/use-controllable-state-reducer.tsx", "../../@radix-ui/react-use-effect-event/src/use-effect-event.tsx", "../../@radix-ui/react-primitive/src/primitive.tsx", "../../@radix-ui/react-primitive/node_modules/@radix-ui/react-slot/src/slot.tsx", "../../@radix-ui/react-menu/src/menu.tsx", "../../@radix-ui/react-collection/src/collection-legacy.tsx", "../../@radix-ui/react-collection/src/collection.tsx", "../../@radix-ui/react-collection/src/ordered-dictionary.ts", "../../@radix-ui/react-collection/node_modules/@radix-ui/react-slot/src/slot.tsx", "../../@radix-ui/react-direction/src/direction.tsx", "../../@radix-ui/react-dismissable-layer/src/dismissable-layer.tsx", "../../@radix-ui/react-use-escape-keydown/src/use-escape-keydown.tsx", "../../@radix-ui/react-focus-guards/src/focus-guards.tsx", "../../@radix-ui/react-focus-scope/src/focus-scope.tsx", "../../@radix-ui/react-id/src/id.tsx", "../../@radix-ui/react-popper/src/popper.tsx", "../../@floating-ui/utils/dist/floating-ui.utils.mjs", "../../@floating-ui/core/dist/floating-ui.core.mjs", "../../@floating-ui/utils/dist/floating-ui.utils.dom.mjs", "../../@floating-ui/dom/dist/floating-ui.dom.mjs", "../../@floating-ui/react-dom/dist/floating-ui.react-dom.mjs", "../../@radix-ui/react-arrow/src/arrow.tsx", "../../@radix-ui/react-use-size/src/use-size.tsx", "../../@radix-ui/react-portal/src/portal.tsx", "../../@radix-ui/react-presence/src/presence.tsx", "../../@radix-ui/react-presence/src/use-state-machine.tsx", "../../@radix-ui/react-roving-focus/src/roving-focus-group.tsx", "../../@radix-ui/react-menu/node_modules/@radix-ui/react-slot/src/slot.tsx", "../../aria-hidden/dist/es2015/index.js", "../../tslib/tslib.es6.mjs", "../../react-remove-scroll/dist/es2015/Combination.js", "../../react-remove-scroll/dist/es2015/UI.js", "../../react-remove-scroll-bar/dist/es2015/constants.js", "../../use-callback-ref/dist/es2015/assignRef.js", "../../use-callback-ref/dist/es2015/useRef.js", "../../use-callback-ref/dist/es2015/useMergeRef.js", "../../use-sidecar/dist/es2015/hoc.js", "../../use-sidecar/dist/es2015/hook.js", "../../use-sidecar/dist/es2015/medium.js", "../../use-sidecar/dist/es2015/renderProp.js", "../../use-sidecar/dist/es2015/exports.js", "../../react-remove-scroll/dist/es2015/medium.js", "../../react-remove-scroll/dist/es2015/SideEffect.js", "../../react-remove-scroll-bar/dist/es2015/component.js", "../../react-style-singleton/dist/es2015/hook.js", "../../get-nonce/dist/es2015/index.js", "../../react-style-singleton/dist/es2015/singleton.js", "../../react-style-singleton/dist/es2015/component.js", "../../react-remove-scroll-bar/dist/es2015/utils.js", "../../react-remove-scroll/dist/es2015/aggresiveCapture.js", "../../react-remove-scroll/dist/es2015/handleScroll.js", "../../react-remove-scroll/dist/es2015/sidecar.js"], - "sourcesContent": ["import * as React from 'react';\nimport { composeEventHandlers } from '@radix-ui/primitive';\nimport { composeRefs } from '@radix-ui/react-compose-refs';\nimport { createContextScope } from '@radix-ui/react-context';\nimport { useControllableState } from '@radix-ui/react-use-controllable-state';\nimport { Primitive } from '@radix-ui/react-primitive';\nimport * as MenuPrimitive from '@radix-ui/react-menu';\nimport { createMenuScope } from '@radix-ui/react-menu';\nimport { useId } from '@radix-ui/react-id';\n\nimport type { Scope } from '@radix-ui/react-context';\n\ntype Direction = 'ltr' | 'rtl';\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenu\n * -----------------------------------------------------------------------------------------------*/\n\nconst DROPDOWN_MENU_NAME = 'DropdownMenu';\n\ntype ScopedProps

= P & { __scopeDropdownMenu?: Scope };\nconst [createDropdownMenuContext, createDropdownMenuScope] = createContextScope(\n DROPDOWN_MENU_NAME,\n [createMenuScope]\n);\nconst useMenuScope = createMenuScope();\n\ntype DropdownMenuContextValue = {\n triggerId: string;\n triggerRef: React.RefObject;\n contentId: string;\n open: boolean;\n onOpenChange(open: boolean): void;\n onOpenToggle(): void;\n modal: boolean;\n};\n\nconst [DropdownMenuProvider, useDropdownMenuContext] =\n createDropdownMenuContext(DROPDOWN_MENU_NAME);\n\ninterface DropdownMenuProps {\n children?: React.ReactNode;\n dir?: Direction;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?(open: boolean): void;\n modal?: boolean;\n}\n\nconst DropdownMenu: React.FC = (props: ScopedProps) => {\n const {\n __scopeDropdownMenu,\n children,\n dir,\n open: openProp,\n defaultOpen,\n onOpenChange,\n modal = true,\n } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n const triggerRef = React.useRef(null);\n const [open, setOpen] = useControllableState({\n prop: openProp,\n defaultProp: defaultOpen ?? false,\n onChange: onOpenChange,\n caller: DROPDOWN_MENU_NAME,\n });\n\n return (\n setOpen((prevOpen) => !prevOpen), [setOpen])}\n modal={modal}\n >\n \n {children}\n \n \n );\n};\n\nDropdownMenu.displayName = DROPDOWN_MENU_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuTrigger\n * -----------------------------------------------------------------------------------------------*/\n\nconst TRIGGER_NAME = 'DropdownMenuTrigger';\n\ntype DropdownMenuTriggerElement = React.ComponentRef;\ntype PrimitiveButtonProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuTriggerProps extends PrimitiveButtonProps {}\n\nconst DropdownMenuTrigger = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, disabled = false, ...triggerProps } = props;\n const context = useDropdownMenuContext(TRIGGER_NAME, __scopeDropdownMenu);\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return (\n \n {\n // only call handler if it's the left button (mousedown gets triggered by all mouse buttons)\n // but not when the control key is pressed (avoiding MacOS right click)\n if (!disabled && event.button === 0 && event.ctrlKey === false) {\n context.onOpenToggle();\n // prevent trigger focusing when opening\n // this allows the content to be given focus without competition\n if (!context.open) event.preventDefault();\n }\n })}\n onKeyDown={composeEventHandlers(props.onKeyDown, (event) => {\n if (disabled) return;\n if (['Enter', ' '].includes(event.key)) context.onOpenToggle();\n if (event.key === 'ArrowDown') context.onOpenChange(true);\n // prevent keydown from scrolling window / first focused item to execute\n // that keydown (inadvertently closing the menu)\n if (['Enter', ' ', 'ArrowDown'].includes(event.key)) event.preventDefault();\n })}\n />\n \n );\n }\n);\n\nDropdownMenuTrigger.displayName = TRIGGER_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuPortal\n * -----------------------------------------------------------------------------------------------*/\n\nconst PORTAL_NAME = 'DropdownMenuPortal';\n\ntype MenuPortalProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuPortalProps extends MenuPortalProps {}\n\nconst DropdownMenuPortal: React.FC = (\n props: ScopedProps\n) => {\n const { __scopeDropdownMenu, ...portalProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n};\n\nDropdownMenuPortal.displayName = PORTAL_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuContent\n * -----------------------------------------------------------------------------------------------*/\n\nconst CONTENT_NAME = 'DropdownMenuContent';\n\ntype DropdownMenuContentElement = React.ComponentRef;\ntype MenuContentProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuContentProps extends Omit {}\n\nconst DropdownMenuContent = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...contentProps } = props;\n const context = useDropdownMenuContext(CONTENT_NAME, __scopeDropdownMenu);\n const menuScope = useMenuScope(__scopeDropdownMenu);\n const hasInteractedOutsideRef = React.useRef(false);\n\n return (\n {\n if (!hasInteractedOutsideRef.current) context.triggerRef.current?.focus();\n hasInteractedOutsideRef.current = false;\n // Always prevent auto focus because we either focus manually or want user agent focus\n event.preventDefault();\n })}\n onInteractOutside={composeEventHandlers(props.onInteractOutside, (event) => {\n const originalEvent = event.detail.originalEvent as PointerEvent;\n const ctrlLeftClick = originalEvent.button === 0 && originalEvent.ctrlKey === true;\n const isRightClick = originalEvent.button === 2 || ctrlLeftClick;\n if (!context.modal || isRightClick) hasInteractedOutsideRef.current = true;\n })}\n style={{\n ...props.style,\n // re-namespace exposed content custom properties\n ...{\n '--radix-dropdown-menu-content-transform-origin':\n 'var(--radix-popper-transform-origin)',\n '--radix-dropdown-menu-content-available-width': 'var(--radix-popper-available-width)',\n '--radix-dropdown-menu-content-available-height':\n 'var(--radix-popper-available-height)',\n '--radix-dropdown-menu-trigger-width': 'var(--radix-popper-anchor-width)',\n '--radix-dropdown-menu-trigger-height': 'var(--radix-popper-anchor-height)',\n },\n }}\n />\n );\n }\n);\n\nDropdownMenuContent.displayName = CONTENT_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuGroup\n * -----------------------------------------------------------------------------------------------*/\n\nconst GROUP_NAME = 'DropdownMenuGroup';\n\ntype DropdownMenuGroupElement = React.ComponentRef;\ntype MenuGroupProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuGroupProps extends MenuGroupProps {}\n\nconst DropdownMenuGroup = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...groupProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n }\n);\n\nDropdownMenuGroup.displayName = GROUP_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuLabel\n * -----------------------------------------------------------------------------------------------*/\n\nconst LABEL_NAME = 'DropdownMenuLabel';\n\ntype DropdownMenuLabelElement = React.ComponentRef;\ntype MenuLabelProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuLabelProps extends MenuLabelProps {}\n\nconst DropdownMenuLabel = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...labelProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n }\n);\n\nDropdownMenuLabel.displayName = LABEL_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuItem\n * -----------------------------------------------------------------------------------------------*/\n\nconst ITEM_NAME = 'DropdownMenuItem';\n\ntype DropdownMenuItemElement = React.ComponentRef;\ntype MenuItemProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuItemProps extends MenuItemProps {}\n\nconst DropdownMenuItem = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...itemProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n }\n);\n\nDropdownMenuItem.displayName = ITEM_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuCheckboxItem\n * -----------------------------------------------------------------------------------------------*/\n\nconst CHECKBOX_ITEM_NAME = 'DropdownMenuCheckboxItem';\n\ntype DropdownMenuCheckboxItemElement = React.ComponentRef;\ntype MenuCheckboxItemProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuCheckboxItemProps extends MenuCheckboxItemProps {}\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n DropdownMenuCheckboxItemElement,\n DropdownMenuCheckboxItemProps\n>((props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...checkboxItemProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n});\n\nDropdownMenuCheckboxItem.displayName = CHECKBOX_ITEM_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuRadioGroup\n * -----------------------------------------------------------------------------------------------*/\n\nconst RADIO_GROUP_NAME = 'DropdownMenuRadioGroup';\n\ntype DropdownMenuRadioGroupElement = React.ComponentRef;\ntype MenuRadioGroupProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuRadioGroupProps extends MenuRadioGroupProps {}\n\nconst DropdownMenuRadioGroup = React.forwardRef<\n DropdownMenuRadioGroupElement,\n DropdownMenuRadioGroupProps\n>((props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...radioGroupProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n});\n\nDropdownMenuRadioGroup.displayName = RADIO_GROUP_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuRadioItem\n * -----------------------------------------------------------------------------------------------*/\n\nconst RADIO_ITEM_NAME = 'DropdownMenuRadioItem';\n\ntype DropdownMenuRadioItemElement = React.ComponentRef;\ntype MenuRadioItemProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuRadioItemProps extends MenuRadioItemProps {}\n\nconst DropdownMenuRadioItem = React.forwardRef<\n DropdownMenuRadioItemElement,\n DropdownMenuRadioItemProps\n>((props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...radioItemProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n});\n\nDropdownMenuRadioItem.displayName = RADIO_ITEM_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuItemIndicator\n * -----------------------------------------------------------------------------------------------*/\n\nconst INDICATOR_NAME = 'DropdownMenuItemIndicator';\n\ntype DropdownMenuItemIndicatorElement = React.ComponentRef;\ntype MenuItemIndicatorProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuItemIndicatorProps extends MenuItemIndicatorProps {}\n\nconst DropdownMenuItemIndicator = React.forwardRef<\n DropdownMenuItemIndicatorElement,\n DropdownMenuItemIndicatorProps\n>((props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...itemIndicatorProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n});\n\nDropdownMenuItemIndicator.displayName = INDICATOR_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuSeparator\n * -----------------------------------------------------------------------------------------------*/\n\nconst SEPARATOR_NAME = 'DropdownMenuSeparator';\n\ntype DropdownMenuSeparatorElement = React.ComponentRef;\ntype MenuSeparatorProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuSeparatorProps extends MenuSeparatorProps {}\n\nconst DropdownMenuSeparator = React.forwardRef<\n DropdownMenuSeparatorElement,\n DropdownMenuSeparatorProps\n>((props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...separatorProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n});\n\nDropdownMenuSeparator.displayName = SEPARATOR_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuArrow\n * -----------------------------------------------------------------------------------------------*/\n\nconst ARROW_NAME = 'DropdownMenuArrow';\n\ntype DropdownMenuArrowElement = React.ComponentRef;\ntype MenuArrowProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuArrowProps extends MenuArrowProps {}\n\nconst DropdownMenuArrow = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...arrowProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n }\n);\n\nDropdownMenuArrow.displayName = ARROW_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuSub\n * -----------------------------------------------------------------------------------------------*/\n\ninterface DropdownMenuSubProps {\n children?: React.ReactNode;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?(open: boolean): void;\n}\n\nconst DropdownMenuSub: React.FC = (\n props: ScopedProps\n) => {\n const { __scopeDropdownMenu, children, open: openProp, onOpenChange, defaultOpen } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n const [open, setOpen] = useControllableState({\n prop: openProp,\n defaultProp: defaultOpen ?? false,\n onChange: onOpenChange,\n caller: 'DropdownMenuSub',\n });\n\n return (\n \n {children}\n \n );\n};\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuSubTrigger\n * -----------------------------------------------------------------------------------------------*/\n\nconst SUB_TRIGGER_NAME = 'DropdownMenuSubTrigger';\n\ntype DropdownMenuSubTriggerElement = React.ComponentRef;\ntype MenuSubTriggerProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuSubTriggerProps extends MenuSubTriggerProps {}\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n DropdownMenuSubTriggerElement,\n DropdownMenuSubTriggerProps\n>((props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...subTriggerProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n return ;\n});\n\nDropdownMenuSubTrigger.displayName = SUB_TRIGGER_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DropdownMenuSubContent\n * -----------------------------------------------------------------------------------------------*/\n\nconst SUB_CONTENT_NAME = 'DropdownMenuSubContent';\n\ntype DropdownMenuSubContentElement = React.ComponentRef;\ntype MenuSubContentProps = React.ComponentPropsWithoutRef;\ninterface DropdownMenuSubContentProps extends MenuSubContentProps {}\n\nconst DropdownMenuSubContent = React.forwardRef<\n DropdownMenuSubContentElement,\n DropdownMenuSubContentProps\n>((props: ScopedProps, forwardedRef) => {\n const { __scopeDropdownMenu, ...subContentProps } = props;\n const menuScope = useMenuScope(__scopeDropdownMenu);\n\n return (\n \n );\n});\n\nDropdownMenuSubContent.displayName = SUB_CONTENT_NAME;\n\n/* -----------------------------------------------------------------------------------------------*/\n\nconst Root = DropdownMenu;\nconst Trigger = DropdownMenuTrigger;\nconst Portal = DropdownMenuPortal;\nconst Content = DropdownMenuContent;\nconst Group = DropdownMenuGroup;\nconst Label = DropdownMenuLabel;\nconst Item = DropdownMenuItem;\nconst CheckboxItem = DropdownMenuCheckboxItem;\nconst RadioGroup = DropdownMenuRadioGroup;\nconst RadioItem = DropdownMenuRadioItem;\nconst ItemIndicator = DropdownMenuItemIndicator;\nconst Separator = DropdownMenuSeparator;\nconst Arrow = DropdownMenuArrow;\nconst Sub = DropdownMenuSub;\nconst SubTrigger = DropdownMenuSubTrigger;\nconst SubContent = DropdownMenuSubContent;\n\nexport {\n createDropdownMenuScope,\n //\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuPortal,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuLabel,\n DropdownMenuItem,\n DropdownMenuCheckboxItem,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuItemIndicator,\n DropdownMenuSeparator,\n DropdownMenuArrow,\n DropdownMenuSub,\n DropdownMenuSubTrigger,\n DropdownMenuSubContent,\n //\n Root,\n Trigger,\n Portal,\n Content,\n Group,\n Label,\n Item,\n CheckboxItem,\n RadioGroup,\n RadioItem,\n ItemIndicator,\n Separator,\n Arrow,\n Sub,\n SubTrigger,\n SubContent,\n};\nexport type {\n DropdownMenuProps,\n DropdownMenuTriggerProps,\n DropdownMenuPortalProps,\n DropdownMenuContentProps,\n DropdownMenuGroupProps,\n DropdownMenuLabelProps,\n DropdownMenuItemProps,\n DropdownMenuCheckboxItemProps,\n DropdownMenuRadioGroupProps,\n DropdownMenuRadioItemProps,\n DropdownMenuItemIndicatorProps,\n DropdownMenuSeparatorProps,\n DropdownMenuArrowProps,\n DropdownMenuSubProps,\n DropdownMenuSubTriggerProps,\n DropdownMenuSubContentProps,\n};\n", "/* eslint-disable no-restricted-properties */\n\n/* eslint-disable no-restricted-globals */\nexport const canUseDOM = !!(\n typeof window !== 'undefined' &&\n window.document &&\n window.document.createElement\n);\n/* eslint-enable no-restricted-globals */\n\nexport function composeEventHandlers(\n originalEventHandler?: (event: E) => void,\n ourEventHandler?: (event: E) => void,\n { checkForDefaultPrevented = true } = {}\n) {\n return function handleEvent(event: E) {\n originalEventHandler?.(event);\n\n if (checkForDefaultPrevented === false || !event.defaultPrevented) {\n return ourEventHandler?.(event);\n }\n };\n}\n\nexport function getOwnerWindow(element: Node | null | undefined) {\n if (!canUseDOM) {\n throw new Error('Cannot access window outside of the DOM');\n }\n // eslint-disable-next-line no-restricted-globals\n return element?.ownerDocument?.defaultView ?? window;\n}\n\nexport function getOwnerDocument(element: Node | null | undefined) {\n if (!canUseDOM) {\n throw new Error('Cannot access document outside of the DOM');\n }\n // eslint-disable-next-line no-restricted-globals\n return element?.ownerDocument ?? document;\n}\n\n/**\n * Lifted from https://github.com/ariakit/ariakit/blob/main/packages/ariakit-core/src/utils/dom.ts#L37\n * MIT License, Copyright (c) AriaKit.\n */\nexport function getActiveElement(\n node: Node | null | undefined,\n activeDescendant = false\n): HTMLElement | null {\n const { activeElement } = getOwnerDocument(node);\n if (!activeElement?.nodeName) {\n // `activeElement` might be an empty object if we're interacting with elements\n // inside of an iframe.\n return null;\n }\n\n if (isFrame(activeElement) && activeElement.contentDocument) {\n return getActiveElement(activeElement.contentDocument.body, activeDescendant);\n }\n\n if (activeDescendant) {\n const id = activeElement.getAttribute('aria-activedescendant');\n if (id) {\n const element = getOwnerDocument(activeElement).getElementById(id);\n if (element) {\n return element;\n }\n }\n }\n\n return activeElement as HTMLElement | null;\n}\n\nexport function isFrame(element: Element): element is HTMLIFrameElement {\n return element.tagName === 'IFRAME';\n}\n", "import * as React from 'react';\n\nfunction createContext(\n rootComponentName: string,\n defaultContext?: ContextValueType\n) {\n const Context = React.createContext(defaultContext);\n\n const Provider: React.FC = (props) => {\n const { children, ...context } = props;\n // Only re-memoize when prop values change\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const value = React.useMemo(() => context, Object.values(context)) as ContextValueType;\n return {children};\n };\n\n Provider.displayName = rootComponentName + 'Provider';\n\n function useContext(consumerName: string) {\n const context = React.useContext(Context);\n if (context) return context;\n if (defaultContext !== undefined) return defaultContext;\n // if a defaultContext wasn't specified, it's a required context.\n throw new Error(`\\`${consumerName}\\` must be used within \\`${rootComponentName}\\``);\n }\n\n return [Provider, useContext] as const;\n}\n\n/* -------------------------------------------------------------------------------------------------\n * createContextScope\n * -----------------------------------------------------------------------------------------------*/\n\ntype Scope = { [scopeName: string]: React.Context[] } | undefined;\ntype ScopeHook = (scope: Scope) => { [__scopeProp: string]: Scope };\ninterface CreateScope {\n scopeName: string;\n (): ScopeHook;\n}\n\nfunction createContextScope(scopeName: string, createContextScopeDeps: CreateScope[] = []) {\n let defaultContexts: any[] = [];\n\n /* -----------------------------------------------------------------------------------------------\n * createContext\n * ---------------------------------------------------------------------------------------------*/\n\n function createContext(\n rootComponentName: string,\n defaultContext?: ContextValueType\n ) {\n const BaseContext = React.createContext(defaultContext);\n const index = defaultContexts.length;\n defaultContexts = [...defaultContexts, defaultContext];\n\n const Provider: React.FC<\n ContextValueType & { scope: Scope; children: React.ReactNode }\n > = (props) => {\n const { scope, children, ...context } = props;\n const Context = scope?.[scopeName]?.[index] || BaseContext;\n // Only re-memoize when prop values change\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const value = React.useMemo(() => context, Object.values(context)) as ContextValueType;\n return {children};\n };\n\n Provider.displayName = rootComponentName + 'Provider';\n\n function useContext(consumerName: string, scope: Scope) {\n const Context = scope?.[scopeName]?.[index] || BaseContext;\n const context = React.useContext(Context);\n if (context) return context;\n if (defaultContext !== undefined) return defaultContext;\n // if a defaultContext wasn't specified, it's a required context.\n throw new Error(`\\`${consumerName}\\` must be used within \\`${rootComponentName}\\``);\n }\n\n return [Provider, useContext] as const;\n }\n\n /* -----------------------------------------------------------------------------------------------\n * createScope\n * ---------------------------------------------------------------------------------------------*/\n\n const createScope: CreateScope = () => {\n const scopeContexts = defaultContexts.map((defaultContext) => {\n return React.createContext(defaultContext);\n });\n return function useScope(scope: Scope) {\n const contexts = scope?.[scopeName] || scopeContexts;\n return React.useMemo(\n () => ({ [`__scope${scopeName}`]: { ...scope, [scopeName]: contexts } }),\n [scope, contexts]\n );\n };\n };\n\n createScope.scopeName = scopeName;\n return [createContext, composeContextScopes(createScope, ...createContextScopeDeps)] as const;\n}\n\n/* -------------------------------------------------------------------------------------------------\n * composeContextScopes\n * -----------------------------------------------------------------------------------------------*/\n\nfunction composeContextScopes(...scopes: CreateScope[]) {\n const baseScope = scopes[0];\n if (scopes.length === 1) return baseScope;\n\n const createScope: CreateScope = () => {\n const scopeHooks = scopes.map((createScope) => ({\n useScope: createScope(),\n scopeName: createScope.scopeName,\n }));\n\n return function useComposedScopes(overrideScopes) {\n const nextScopes = scopeHooks.reduce((nextScopes, { useScope, scopeName }) => {\n // We are calling a hook inside a callback which React warns against to avoid inconsistent\n // renders, however, scoping doesn't have render side effects so we ignore the rule.\n // eslint-disable-next-line react-hooks/rules-of-hooks\n const scopeProps = useScope(overrideScopes);\n const currentScope = scopeProps[`__scope${scopeName}`];\n return { ...nextScopes, ...currentScope };\n }, {});\n\n return React.useMemo(() => ({ [`__scope${baseScope.scopeName}`]: nextScopes }), [nextScopes]);\n };\n };\n\n createScope.scopeName = baseScope.scopeName;\n return createScope;\n}\n\n/* -----------------------------------------------------------------------------------------------*/\n\nexport { createContext, createContextScope };\nexport type { CreateScope, Scope };\n", "import * as React from 'react';\nimport { useLayoutEffect } from '@radix-ui/react-use-layout-effect';\n\n// Prevent bundlers from trying to optimize the import\nconst useInsertionEffect: typeof useLayoutEffect =\n (React as any)[' useInsertionEffect '.trim().toString()] || useLayoutEffect;\n\ntype ChangeHandler = (state: T) => void;\ntype SetStateFn = React.Dispatch>;\n\ninterface UseControllableStateParams {\n prop?: T | undefined;\n defaultProp: T;\n onChange?: ChangeHandler;\n caller?: string;\n}\n\nexport function useControllableState({\n prop,\n defaultProp,\n onChange = () => {},\n caller,\n}: UseControllableStateParams): [T, SetStateFn] {\n const [uncontrolledProp, setUncontrolledProp, onChangeRef] = useUncontrolledState({\n defaultProp,\n onChange,\n });\n const isControlled = prop !== undefined;\n const value = isControlled ? prop : uncontrolledProp;\n\n // OK to disable conditionally calling hooks here because they will always run\n // consistently in the same environment. Bundlers should be able to remove the\n // code block entirely in production.\n /* eslint-disable react-hooks/rules-of-hooks */\n if (process.env.NODE_ENV !== 'production') {\n const isControlledRef = React.useRef(prop !== undefined);\n React.useEffect(() => {\n const wasControlled = isControlledRef.current;\n if (wasControlled !== isControlled) {\n const from = wasControlled ? 'controlled' : 'uncontrolled';\n const to = isControlled ? 'controlled' : 'uncontrolled';\n console.warn(\n `${caller} is changing from ${from} to ${to}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`\n );\n }\n isControlledRef.current = isControlled;\n }, [isControlled, caller]);\n }\n /* eslint-enable react-hooks/rules-of-hooks */\n\n const setValue = React.useCallback>(\n (nextValue) => {\n if (isControlled) {\n const value = isFunction(nextValue) ? nextValue(prop) : nextValue;\n if (value !== prop) {\n onChangeRef.current?.(value);\n }\n } else {\n setUncontrolledProp(nextValue);\n }\n },\n [isControlled, prop, setUncontrolledProp, onChangeRef]\n );\n\n return [value, setValue];\n}\n\nfunction useUncontrolledState({\n defaultProp,\n onChange,\n}: Omit, 'prop'>): [\n Value: T,\n setValue: React.Dispatch>,\n OnChangeRef: React.RefObject | undefined>,\n] {\n const [value, setValue] = React.useState(defaultProp);\n const prevValueRef = React.useRef(value);\n\n const onChangeRef = React.useRef(onChange);\n useInsertionEffect(() => {\n onChangeRef.current = onChange;\n }, [onChange]);\n\n React.useEffect(() => {\n if (prevValueRef.current !== value) {\n onChangeRef.current?.(value);\n prevValueRef.current = value;\n }\n }, [value, prevValueRef]);\n\n return [value, setValue, onChangeRef];\n}\n\nfunction isFunction(value: unknown): value is (...args: any[]) => any {\n return typeof value === 'function';\n}\n", "import * as React from 'react';\nimport { useEffectEvent } from '@radix-ui/react-use-effect-event';\n\ntype ChangeHandler = (state: T) => void;\n\ninterface UseControllableStateParams {\n prop: T | undefined;\n defaultProp: T;\n onChange: ChangeHandler | undefined;\n caller: string;\n}\n\ninterface AnyAction {\n type: string;\n}\n\nconst SYNC_STATE = Symbol('RADIX:SYNC_STATE');\n\ninterface SyncStateAction {\n type: typeof SYNC_STATE;\n state: T;\n}\n\nexport function useControllableStateReducer(\n reducer: (prevState: S & { state: T }, action: A) => S & { state: T },\n userArgs: UseControllableStateParams,\n initialState: S\n): [S & { state: T }, React.Dispatch];\n\nexport function useControllableStateReducer(\n reducer: (prevState: S & { state: T }, action: A) => S & { state: T },\n userArgs: UseControllableStateParams,\n initialArg: I,\n init: (i: I & { state: T }) => S\n): [S & { state: T }, React.Dispatch];\n\nexport function useControllableStateReducer(\n reducer: (prevState: S & { state: T }, action: A) => S & { state: T },\n userArgs: UseControllableStateParams,\n initialArg: any,\n init?: (i: any) => Omit\n): [S & { state: T }, React.Dispatch] {\n const { prop: controlledState, defaultProp, onChange: onChangeProp, caller } = userArgs;\n const isControlled = controlledState !== undefined;\n\n const onChange = useEffectEvent(onChangeProp);\n\n // OK to disable conditionally calling hooks here because they will always run\n // consistently in the same environment. Bundlers should be able to remove the\n // code block entirely in production.\n /* eslint-disable react-hooks/rules-of-hooks */\n if (process.env.NODE_ENV !== 'production') {\n const isControlledRef = React.useRef(controlledState !== undefined);\n React.useEffect(() => {\n const wasControlled = isControlledRef.current;\n if (wasControlled !== isControlled) {\n const from = wasControlled ? 'controlled' : 'uncontrolled';\n const to = isControlled ? 'controlled' : 'uncontrolled';\n console.warn(\n `${caller} is changing from ${from} to ${to}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`\n );\n }\n isControlledRef.current = isControlled;\n }, [isControlled, caller]);\n }\n /* eslint-enable react-hooks/rules-of-hooks */\n\n type InternalState = S & { state: T };\n const args: [InternalState] = [{ ...initialArg, state: defaultProp }];\n if (init) {\n // @ts-expect-error\n args.push(init);\n }\n\n const [internalState, dispatch] = React.useReducer(\n (state: InternalState, action: A | SyncStateAction): InternalState => {\n if (action.type === SYNC_STATE) {\n return { ...state, state: action.state };\n }\n\n const next = reducer(state, action);\n if (isControlled && !Object.is(next.state, state.state)) {\n onChange(next.state);\n }\n return next;\n },\n ...args\n );\n\n const uncontrolledState = internalState.state;\n const prevValueRef = React.useRef(uncontrolledState);\n React.useEffect(() => {\n if (prevValueRef.current !== uncontrolledState) {\n prevValueRef.current = uncontrolledState;\n if (!isControlled) {\n onChange(uncontrolledState);\n }\n }\n }, [onChange, uncontrolledState, prevValueRef, isControlled]);\n\n const state = React.useMemo(() => {\n const isControlled = controlledState !== undefined;\n if (isControlled) {\n return { ...internalState, state: controlledState };\n }\n\n return internalState;\n }, [internalState, controlledState]);\n\n React.useEffect(() => {\n // Sync internal state for controlled components so that reducer is called\n // with the correct state values\n if (isControlled && !Object.is(controlledState, internalState.state)) {\n dispatch({ type: SYNC_STATE, state: controlledState });\n }\n }, [controlledState, internalState.state, isControlled]);\n\n return [state, dispatch as React.Dispatch];\n}\n", "/* eslint-disable react-hooks/rules-of-hooks */\nimport { useLayoutEffect } from '@radix-ui/react-use-layout-effect';\nimport * as React from 'react';\n\ntype AnyFunction = (...args: any[]) => any;\n\n// See https://github.com/webpack/webpack/issues/14814\nconst useReactEffectEvent = (React as any)[' useEffectEvent '.trim().toString()];\nconst useReactInsertionEffect = (React as any)[' useInsertionEffect '.trim().toString()];\n\n/**\n * Designed to approximate the behavior on `experimental_useEffectEvent` as best\n * as possible until its stable release, and back-fill it as a shim as needed.\n */\nexport function useEffectEvent(callback?: T): T {\n if (typeof useReactEffectEvent === 'function') {\n return useReactEffectEvent(callback);\n }\n\n const ref = React.useRef(() => {\n throw new Error('Cannot call an event handler while rendering.');\n });\n // See https://github.com/webpack/webpack/issues/14814\n if (typeof useReactInsertionEffect === 'function') {\n useReactInsertionEffect(() => {\n ref.current = callback;\n });\n } else {\n useLayoutEffect(() => {\n ref.current = callback;\n });\n }\n\n // https://github.com/facebook/react/issues/19240\n return React.useMemo(() => ((...args) => ref.current?.(...args)) as T, []);\n}\n", "import * as React from 'react';\nimport * as ReactDOM from 'react-dom';\nimport { createSlot } from '@radix-ui/react-slot';\n\nconst NODES = [\n 'a',\n 'button',\n 'div',\n 'form',\n 'h2',\n 'h3',\n 'img',\n 'input',\n 'label',\n 'li',\n 'nav',\n 'ol',\n 'p',\n 'select',\n 'span',\n 'svg',\n 'ul',\n] as const;\n\ntype Primitives = { [E in (typeof NODES)[number]]: PrimitiveForwardRefComponent };\ntype PrimitivePropsWithRef = React.ComponentPropsWithRef & {\n asChild?: boolean;\n};\n\ninterface PrimitiveForwardRefComponent\n extends React.ForwardRefExoticComponent> {}\n\n/* -------------------------------------------------------------------------------------------------\n * Primitive\n * -----------------------------------------------------------------------------------------------*/\n\nconst Primitive = NODES.reduce((primitive, node) => {\n const Slot = createSlot(`Primitive.${node}`);\n const Node = React.forwardRef((props: PrimitivePropsWithRef, forwardedRef: any) => {\n const { asChild, ...primitiveProps } = props;\n const Comp: any = asChild ? Slot : node;\n\n if (typeof window !== 'undefined') {\n (window as any)[Symbol.for('radix-ui')] = true;\n }\n\n return ;\n });\n\n Node.displayName = `Primitive.${node}`;\n\n return { ...primitive, [node]: Node };\n}, {} as Primitives);\n\n/* -------------------------------------------------------------------------------------------------\n * Utils\n * -----------------------------------------------------------------------------------------------*/\n\n/**\n * Flush custom event dispatch\n * https://github.com/radix-ui/primitives/pull/1378\n *\n * React batches *all* event handlers since version 18, this introduces certain considerations when using custom event types.\n *\n * Internally, React prioritises events in the following order:\n * - discrete\n * - continuous\n * - default\n *\n * https://github.com/facebook/react/blob/a8a4742f1c54493df00da648a3f9d26e3db9c8b5/packages/react-dom/src/events/ReactDOMEventListener.js#L294-L350\n *\n * `discrete` is an important distinction as updates within these events are applied immediately.\n * React however, is not able to infer the priority of custom event types due to how they are detected internally.\n * Because of this, it's possible for updates from custom events to be unexpectedly batched when\n * dispatched by another `discrete` event.\n *\n * In order to ensure that updates from custom events are applied predictably, we need to manually flush the batch.\n * This utility should be used when dispatching a custom event from within another `discrete` event, this utility\n * is not necessary when dispatching known event types, or if dispatching a custom type inside a non-discrete event.\n * For example:\n *\n * dispatching a known click \uD83D\uDC4E\n * target.dispatchEvent(new Event(\u2018click\u2019))\n *\n * dispatching a custom type within a non-discrete event \uD83D\uDC4E\n * onScroll={(event) => event.target.dispatchEvent(new CustomEvent(\u2018customType\u2019))}\n *\n * dispatching a custom type within a `discrete` event \uD83D\uDC4D\n * onPointerDown={(event) => dispatchDiscreteCustomEvent(event.target, new CustomEvent(\u2018customType\u2019))}\n *\n * Note: though React classifies `focus`, `focusin` and `focusout` events as `discrete`, it's not recommended to use\n * this utility with them. This is because it's possible for those handlers to be called implicitly during render\n * e.g. when focus is within a component as it is unmounted, or when managing focus on mount.\n */\n\nfunction dispatchDiscreteCustomEvent(target: E['target'], event: E) {\n if (target) ReactDOM.flushSync(() => target.dispatchEvent(event));\n}\n\n/* -----------------------------------------------------------------------------------------------*/\n\nconst Root = Primitive;\n\nexport {\n Primitive,\n //\n Root,\n //\n dispatchDiscreteCustomEvent,\n};\nexport type { PrimitivePropsWithRef };\n", "import * as React from 'react';\nimport { composeRefs } from '@radix-ui/react-compose-refs';\n\n/* -------------------------------------------------------------------------------------------------\n * Slot\n * -----------------------------------------------------------------------------------------------*/\n\ninterface SlotProps extends React.HTMLAttributes {\n children?: React.ReactNode;\n}\n\n/* @__NO_SIDE_EFFECTS__ */ export function createSlot(ownerName: string) {\n const SlotClone = createSlotClone(ownerName);\n const Slot = React.forwardRef((props, forwardedRef) => {\n const { children, ...slotProps } = props;\n const childrenArray = React.Children.toArray(children);\n const slottable = childrenArray.find(isSlottable);\n\n if (slottable) {\n // the new element to render is the one passed as a child of `Slottable`\n const newElement = slottable.props.children;\n\n const newChildren = childrenArray.map((child) => {\n if (child === slottable) {\n // because the new element will be the one rendered, we are only interested\n // in grabbing its children (`newElement.props.children`)\n if (React.Children.count(newElement) > 1) return React.Children.only(null);\n return React.isValidElement(newElement)\n ? (newElement.props as { children: React.ReactNode }).children\n : null;\n } else {\n return child;\n }\n });\n\n return (\n \n {React.isValidElement(newElement)\n ? React.cloneElement(newElement, undefined, newChildren)\n : null}\n \n );\n }\n\n return (\n \n {children}\n \n );\n });\n\n Slot.displayName = `${ownerName}.Slot`;\n return Slot;\n}\n\nconst Slot = createSlot('Slot');\n\n/* -------------------------------------------------------------------------------------------------\n * SlotClone\n * -----------------------------------------------------------------------------------------------*/\n\ninterface SlotCloneProps {\n children: React.ReactNode;\n}\n\n/* @__NO_SIDE_EFFECTS__ */ function createSlotClone(ownerName: string) {\n const SlotClone = React.forwardRef((props, forwardedRef) => {\n const { children, ...slotProps } = props;\n\n if (React.isValidElement(children)) {\n const childrenRef = getElementRef(children);\n const props = mergeProps(slotProps, children.props as AnyProps);\n // do not pass ref to React.Fragment for React 19 compatibility\n if (children.type !== React.Fragment) {\n props.ref = forwardedRef ? composeRefs(forwardedRef, childrenRef) : childrenRef;\n }\n return React.cloneElement(children, props);\n }\n\n return React.Children.count(children) > 1 ? React.Children.only(null) : null;\n });\n\n SlotClone.displayName = `${ownerName}.SlotClone`;\n return SlotClone;\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Slottable\n * -----------------------------------------------------------------------------------------------*/\n\nconst SLOTTABLE_IDENTIFIER = Symbol('radix.slottable');\n\ninterface SlottableProps {\n children: React.ReactNode;\n}\n\ninterface SlottableComponent extends React.FC {\n __radixId: symbol;\n}\n\n/* @__NO_SIDE_EFFECTS__ */ export function createSlottable(ownerName: string) {\n const Slottable: SlottableComponent = ({ children }) => {\n return <>{children};\n };\n Slottable.displayName = `${ownerName}.Slottable`;\n Slottable.__radixId = SLOTTABLE_IDENTIFIER;\n return Slottable;\n}\n\nconst Slottable = createSlottable('Slottable');\n\n/* ---------------------------------------------------------------------------------------------- */\n\ntype AnyProps = Record;\n\nfunction isSlottable(\n child: React.ReactNode\n): child is React.ReactElement {\n return (\n React.isValidElement(child) &&\n typeof child.type === 'function' &&\n '__radixId' in child.type &&\n child.type.__radixId === SLOTTABLE_IDENTIFIER\n );\n}\n\nfunction mergeProps(slotProps: AnyProps, childProps: AnyProps) {\n // all child props should override\n const overrideProps = { ...childProps };\n\n for (const propName in childProps) {\n const slotPropValue = slotProps[propName];\n const childPropValue = childProps[propName];\n\n const isHandler = /^on[A-Z]/.test(propName);\n if (isHandler) {\n // if the handler exists on both, we compose them\n if (slotPropValue && childPropValue) {\n overrideProps[propName] = (...args: unknown[]) => {\n const result = childPropValue(...args);\n slotPropValue(...args);\n return result;\n };\n }\n // but if it exists only on the slot, we use only this one\n else if (slotPropValue) {\n overrideProps[propName] = slotPropValue;\n }\n }\n // if it's `style`, we merge them\n else if (propName === 'style') {\n overrideProps[propName] = { ...slotPropValue, ...childPropValue };\n } else if (propName === 'className') {\n overrideProps[propName] = [slotPropValue, childPropValue].filter(Boolean).join(' ');\n }\n }\n\n return { ...slotProps, ...overrideProps };\n}\n\n// Before React 19 accessing `element.props.ref` will throw a warning and suggest using `element.ref`\n// After React 19 accessing `element.ref` does the opposite.\n// https://github.com/facebook/react/pull/28348\n//\n// Access the ref using the method that doesn't yield a warning.\nfunction getElementRef(element: React.ReactElement) {\n // React <=18 in DEV\n let getter = Object.getOwnPropertyDescriptor(element.props, 'ref')?.get;\n let mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning;\n if (mayWarn) {\n return (element as any).ref;\n }\n\n // React 19 in DEV\n getter = Object.getOwnPropertyDescriptor(element, 'ref')?.get;\n mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning;\n if (mayWarn) {\n return (element.props as { ref?: React.Ref }).ref;\n }\n\n // Not DEV\n return (element.props as { ref?: React.Ref }).ref || (element as any).ref;\n}\n\nexport {\n Slot,\n Slottable,\n //\n Slot as Root,\n};\nexport type { SlotProps };\n", "import * as React from 'react';\nimport { composeEventHandlers } from '@radix-ui/primitive';\nimport { createCollection } from '@radix-ui/react-collection';\nimport { useComposedRefs, composeRefs } from '@radix-ui/react-compose-refs';\nimport { createContextScope } from '@radix-ui/react-context';\nimport { useDirection } from '@radix-ui/react-direction';\nimport { DismissableLayer } from '@radix-ui/react-dismissable-layer';\nimport { useFocusGuards } from '@radix-ui/react-focus-guards';\nimport { FocusScope } from '@radix-ui/react-focus-scope';\nimport { useId } from '@radix-ui/react-id';\nimport * as PopperPrimitive from '@radix-ui/react-popper';\nimport { createPopperScope } from '@radix-ui/react-popper';\nimport { Portal as PortalPrimitive } from '@radix-ui/react-portal';\nimport { Presence } from '@radix-ui/react-presence';\nimport { Primitive, dispatchDiscreteCustomEvent } from '@radix-ui/react-primitive';\nimport * as RovingFocusGroup from '@radix-ui/react-roving-focus';\nimport { createRovingFocusGroupScope } from '@radix-ui/react-roving-focus';\nimport { createSlot } from '@radix-ui/react-slot';\nimport { useCallbackRef } from '@radix-ui/react-use-callback-ref';\nimport { hideOthers } from 'aria-hidden';\nimport { RemoveScroll } from 'react-remove-scroll';\n\nimport type { Scope } from '@radix-ui/react-context';\n\ntype Direction = 'ltr' | 'rtl';\n\nconst SELECTION_KEYS = ['Enter', ' '];\nconst FIRST_KEYS = ['ArrowDown', 'PageUp', 'Home'];\nconst LAST_KEYS = ['ArrowUp', 'PageDown', 'End'];\nconst FIRST_LAST_KEYS = [...FIRST_KEYS, ...LAST_KEYS];\nconst SUB_OPEN_KEYS: Record = {\n ltr: [...SELECTION_KEYS, 'ArrowRight'],\n rtl: [...SELECTION_KEYS, 'ArrowLeft'],\n};\nconst SUB_CLOSE_KEYS: Record = {\n ltr: ['ArrowLeft'],\n rtl: ['ArrowRight'],\n};\n\n/* -------------------------------------------------------------------------------------------------\n * Menu\n * -----------------------------------------------------------------------------------------------*/\n\nconst MENU_NAME = 'Menu';\n\ntype ItemData = { disabled: boolean; textValue: string };\nconst [Collection, useCollection, createCollectionScope] = createCollection<\n MenuItemElement,\n ItemData\n>(MENU_NAME);\n\ntype ScopedProps

= P & { __scopeMenu?: Scope };\nconst [createMenuContext, createMenuScope] = createContextScope(MENU_NAME, [\n createCollectionScope,\n createPopperScope,\n createRovingFocusGroupScope,\n]);\nconst usePopperScope = createPopperScope();\nconst useRovingFocusGroupScope = createRovingFocusGroupScope();\n\ntype MenuContextValue = {\n open: boolean;\n onOpenChange(open: boolean): void;\n content: MenuContentElement | null;\n onContentChange(content: MenuContentElement | null): void;\n};\n\nconst [MenuProvider, useMenuContext] = createMenuContext(MENU_NAME);\n\ntype MenuRootContextValue = {\n onClose(): void;\n isUsingKeyboardRef: React.RefObject;\n dir: Direction;\n modal: boolean;\n};\n\nconst [MenuRootProvider, useMenuRootContext] = createMenuContext(MENU_NAME);\n\ninterface MenuProps {\n children?: React.ReactNode;\n open?: boolean;\n onOpenChange?(open: boolean): void;\n dir?: Direction;\n modal?: boolean;\n}\n\nconst Menu: React.FC = (props: ScopedProps) => {\n const { __scopeMenu, open = false, children, dir, onOpenChange, modal = true } = props;\n const popperScope = usePopperScope(__scopeMenu);\n const [content, setContent] = React.useState(null);\n const isUsingKeyboardRef = React.useRef(false);\n const handleOpenChange = useCallbackRef(onOpenChange);\n const direction = useDirection(dir);\n\n React.useEffect(() => {\n // Capture phase ensures we set the boolean before any side effects execute\n // in response to the key or pointer event as they might depend on this value.\n const handleKeyDown = () => {\n isUsingKeyboardRef.current = true;\n document.addEventListener('pointerdown', handlePointer, { capture: true, once: true });\n document.addEventListener('pointermove', handlePointer, { capture: true, once: true });\n };\n const handlePointer = () => (isUsingKeyboardRef.current = false);\n document.addEventListener('keydown', handleKeyDown, { capture: true });\n return () => {\n document.removeEventListener('keydown', handleKeyDown, { capture: true });\n document.removeEventListener('pointerdown', handlePointer, { capture: true });\n document.removeEventListener('pointermove', handlePointer, { capture: true });\n };\n }, []);\n\n return (\n \n \n handleOpenChange(false), [handleOpenChange])}\n isUsingKeyboardRef={isUsingKeyboardRef}\n dir={direction}\n modal={modal}\n >\n {children}\n \n \n \n );\n};\n\nMenu.displayName = MENU_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuAnchor\n * -----------------------------------------------------------------------------------------------*/\n\nconst ANCHOR_NAME = 'MenuAnchor';\n\ntype MenuAnchorElement = React.ComponentRef;\ntype PopperAnchorProps = React.ComponentPropsWithoutRef;\ninterface MenuAnchorProps extends PopperAnchorProps {}\n\nconst MenuAnchor = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeMenu, ...anchorProps } = props;\n const popperScope = usePopperScope(__scopeMenu);\n return ;\n }\n);\n\nMenuAnchor.displayName = ANCHOR_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuPortal\n * -----------------------------------------------------------------------------------------------*/\n\nconst PORTAL_NAME = 'MenuPortal';\n\ntype PortalContextValue = { forceMount?: true };\nconst [PortalProvider, usePortalContext] = createMenuContext(PORTAL_NAME, {\n forceMount: undefined,\n});\n\ntype PortalProps = React.ComponentPropsWithoutRef;\ninterface MenuPortalProps {\n children?: React.ReactNode;\n /**\n * Specify a container element to portal the content into.\n */\n container?: PortalProps['container'];\n /**\n * Used to force mounting when more control is needed. Useful when\n * controlling animation with React animation libraries.\n */\n forceMount?: true;\n}\n\nconst MenuPortal: React.FC = (props: ScopedProps) => {\n const { __scopeMenu, forceMount, children, container } = props;\n const context = useMenuContext(PORTAL_NAME, __scopeMenu);\n return (\n \n \n \n {children}\n \n \n \n );\n};\n\nMenuPortal.displayName = PORTAL_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuContent\n * -----------------------------------------------------------------------------------------------*/\n\nconst CONTENT_NAME = 'MenuContent';\n\ntype MenuContentContextValue = {\n onItemEnter(event: React.PointerEvent): void;\n onItemLeave(event: React.PointerEvent): void;\n onTriggerLeave(event: React.PointerEvent): void;\n searchRef: React.RefObject;\n pointerGraceTimerRef: React.MutableRefObject;\n onPointerGraceIntentChange(intent: GraceIntent | null): void;\n};\nconst [MenuContentProvider, useMenuContentContext] =\n createMenuContext(CONTENT_NAME);\n\ntype MenuContentElement = MenuRootContentTypeElement;\n/**\n * We purposefully don't union MenuRootContent and MenuSubContent props here because\n * they have conflicting prop types. We agreed that we would allow MenuSubContent to\n * accept props that it would just ignore.\n */\ninterface MenuContentProps extends MenuRootContentTypeProps {\n /**\n * Used to force mounting when more control is needed. Useful when\n * controlling animation with React animation libraries.\n */\n forceMount?: true;\n}\n\nconst MenuContent = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const portalContext = usePortalContext(CONTENT_NAME, props.__scopeMenu);\n const { forceMount = portalContext.forceMount, ...contentProps } = props;\n const context = useMenuContext(CONTENT_NAME, props.__scopeMenu);\n const rootContext = useMenuRootContext(CONTENT_NAME, props.__scopeMenu);\n\n return (\n \n \n \n {rootContext.modal ? (\n \n ) : (\n \n )}\n \n \n \n );\n }\n);\n\n/* ---------------------------------------------------------------------------------------------- */\n\ntype MenuRootContentTypeElement = MenuContentImplElement;\ninterface MenuRootContentTypeProps\n extends Omit {}\n\nconst MenuRootContentModal = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const context = useMenuContext(CONTENT_NAME, props.__scopeMenu);\n const ref = React.useRef(null);\n const composedRefs = useComposedRefs(forwardedRef, ref);\n\n // Hide everything from ARIA except the `MenuContent`\n React.useEffect(() => {\n const content = ref.current;\n if (content) return hideOthers(content);\n }, []);\n\n return (\n event.preventDefault(),\n { checkForDefaultPrevented: false }\n )}\n onDismiss={() => context.onOpenChange(false)}\n />\n );\n }\n);\n\nconst MenuRootContentNonModal = React.forwardRef<\n MenuRootContentTypeElement,\n MenuRootContentTypeProps\n>((props: ScopedProps, forwardedRef) => {\n const context = useMenuContext(CONTENT_NAME, props.__scopeMenu);\n return (\n context.onOpenChange(false)}\n />\n );\n});\n\n/* ---------------------------------------------------------------------------------------------- */\n\ntype MenuContentImplElement = React.ComponentRef;\ntype FocusScopeProps = React.ComponentPropsWithoutRef;\ntype DismissableLayerProps = React.ComponentPropsWithoutRef;\ntype RovingFocusGroupProps = React.ComponentPropsWithoutRef;\ntype PopperContentProps = React.ComponentPropsWithoutRef;\ntype MenuContentImplPrivateProps = {\n onOpenAutoFocus?: FocusScopeProps['onMountAutoFocus'];\n onDismiss?: DismissableLayerProps['onDismiss'];\n disableOutsidePointerEvents?: DismissableLayerProps['disableOutsidePointerEvents'];\n\n /**\n * Whether scrolling outside the `MenuContent` should be prevented\n * (default: `false`)\n */\n disableOutsideScroll?: boolean;\n\n /**\n * Whether focus should be trapped within the `MenuContent`\n * (default: false)\n */\n trapFocus?: FocusScopeProps['trapped'];\n};\ninterface MenuContentImplProps\n extends MenuContentImplPrivateProps,\n Omit {\n /**\n * Event handler called when auto-focusing on close.\n * Can be prevented.\n */\n onCloseAutoFocus?: FocusScopeProps['onUnmountAutoFocus'];\n\n /**\n * Whether keyboard navigation should loop around\n * @defaultValue false\n */\n loop?: RovingFocusGroupProps['loop'];\n\n onEntryFocus?: RovingFocusGroupProps['onEntryFocus'];\n onEscapeKeyDown?: DismissableLayerProps['onEscapeKeyDown'];\n onPointerDownOutside?: DismissableLayerProps['onPointerDownOutside'];\n onFocusOutside?: DismissableLayerProps['onFocusOutside'];\n onInteractOutside?: DismissableLayerProps['onInteractOutside'];\n}\n\nconst Slot = createSlot('MenuContent.ScrollLock');\n\nconst MenuContentImpl = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const {\n __scopeMenu,\n loop = false,\n trapFocus,\n onOpenAutoFocus,\n onCloseAutoFocus,\n disableOutsidePointerEvents,\n onEntryFocus,\n onEscapeKeyDown,\n onPointerDownOutside,\n onFocusOutside,\n onInteractOutside,\n onDismiss,\n disableOutsideScroll,\n ...contentProps\n } = props;\n const context = useMenuContext(CONTENT_NAME, __scopeMenu);\n const rootContext = useMenuRootContext(CONTENT_NAME, __scopeMenu);\n const popperScope = usePopperScope(__scopeMenu);\n const rovingFocusGroupScope = useRovingFocusGroupScope(__scopeMenu);\n const getItems = useCollection(__scopeMenu);\n const [currentItemId, setCurrentItemId] = React.useState(null);\n const contentRef = React.useRef(null);\n const composedRefs = useComposedRefs(forwardedRef, contentRef, context.onContentChange);\n const timerRef = React.useRef(0);\n const searchRef = React.useRef('');\n const pointerGraceTimerRef = React.useRef(0);\n const pointerGraceIntentRef = React.useRef(null);\n const pointerDirRef = React.useRef('right');\n const lastPointerXRef = React.useRef(0);\n\n const ScrollLockWrapper = disableOutsideScroll ? RemoveScroll : React.Fragment;\n const scrollLockWrapperProps = disableOutsideScroll\n ? { as: Slot, allowPinchZoom: true }\n : undefined;\n\n const handleTypeaheadSearch = (key: string) => {\n const search = searchRef.current + key;\n const items = getItems().filter((item) => !item.disabled);\n const currentItem = document.activeElement;\n const currentMatch = items.find((item) => item.ref.current === currentItem)?.textValue;\n const values = items.map((item) => item.textValue);\n const nextMatch = getNextMatch(values, search, currentMatch);\n const newItem = items.find((item) => item.textValue === nextMatch)?.ref.current;\n\n // Reset `searchRef` 1 second after it was last updated\n (function updateSearch(value: string) {\n searchRef.current = value;\n window.clearTimeout(timerRef.current);\n if (value !== '') timerRef.current = window.setTimeout(() => updateSearch(''), 1000);\n })(search);\n\n if (newItem) {\n /**\n * Imperative focus during keydown is risky so we prevent React's batching updates\n * to avoid potential bugs. See: https://github.com/facebook/react/issues/20332\n */\n setTimeout(() => (newItem as HTMLElement).focus());\n }\n };\n\n React.useEffect(() => {\n return () => window.clearTimeout(timerRef.current);\n }, []);\n\n // Make sure the whole tree has focus guards as our `MenuContent` may be\n // the last element in the DOM (because of the `Portal`)\n useFocusGuards();\n\n const isPointerMovingToSubmenu = React.useCallback((event: React.PointerEvent) => {\n const isMovingTowards = pointerDirRef.current === pointerGraceIntentRef.current?.side;\n return isMovingTowards && isPointerInGraceArea(event, pointerGraceIntentRef.current?.area);\n }, []);\n\n return (\n {\n if (isPointerMovingToSubmenu(event)) event.preventDefault();\n },\n [isPointerMovingToSubmenu]\n )}\n onItemLeave={React.useCallback(\n (event) => {\n if (isPointerMovingToSubmenu(event)) return;\n contentRef.current?.focus();\n setCurrentItemId(null);\n },\n [isPointerMovingToSubmenu]\n )}\n onTriggerLeave={React.useCallback(\n (event) => {\n if (isPointerMovingToSubmenu(event)) event.preventDefault();\n },\n [isPointerMovingToSubmenu]\n )}\n pointerGraceTimerRef={pointerGraceTimerRef}\n onPointerGraceIntentChange={React.useCallback((intent) => {\n pointerGraceIntentRef.current = intent;\n }, [])}\n >\n \n {\n // when opening, explicitly focus the content area only and leave\n // `onEntryFocus` in control of focusing first item\n event.preventDefault();\n contentRef.current?.focus({ preventScroll: true });\n })}\n onUnmountAutoFocus={onCloseAutoFocus}\n >\n \n {\n // only focus first item when using keyboard\n if (!rootContext.isUsingKeyboardRef.current) event.preventDefault();\n })}\n preventScrollOnEntryFocus\n >\n {\n // submenu key events bubble through portals. We only care about keys in this menu.\n const target = event.target as HTMLElement;\n const isKeyDownInside =\n target.closest('[data-radix-menu-content]') === event.currentTarget;\n const isModifierKey = event.ctrlKey || event.altKey || event.metaKey;\n const isCharacterKey = event.key.length === 1;\n if (isKeyDownInside) {\n // menus should not be navigated using tab key so we prevent it\n if (event.key === 'Tab') event.preventDefault();\n if (!isModifierKey && isCharacterKey) handleTypeaheadSearch(event.key);\n }\n // focus first/last item based on key pressed\n const content = contentRef.current;\n if (event.target !== content) return;\n if (!FIRST_LAST_KEYS.includes(event.key)) return;\n event.preventDefault();\n const items = getItems().filter((item) => !item.disabled);\n const candidateNodes = items.map((item) => item.ref.current!);\n if (LAST_KEYS.includes(event.key)) candidateNodes.reverse();\n focusFirst(candidateNodes);\n })}\n onBlur={composeEventHandlers(props.onBlur, (event) => {\n // clear search buffer when leaving the menu\n if (!event.currentTarget.contains(event.target)) {\n window.clearTimeout(timerRef.current);\n searchRef.current = '';\n }\n })}\n onPointerMove={composeEventHandlers(\n props.onPointerMove,\n whenMouse((event) => {\n const target = event.target as HTMLElement;\n const pointerXHasChanged = lastPointerXRef.current !== event.clientX;\n\n // We don't use `event.movementX` for this check because Safari will\n // always return `0` on a pointer event.\n if (event.currentTarget.contains(target) && pointerXHasChanged) {\n const newDir = event.clientX > lastPointerXRef.current ? 'right' : 'left';\n pointerDirRef.current = newDir;\n lastPointerXRef.current = event.clientX;\n }\n })\n )}\n />\n \n \n \n \n \n );\n }\n);\n\nMenuContent.displayName = CONTENT_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuGroup\n * -----------------------------------------------------------------------------------------------*/\n\nconst GROUP_NAME = 'MenuGroup';\n\ntype MenuGroupElement = React.ComponentRef;\ntype PrimitiveDivProps = React.ComponentPropsWithoutRef;\ninterface MenuGroupProps extends PrimitiveDivProps {}\n\nconst MenuGroup = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeMenu, ...groupProps } = props;\n return ;\n }\n);\n\nMenuGroup.displayName = GROUP_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuLabel\n * -----------------------------------------------------------------------------------------------*/\n\nconst LABEL_NAME = 'MenuLabel';\n\ntype MenuLabelElement = React.ComponentRef;\ninterface MenuLabelProps extends PrimitiveDivProps {}\n\nconst MenuLabel = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeMenu, ...labelProps } = props;\n return ;\n }\n);\n\nMenuLabel.displayName = LABEL_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuItem\n * -----------------------------------------------------------------------------------------------*/\n\nconst ITEM_NAME = 'MenuItem';\nconst ITEM_SELECT = 'menu.itemSelect';\n\ntype MenuItemElement = MenuItemImplElement;\ninterface MenuItemProps extends Omit {\n onSelect?: (event: Event) => void;\n}\n\nconst MenuItem = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { disabled = false, onSelect, ...itemProps } = props;\n const ref = React.useRef(null);\n const rootContext = useMenuRootContext(ITEM_NAME, props.__scopeMenu);\n const contentContext = useMenuContentContext(ITEM_NAME, props.__scopeMenu);\n const composedRefs = useComposedRefs(forwardedRef, ref);\n const isPointerDownRef = React.useRef(false);\n\n const handleSelect = () => {\n const menuItem = ref.current;\n if (!disabled && menuItem) {\n const itemSelectEvent = new CustomEvent(ITEM_SELECT, { bubbles: true, cancelable: true });\n menuItem.addEventListener(ITEM_SELECT, (event) => onSelect?.(event), { once: true });\n dispatchDiscreteCustomEvent(menuItem, itemSelectEvent);\n if (itemSelectEvent.defaultPrevented) {\n isPointerDownRef.current = false;\n } else {\n rootContext.onClose();\n }\n }\n };\n\n return (\n {\n props.onPointerDown?.(event);\n isPointerDownRef.current = true;\n }}\n onPointerUp={composeEventHandlers(props.onPointerUp, (event) => {\n // Pointer down can move to a different menu item which should activate it on pointer up.\n // We dispatch a click for selection to allow composition with click based triggers and to\n // prevent Firefox from getting stuck in text selection mode when the menu closes.\n if (!isPointerDownRef.current) event.currentTarget?.click();\n })}\n onKeyDown={composeEventHandlers(props.onKeyDown, (event) => {\n const isTypingAhead = contentContext.searchRef.current !== '';\n if (disabled || (isTypingAhead && event.key === ' ')) return;\n if (SELECTION_KEYS.includes(event.key)) {\n event.currentTarget.click();\n /**\n * We prevent default browser behaviour for selection keys as they should trigger\n * a selection only:\n * - prevents space from scrolling the page.\n * - if keydown causes focus to move, prevents keydown from firing on the new target.\n */\n event.preventDefault();\n }\n })}\n />\n );\n }\n);\n\nMenuItem.displayName = ITEM_NAME;\n\n/* ---------------------------------------------------------------------------------------------- */\n\ntype MenuItemImplElement = React.ComponentRef;\ninterface MenuItemImplProps extends PrimitiveDivProps {\n disabled?: boolean;\n textValue?: string;\n}\n\nconst MenuItemImpl = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeMenu, disabled = false, textValue, ...itemProps } = props;\n const contentContext = useMenuContentContext(ITEM_NAME, __scopeMenu);\n const rovingFocusGroupScope = useRovingFocusGroupScope(__scopeMenu);\n const ref = React.useRef(null);\n const composedRefs = useComposedRefs(forwardedRef, ref);\n const [isFocused, setIsFocused] = React.useState(false);\n\n // get the item's `.textContent` as default strategy for typeahead `textValue`\n const [textContent, setTextContent] = React.useState('');\n React.useEffect(() => {\n const menuItem = ref.current;\n if (menuItem) {\n setTextContent((menuItem.textContent ?? '').trim());\n }\n }, [itemProps.children]);\n\n return (\n \n \n {\n if (disabled) {\n contentContext.onItemLeave(event);\n } else {\n contentContext.onItemEnter(event);\n if (!event.defaultPrevented) {\n const item = event.currentTarget;\n item.focus({ preventScroll: true });\n }\n }\n })\n )}\n onPointerLeave={composeEventHandlers(\n props.onPointerLeave,\n whenMouse((event) => contentContext.onItemLeave(event))\n )}\n onFocus={composeEventHandlers(props.onFocus, () => setIsFocused(true))}\n onBlur={composeEventHandlers(props.onBlur, () => setIsFocused(false))}\n />\n \n \n );\n }\n);\n\n/* -------------------------------------------------------------------------------------------------\n * MenuCheckboxItem\n * -----------------------------------------------------------------------------------------------*/\n\nconst CHECKBOX_ITEM_NAME = 'MenuCheckboxItem';\n\ntype MenuCheckboxItemElement = MenuItemElement;\n\ntype CheckedState = boolean | 'indeterminate';\n\ninterface MenuCheckboxItemProps extends MenuItemProps {\n checked?: CheckedState;\n // `onCheckedChange` can never be called with `\"indeterminate\"` from the inside\n onCheckedChange?: (checked: boolean) => void;\n}\n\nconst MenuCheckboxItem = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { checked = false, onCheckedChange, ...checkboxItemProps } = props;\n return (\n \n onCheckedChange?.(isIndeterminate(checked) ? true : !checked),\n { checkForDefaultPrevented: false }\n )}\n />\n \n );\n }\n);\n\nMenuCheckboxItem.displayName = CHECKBOX_ITEM_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuRadioGroup\n * -----------------------------------------------------------------------------------------------*/\n\nconst RADIO_GROUP_NAME = 'MenuRadioGroup';\n\nconst [RadioGroupProvider, useRadioGroupContext] = createMenuContext(\n RADIO_GROUP_NAME,\n { value: undefined, onValueChange: () => {} }\n);\n\ntype MenuRadioGroupElement = React.ComponentRef;\ninterface MenuRadioGroupProps extends MenuGroupProps {\n value?: string;\n onValueChange?: (value: string) => void;\n}\n\nconst MenuRadioGroup = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { value, onValueChange, ...groupProps } = props;\n const handleValueChange = useCallbackRef(onValueChange);\n return (\n \n \n \n );\n }\n);\n\nMenuRadioGroup.displayName = RADIO_GROUP_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuRadioItem\n * -----------------------------------------------------------------------------------------------*/\n\nconst RADIO_ITEM_NAME = 'MenuRadioItem';\n\ntype MenuRadioItemElement = React.ComponentRef;\ninterface MenuRadioItemProps extends MenuItemProps {\n value: string;\n}\n\nconst MenuRadioItem = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { value, ...radioItemProps } = props;\n const context = useRadioGroupContext(RADIO_ITEM_NAME, props.__scopeMenu);\n const checked = value === context.value;\n return (\n \n context.onValueChange?.(value),\n { checkForDefaultPrevented: false }\n )}\n />\n \n );\n }\n);\n\nMenuRadioItem.displayName = RADIO_ITEM_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuItemIndicator\n * -----------------------------------------------------------------------------------------------*/\n\nconst ITEM_INDICATOR_NAME = 'MenuItemIndicator';\n\ntype CheckboxContextValue = { checked: CheckedState };\n\nconst [ItemIndicatorProvider, useItemIndicatorContext] = createMenuContext(\n ITEM_INDICATOR_NAME,\n { checked: false }\n);\n\ntype MenuItemIndicatorElement = React.ComponentRef;\ntype PrimitiveSpanProps = React.ComponentPropsWithoutRef;\ninterface MenuItemIndicatorProps extends PrimitiveSpanProps {\n /**\n * Used to force mounting when more control is needed. Useful when\n * controlling animation with React animation libraries.\n */\n forceMount?: true;\n}\n\nconst MenuItemIndicator = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeMenu, forceMount, ...itemIndicatorProps } = props;\n const indicatorContext = useItemIndicatorContext(ITEM_INDICATOR_NAME, __scopeMenu);\n return (\n \n \n \n );\n }\n);\n\nMenuItemIndicator.displayName = ITEM_INDICATOR_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuSeparator\n * -----------------------------------------------------------------------------------------------*/\n\nconst SEPARATOR_NAME = 'MenuSeparator';\n\ntype MenuSeparatorElement = React.ComponentRef;\ninterface MenuSeparatorProps extends PrimitiveDivProps {}\n\nconst MenuSeparator = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeMenu, ...separatorProps } = props;\n return (\n \n );\n }\n);\n\nMenuSeparator.displayName = SEPARATOR_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuArrow\n * -----------------------------------------------------------------------------------------------*/\n\nconst ARROW_NAME = 'MenuArrow';\n\ntype MenuArrowElement = React.ComponentRef;\ntype PopperArrowProps = React.ComponentPropsWithoutRef;\ninterface MenuArrowProps extends PopperArrowProps {}\n\nconst MenuArrow = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopeMenu, ...arrowProps } = props;\n const popperScope = usePopperScope(__scopeMenu);\n return ;\n }\n);\n\nMenuArrow.displayName = ARROW_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuSub\n * -----------------------------------------------------------------------------------------------*/\n\nconst SUB_NAME = 'MenuSub';\n\ntype MenuSubContextValue = {\n contentId: string;\n triggerId: string;\n trigger: MenuSubTriggerElement | null;\n onTriggerChange(trigger: MenuSubTriggerElement | null): void;\n};\n\nconst [MenuSubProvider, useMenuSubContext] = createMenuContext(SUB_NAME);\n\ninterface MenuSubProps {\n children?: React.ReactNode;\n open?: boolean;\n onOpenChange?(open: boolean): void;\n}\n\nconst MenuSub: React.FC = (props: ScopedProps) => {\n const { __scopeMenu, children, open = false, onOpenChange } = props;\n const parentMenuContext = useMenuContext(SUB_NAME, __scopeMenu);\n const popperScope = usePopperScope(__scopeMenu);\n const [trigger, setTrigger] = React.useState(null);\n const [content, setContent] = React.useState(null);\n const handleOpenChange = useCallbackRef(onOpenChange);\n\n // Prevent the parent menu from reopening with open submenus.\n React.useEffect(() => {\n if (parentMenuContext.open === false) handleOpenChange(false);\n return () => handleOpenChange(false);\n }, [parentMenuContext.open, handleOpenChange]);\n\n return (\n \n \n \n {children}\n \n \n \n );\n};\n\nMenuSub.displayName = SUB_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuSubTrigger\n * -----------------------------------------------------------------------------------------------*/\n\nconst SUB_TRIGGER_NAME = 'MenuSubTrigger';\n\ntype MenuSubTriggerElement = MenuItemImplElement;\ninterface MenuSubTriggerProps extends MenuItemImplProps {}\n\nconst MenuSubTrigger = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const context = useMenuContext(SUB_TRIGGER_NAME, props.__scopeMenu);\n const rootContext = useMenuRootContext(SUB_TRIGGER_NAME, props.__scopeMenu);\n const subContext = useMenuSubContext(SUB_TRIGGER_NAME, props.__scopeMenu);\n const contentContext = useMenuContentContext(SUB_TRIGGER_NAME, props.__scopeMenu);\n const openTimerRef = React.useRef(null);\n const { pointerGraceTimerRef, onPointerGraceIntentChange } = contentContext;\n const scope = { __scopeMenu: props.__scopeMenu };\n\n const clearOpenTimer = React.useCallback(() => {\n if (openTimerRef.current) window.clearTimeout(openTimerRef.current);\n openTimerRef.current = null;\n }, []);\n\n React.useEffect(() => clearOpenTimer, [clearOpenTimer]);\n\n React.useEffect(() => {\n const pointerGraceTimer = pointerGraceTimerRef.current;\n return () => {\n window.clearTimeout(pointerGraceTimer);\n onPointerGraceIntentChange(null);\n };\n }, [pointerGraceTimerRef, onPointerGraceIntentChange]);\n\n return (\n \n {\n props.onClick?.(event);\n if (props.disabled || event.defaultPrevented) return;\n /**\n * We manually focus because iOS Safari doesn't always focus on click (e.g. buttons)\n * and we rely heavily on `onFocusOutside` for submenus to close when switching\n * between separate submenus.\n */\n event.currentTarget.focus();\n if (!context.open) context.onOpenChange(true);\n }}\n onPointerMove={composeEventHandlers(\n props.onPointerMove,\n whenMouse((event) => {\n contentContext.onItemEnter(event);\n if (event.defaultPrevented) return;\n if (!props.disabled && !context.open && !openTimerRef.current) {\n contentContext.onPointerGraceIntentChange(null);\n openTimerRef.current = window.setTimeout(() => {\n context.onOpenChange(true);\n clearOpenTimer();\n }, 100);\n }\n })\n )}\n onPointerLeave={composeEventHandlers(\n props.onPointerLeave,\n whenMouse((event) => {\n clearOpenTimer();\n\n const contentRect = context.content?.getBoundingClientRect();\n if (contentRect) {\n // TODO: make sure to update this when we change positioning logic\n const side = context.content?.dataset.side as Side;\n const rightSide = side === 'right';\n const bleed = rightSide ? -5 : +5;\n const contentNearEdge = contentRect[rightSide ? 'left' : 'right'];\n const contentFarEdge = contentRect[rightSide ? 'right' : 'left'];\n\n contentContext.onPointerGraceIntentChange({\n area: [\n // Apply a bleed on clientX to ensure that our exit point is\n // consistently within polygon bounds\n { x: event.clientX + bleed, y: event.clientY },\n { x: contentNearEdge, y: contentRect.top },\n { x: contentFarEdge, y: contentRect.top },\n { x: contentFarEdge, y: contentRect.bottom },\n { x: contentNearEdge, y: contentRect.bottom },\n ],\n side,\n });\n\n window.clearTimeout(pointerGraceTimerRef.current);\n pointerGraceTimerRef.current = window.setTimeout(\n () => contentContext.onPointerGraceIntentChange(null),\n 300\n );\n } else {\n contentContext.onTriggerLeave(event);\n if (event.defaultPrevented) return;\n\n // There's 100ms where the user may leave an item before the submenu was opened.\n contentContext.onPointerGraceIntentChange(null);\n }\n })\n )}\n onKeyDown={composeEventHandlers(props.onKeyDown, (event) => {\n const isTypingAhead = contentContext.searchRef.current !== '';\n if (props.disabled || (isTypingAhead && event.key === ' ')) return;\n if (SUB_OPEN_KEYS[rootContext.dir].includes(event.key)) {\n context.onOpenChange(true);\n // The trigger may hold focus if opened via pointer interaction\n // so we ensure content is given focus again when switching to keyboard.\n context.content?.focus();\n // prevent window from scrolling\n event.preventDefault();\n }\n })}\n />\n \n );\n }\n);\n\nMenuSubTrigger.displayName = SUB_TRIGGER_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * MenuSubContent\n * -----------------------------------------------------------------------------------------------*/\n\nconst SUB_CONTENT_NAME = 'MenuSubContent';\n\ntype MenuSubContentElement = MenuContentImplElement;\ninterface MenuSubContentProps\n extends Omit<\n MenuContentImplProps,\n keyof MenuContentImplPrivateProps | 'onCloseAutoFocus' | 'onEntryFocus' | 'side' | 'align'\n > {\n /**\n * Used to force mounting when more control is needed. Useful when\n * controlling animation with React animation libraries.\n */\n forceMount?: true;\n}\n\nconst MenuSubContent = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const portalContext = usePortalContext(CONTENT_NAME, props.__scopeMenu);\n const { forceMount = portalContext.forceMount, ...subContentProps } = props;\n const context = useMenuContext(CONTENT_NAME, props.__scopeMenu);\n const rootContext = useMenuRootContext(CONTENT_NAME, props.__scopeMenu);\n const subContext = useMenuSubContext(SUB_CONTENT_NAME, props.__scopeMenu);\n const ref = React.useRef(null);\n const composedRefs = useComposedRefs(forwardedRef, ref);\n return (\n \n \n \n {\n // when opening a submenu, focus content for keyboard users only\n if (rootContext.isUsingKeyboardRef.current) ref.current?.focus();\n event.preventDefault();\n }}\n // The menu might close because of focusing another menu item in the parent menu. We\n // don't want it to refocus the trigger in that case so we handle trigger focus ourselves.\n onCloseAutoFocus={(event) => event.preventDefault()}\n onFocusOutside={composeEventHandlers(props.onFocusOutside, (event) => {\n // We prevent closing when the trigger is focused to avoid triggering a re-open animation\n // on pointer interaction.\n if (event.target !== subContext.trigger) context.onOpenChange(false);\n })}\n onEscapeKeyDown={composeEventHandlers(props.onEscapeKeyDown, (event) => {\n rootContext.onClose();\n // ensure pressing escape in submenu doesn't escape full screen mode\n event.preventDefault();\n })}\n onKeyDown={composeEventHandlers(props.onKeyDown, (event) => {\n // Submenu key events bubble through portals. We only care about keys in this menu.\n const isKeyDownInside = event.currentTarget.contains(event.target as HTMLElement);\n const isCloseKey = SUB_CLOSE_KEYS[rootContext.dir].includes(event.key);\n if (isKeyDownInside && isCloseKey) {\n context.onOpenChange(false);\n // We focus manually because we prevented it in `onCloseAutoFocus`\n subContext.trigger?.focus();\n // prevent window from scrolling\n event.preventDefault();\n }\n })}\n />\n \n \n \n );\n }\n);\n\nMenuSubContent.displayName = SUB_CONTENT_NAME;\n\n/* -----------------------------------------------------------------------------------------------*/\n\nfunction getOpenState(open: boolean) {\n return open ? 'open' : 'closed';\n}\n\nfunction isIndeterminate(checked?: CheckedState): checked is 'indeterminate' {\n return checked === 'indeterminate';\n}\n\nfunction getCheckedState(checked: CheckedState) {\n return isIndeterminate(checked) ? 'indeterminate' : checked ? 'checked' : 'unchecked';\n}\n\nfunction focusFirst(candidates: HTMLElement[]) {\n const PREVIOUSLY_FOCUSED_ELEMENT = document.activeElement;\n for (const candidate of candidates) {\n // if focus is already where we want to go, we don't want to keep going through the candidates\n if (candidate === PREVIOUSLY_FOCUSED_ELEMENT) return;\n candidate.focus();\n if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT) return;\n }\n}\n\n/**\n * Wraps an array around itself at a given start index\n * Example: `wrapArray(['a', 'b', 'c', 'd'], 2) === ['c', 'd', 'a', 'b']`\n */\nfunction wrapArray(array: T[], startIndex: number) {\n return array.map((_, index) => array[(startIndex + index) % array.length]!);\n}\n\n/**\n * This is the \"meat\" of the typeahead matching logic. It takes in all the values,\n * the search and the current match, and returns the next match (or `undefined`).\n *\n * We normalize the search because if a user has repeatedly pressed a character,\n * we want the exact same behavior as if we only had that one character\n * (ie. cycle through options starting with that character)\n *\n * We also reorder the values by wrapping the array around the current match.\n * This is so we always look forward from the current match, and picking the first\n * match will always be the correct one.\n *\n * Finally, if the normalized search is exactly one character, we exclude the\n * current match from the values because otherwise it would be the first to match always\n * and focus would never move. This is as opposed to the regular case, where we\n * don't want focus to move if the current match still matches.\n */\nfunction getNextMatch(values: string[], search: string, currentMatch?: string) {\n const isRepeated = search.length > 1 && Array.from(search).every((char) => char === search[0]);\n const normalizedSearch = isRepeated ? search[0]! : search;\n const currentMatchIndex = currentMatch ? values.indexOf(currentMatch) : -1;\n let wrappedValues = wrapArray(values, Math.max(currentMatchIndex, 0));\n const excludeCurrentMatch = normalizedSearch.length === 1;\n if (excludeCurrentMatch) wrappedValues = wrappedValues.filter((v) => v !== currentMatch);\n const nextMatch = wrappedValues.find((value) =>\n value.toLowerCase().startsWith(normalizedSearch.toLowerCase())\n );\n return nextMatch !== currentMatch ? nextMatch : undefined;\n}\n\ntype Point = { x: number; y: number };\ntype Polygon = Point[];\ntype Side = 'left' | 'right';\ntype GraceIntent = { area: Polygon; side: Side };\n\n// Determine if a point is inside of a polygon.\n// Based on https://github.com/substack/point-in-polygon\nfunction isPointInPolygon(point: Point, polygon: Polygon) {\n const { x, y } = point;\n let inside = false;\n for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {\n const ii = polygon[i]!;\n const jj = polygon[j]!;\n const xi = ii.x;\n const yi = ii.y;\n const xj = jj.x;\n const yj = jj.y;\n\n // prettier-ignore\n const intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);\n if (intersect) inside = !inside;\n }\n\n return inside;\n}\n\nfunction isPointerInGraceArea(event: React.PointerEvent, area?: Polygon) {\n if (!area) return false;\n const cursorPos = { x: event.clientX, y: event.clientY };\n return isPointInPolygon(cursorPos, area);\n}\n\nfunction whenMouse(handler: React.PointerEventHandler): React.PointerEventHandler {\n return (event) => (event.pointerType === 'mouse' ? handler(event) : undefined);\n}\n\nconst Root = Menu;\nconst Anchor = MenuAnchor;\nconst Portal = MenuPortal;\nconst Content = MenuContent;\nconst Group = MenuGroup;\nconst Label = MenuLabel;\nconst Item = MenuItem;\nconst CheckboxItem = MenuCheckboxItem;\nconst RadioGroup = MenuRadioGroup;\nconst RadioItem = MenuRadioItem;\nconst ItemIndicator = MenuItemIndicator;\nconst Separator = MenuSeparator;\nconst Arrow = MenuArrow;\nconst Sub = MenuSub;\nconst SubTrigger = MenuSubTrigger;\nconst SubContent = MenuSubContent;\n\nexport {\n createMenuScope,\n //\n Menu,\n MenuAnchor,\n MenuPortal,\n MenuContent,\n MenuGroup,\n MenuLabel,\n MenuItem,\n MenuCheckboxItem,\n MenuRadioGroup,\n MenuRadioItem,\n MenuItemIndicator,\n MenuSeparator,\n MenuArrow,\n MenuSub,\n MenuSubTrigger,\n MenuSubContent,\n //\n Root,\n Anchor,\n Portal,\n Content,\n Group,\n Label,\n Item,\n CheckboxItem,\n RadioGroup,\n RadioItem,\n ItemIndicator,\n Separator,\n Arrow,\n Sub,\n SubTrigger,\n SubContent,\n};\nexport type {\n MenuProps,\n MenuAnchorProps,\n MenuPortalProps,\n MenuContentProps,\n MenuGroupProps,\n MenuLabelProps,\n MenuItemProps,\n MenuCheckboxItemProps,\n MenuRadioGroupProps,\n MenuRadioItemProps,\n MenuItemIndicatorProps,\n MenuSeparatorProps,\n MenuArrowProps,\n MenuSubProps,\n MenuSubTriggerProps,\n MenuSubContentProps,\n};\n", "import React from 'react';\nimport { createContextScope } from '@radix-ui/react-context';\nimport { useComposedRefs } from '@radix-ui/react-compose-refs';\nimport { createSlot, type Slot } from '@radix-ui/react-slot';\n\ntype SlotProps = React.ComponentPropsWithoutRef;\ntype CollectionElement = HTMLElement;\ninterface CollectionProps extends SlotProps {\n scope: any;\n}\n\n// We have resorted to returning slots directly rather than exposing primitives that can then\n// be slotted like `\u2026`.\n// This is because we encountered issues with generic types that cannot be statically analysed\n// due to creating them dynamically via createCollection.\n\nfunction createCollection(name: string) {\n /* -----------------------------------------------------------------------------------------------\n * CollectionProvider\n * ---------------------------------------------------------------------------------------------*/\n\n const PROVIDER_NAME = name + 'CollectionProvider';\n const [createCollectionContext, createCollectionScope] = createContextScope(PROVIDER_NAME);\n\n type ContextValue = {\n collectionRef: React.RefObject;\n itemMap: Map<\n React.RefObject,\n { ref: React.RefObject } & ItemData\n >;\n };\n\n const [CollectionProviderImpl, useCollectionContext] = createCollectionContext(\n PROVIDER_NAME,\n { collectionRef: { current: null }, itemMap: new Map() }\n );\n\n const CollectionProvider: React.FC<{ children?: React.ReactNode; scope: any }> = (props) => {\n const { scope, children } = props;\n const ref = React.useRef(null);\n const itemMap = React.useRef(new Map()).current;\n return (\n \n {children}\n \n );\n };\n\n CollectionProvider.displayName = PROVIDER_NAME;\n\n /* -----------------------------------------------------------------------------------------------\n * CollectionSlot\n * ---------------------------------------------------------------------------------------------*/\n\n const COLLECTION_SLOT_NAME = name + 'CollectionSlot';\n\n const CollectionSlotImpl = createSlot(COLLECTION_SLOT_NAME);\n const CollectionSlot = React.forwardRef(\n (props, forwardedRef) => {\n const { scope, children } = props;\n const context = useCollectionContext(COLLECTION_SLOT_NAME, scope);\n const composedRefs = useComposedRefs(forwardedRef, context.collectionRef);\n return {children};\n }\n );\n\n CollectionSlot.displayName = COLLECTION_SLOT_NAME;\n\n /* -----------------------------------------------------------------------------------------------\n * CollectionItem\n * ---------------------------------------------------------------------------------------------*/\n\n const ITEM_SLOT_NAME = name + 'CollectionItemSlot';\n const ITEM_DATA_ATTR = 'data-radix-collection-item';\n\n type CollectionItemSlotProps = ItemData & {\n children: React.ReactNode;\n scope: any;\n };\n\n const CollectionItemSlotImpl = createSlot(ITEM_SLOT_NAME);\n const CollectionItemSlot = React.forwardRef(\n (props, forwardedRef) => {\n const { scope, children, ...itemData } = props;\n const ref = React.useRef(null);\n const composedRefs = useComposedRefs(forwardedRef, ref);\n const context = useCollectionContext(ITEM_SLOT_NAME, scope);\n\n React.useEffect(() => {\n context.itemMap.set(ref, { ref, ...(itemData as unknown as ItemData) });\n return () => void context.itemMap.delete(ref);\n });\n\n return (\n \n {children}\n \n );\n }\n );\n\n CollectionItemSlot.displayName = ITEM_SLOT_NAME;\n\n /* -----------------------------------------------------------------------------------------------\n * useCollection\n * ---------------------------------------------------------------------------------------------*/\n\n function useCollection(scope: any) {\n const context = useCollectionContext(name + 'CollectionConsumer', scope);\n\n const getItems = React.useCallback(() => {\n const collectionNode = context.collectionRef.current;\n if (!collectionNode) return [];\n const orderedNodes = Array.from(collectionNode.querySelectorAll(`[${ITEM_DATA_ATTR}]`));\n const items = Array.from(context.itemMap.values());\n const orderedItems = items.sort(\n (a, b) => orderedNodes.indexOf(a.ref.current!) - orderedNodes.indexOf(b.ref.current!)\n );\n return orderedItems;\n }, [context.collectionRef, context.itemMap]);\n\n return getItems;\n }\n\n return [\n { Provider: CollectionProvider, Slot: CollectionSlot, ItemSlot: CollectionItemSlot },\n useCollection,\n createCollectionScope,\n ] as const;\n}\n\nexport { createCollection };\nexport type { CollectionProps };\n", "import React from 'react';\nimport { createContextScope } from '@radix-ui/react-context';\nimport { useComposedRefs } from '@radix-ui/react-compose-refs';\nimport { createSlot, type Slot } from '@radix-ui/react-slot';\nimport type { EntryOf } from './ordered-dictionary';\nimport { OrderedDict } from './ordered-dictionary';\n\ntype SlotProps = React.ComponentPropsWithoutRef;\ntype CollectionElement = HTMLElement;\ninterface CollectionProps extends SlotProps {\n scope: any;\n}\n\ninterface BaseItemData {\n id?: string;\n}\n\ntype ItemDataWithElement<\n ItemData extends BaseItemData,\n ItemElement extends HTMLElement,\n> = ItemData & {\n element: ItemElement;\n};\n\ntype ItemMap = OrderedDict<\n ItemElement,\n ItemDataWithElement\n>;\n\nfunction createCollection<\n ItemElement extends HTMLElement,\n ItemData extends BaseItemData = BaseItemData,\n>(name: string) {\n /* -----------------------------------------------------------------------------------------------\n * CollectionProvider\n * ---------------------------------------------------------------------------------------------*/\n\n const PROVIDER_NAME = name + 'CollectionProvider';\n const [createCollectionContext, createCollectionScope] = createContextScope(PROVIDER_NAME);\n\n type ContextValue = {\n collectionElement: CollectionElement | null;\n collectionRef: React.Ref;\n collectionRefObject: React.RefObject;\n itemMap: ItemMap;\n setItemMap: React.Dispatch>>;\n };\n\n const [CollectionContextProvider, useCollectionContext] = createCollectionContext(\n PROVIDER_NAME,\n {\n collectionElement: null,\n collectionRef: { current: null },\n collectionRefObject: { current: null },\n itemMap: new OrderedDict(),\n setItemMap: () => void 0,\n }\n );\n\n type CollectionState = [\n ItemMap: ItemMap,\n SetItemMap: React.Dispatch>>,\n ];\n\n const CollectionProvider: React.FC<{\n children?: React.ReactNode;\n scope: any;\n state?: CollectionState;\n }> = ({ state, ...props }) => {\n return state ? (\n \n ) : (\n \n );\n };\n CollectionProvider.displayName = PROVIDER_NAME;\n\n const CollectionInit: React.FC<{\n children?: React.ReactNode;\n scope: any;\n }> = (props) => {\n const state = useInitCollection();\n return ;\n };\n CollectionInit.displayName = PROVIDER_NAME + 'Init';\n\n const CollectionProviderImpl: React.FC<{\n children?: React.ReactNode;\n scope: any;\n state: CollectionState;\n }> = (props) => {\n const { scope, children, state } = props;\n const ref = React.useRef(null);\n const [collectionElement, setCollectionElement] = React.useState(\n null\n );\n const composeRefs = useComposedRefs(ref, setCollectionElement);\n const [itemMap, setItemMap] = state;\n\n React.useEffect(() => {\n if (!collectionElement) return;\n\n const observer = getChildListObserver(() => {\n // setItemMap((map) => {\n // const copy = new OrderedDict(map).toSorted(([, a], [, b]) =>\n // !a.element || !b.element ? 0 : isElementPreceding(a.element, b.element) ? -1 : 1\n // );\n // // check if the order has changed\n // let index = -1;\n // for (const entry of copy) {\n // index++;\n // const key = map.keyAt(index)!;\n // const [copyKey] = entry;\n // if (key !== copyKey) {\n // // order has changed!\n // return copy;\n // }\n // }\n // return map;\n // });\n });\n observer.observe(collectionElement, {\n childList: true,\n subtree: true,\n });\n return () => {\n observer.disconnect();\n };\n }, [collectionElement]);\n\n return (\n \n {children}\n \n );\n };\n\n CollectionProviderImpl.displayName = PROVIDER_NAME + 'Impl';\n\n /* -----------------------------------------------------------------------------------------------\n * CollectionSlot\n * ---------------------------------------------------------------------------------------------*/\n\n const COLLECTION_SLOT_NAME = name + 'CollectionSlot';\n\n const CollectionSlotImpl = createSlot(COLLECTION_SLOT_NAME);\n const CollectionSlot = React.forwardRef(\n (props, forwardedRef) => {\n const { scope, children } = props;\n const context = useCollectionContext(COLLECTION_SLOT_NAME, scope);\n const composedRefs = useComposedRefs(forwardedRef, context.collectionRef);\n return {children};\n }\n );\n\n CollectionSlot.displayName = COLLECTION_SLOT_NAME;\n\n /* -----------------------------------------------------------------------------------------------\n * CollectionItem\n * ---------------------------------------------------------------------------------------------*/\n\n const ITEM_SLOT_NAME = name + 'CollectionItemSlot';\n const ITEM_DATA_ATTR = 'data-radix-collection-item';\n\n type CollectionItemSlotProps = ItemData & {\n children: React.ReactNode;\n scope: any;\n };\n\n const CollectionItemSlotImpl = createSlot(ITEM_SLOT_NAME);\n const CollectionItemSlot = React.forwardRef(\n (props, forwardedRef) => {\n const { scope, children, ...itemData } = props;\n const ref = React.useRef(null);\n const [element, setElement] = React.useState(null);\n const composedRefs = useComposedRefs(forwardedRef, ref, setElement);\n const context = useCollectionContext(ITEM_SLOT_NAME, scope);\n\n const { setItemMap } = context;\n\n const itemDataRef = React.useRef(itemData);\n if (!shallowEqual(itemDataRef.current, itemData)) {\n itemDataRef.current = itemData;\n }\n const memoizedItemData = itemDataRef.current;\n\n React.useEffect(() => {\n const itemData = memoizedItemData;\n setItemMap((map) => {\n if (!element) {\n return map;\n }\n\n if (!map.has(element)) {\n map.set(element, { ...(itemData as unknown as ItemData), element });\n return map.toSorted(sortByDocumentPosition);\n }\n\n return map\n .set(element, { ...(itemData as unknown as ItemData), element })\n .toSorted(sortByDocumentPosition);\n });\n\n return () => {\n setItemMap((map) => {\n if (!element || !map.has(element)) {\n return map;\n }\n map.delete(element);\n return new OrderedDict(map);\n });\n };\n }, [element, memoizedItemData, setItemMap]);\n\n return (\n \n {children}\n \n );\n }\n );\n\n CollectionItemSlot.displayName = ITEM_SLOT_NAME;\n\n /* -----------------------------------------------------------------------------------------------\n * useInitCollection\n * ---------------------------------------------------------------------------------------------*/\n\n function useInitCollection() {\n return React.useState>(new OrderedDict());\n }\n\n /* -----------------------------------------------------------------------------------------------\n * useCollection\n * ---------------------------------------------------------------------------------------------*/\n\n function useCollection(scope: any) {\n const { itemMap } = useCollectionContext(name + 'CollectionConsumer', scope);\n\n return itemMap;\n }\n\n const functions = {\n createCollectionScope,\n useCollection,\n useInitCollection,\n };\n\n return [\n { Provider: CollectionProvider, Slot: CollectionSlot, ItemSlot: CollectionItemSlot },\n functions,\n ] as const;\n}\n\nexport { createCollection };\nexport type { CollectionProps };\n\nfunction shallowEqual(a: any, b: any) {\n if (a === b) return true;\n if (typeof a !== 'object' || typeof b !== 'object') return false;\n if (a == null || b == null) return false;\n const keysA = Object.keys(a);\n const keysB = Object.keys(b);\n if (keysA.length !== keysB.length) return false;\n for (const key of keysA) {\n if (!Object.prototype.hasOwnProperty.call(b, key)) return false;\n if (a[key] !== b[key]) return false;\n }\n return true;\n}\n\nfunction isElementPreceding(a: Element, b: Element) {\n return !!(b.compareDocumentPosition(a) & Node.DOCUMENT_POSITION_PRECEDING);\n}\n\nfunction sortByDocumentPosition(\n a: EntryOf>,\n b: EntryOf>\n) {\n return !a[1].element || !b[1].element\n ? 0\n : isElementPreceding(a[1].element, b[1].element)\n ? -1\n : 1;\n}\n\nfunction getChildListObserver(callback: () => void) {\n const observer = new MutationObserver((mutationsList) => {\n for (const mutation of mutationsList) {\n if (mutation.type === 'childList') {\n callback();\n return;\n }\n }\n });\n\n return observer;\n}\n", "// Not a real member because it shouldn't be accessible, but the super class\n// calls `set` which needs to read the instanciation state, so it can't be a\n// private member.\nconst __instanciated = new WeakMap, boolean>();\nexport class OrderedDict extends Map {\n #keys: K[];\n\n constructor(iterable?: Iterable | null | undefined);\n constructor(entries?: readonly (readonly [K, V])[] | null) {\n super(entries);\n this.#keys = [...super.keys()];\n __instanciated.set(this, true);\n }\n\n set(key: K, value: V) {\n if (__instanciated.get(this)) {\n if (this.has(key)) {\n this.#keys[this.#keys.indexOf(key)] = key;\n } else {\n this.#keys.push(key);\n }\n }\n super.set(key, value);\n return this;\n }\n\n insert(index: number, key: K, value: V) {\n const has = this.has(key);\n const length = this.#keys.length;\n const relativeIndex = toSafeInteger(index);\n let actualIndex = relativeIndex >= 0 ? relativeIndex : length + relativeIndex;\n const safeIndex = actualIndex < 0 || actualIndex >= length ? -1 : actualIndex;\n\n if (safeIndex === this.size || (has && safeIndex === this.size - 1) || safeIndex === -1) {\n this.set(key, value);\n return this;\n }\n\n const size = this.size + (has ? 0 : 1);\n\n // If you insert at, say, -2, without this bit you'd replace the\n // second-to-last item and push the rest up one, which means the new item is\n // 3rd to last. This isn't very intuitive; inserting at -2 is more like\n // saying \"make this item the second to last\".\n if (relativeIndex < 0) {\n actualIndex++;\n }\n\n const keys = [...this.#keys];\n let nextValue: V | undefined;\n let shouldSkip = false;\n for (let i = actualIndex; i < size; i++) {\n if (actualIndex === i) {\n let nextKey = keys[i]!;\n if (keys[i] === key) {\n nextKey = keys[i + 1]!;\n }\n if (has) {\n // delete first to ensure that the item is moved to the end\n this.delete(key);\n }\n nextValue = this.get(nextKey);\n this.set(key, value);\n } else {\n if (!shouldSkip && keys[i - 1] === key) {\n shouldSkip = true;\n }\n const currentKey = keys[shouldSkip ? i : i - 1]!;\n const currentValue = nextValue!;\n nextValue = this.get(currentKey);\n this.delete(currentKey);\n this.set(currentKey, currentValue);\n }\n }\n return this;\n }\n\n with(index: number, key: K, value: V) {\n const copy = new OrderedDict(this);\n copy.insert(index, key, value);\n return copy;\n }\n\n before(key: K) {\n const index = this.#keys.indexOf(key) - 1;\n if (index < 0) {\n return undefined;\n }\n return this.entryAt(index);\n }\n\n /**\n * Sets a new key-value pair at the position before the given key.\n */\n setBefore(key: K, newKey: K, value: V) {\n const index = this.#keys.indexOf(key);\n if (index === -1) {\n return this;\n }\n return this.insert(index, newKey, value);\n }\n\n after(key: K) {\n let index = this.#keys.indexOf(key);\n index = index === -1 || index === this.size - 1 ? -1 : index + 1;\n if (index === -1) {\n return undefined;\n }\n return this.entryAt(index);\n }\n\n /**\n * Sets a new key-value pair at the position after the given key.\n */\n setAfter(key: K, newKey: K, value: V) {\n const index = this.#keys.indexOf(key);\n if (index === -1) {\n return this;\n }\n return this.insert(index + 1, newKey, value);\n }\n\n first() {\n return this.entryAt(0);\n }\n\n last() {\n return this.entryAt(-1);\n }\n\n clear() {\n this.#keys = [];\n return super.clear();\n }\n\n delete(key: K) {\n const deleted = super.delete(key);\n if (deleted) {\n this.#keys.splice(this.#keys.indexOf(key), 1);\n }\n return deleted;\n }\n\n deleteAt(index: number) {\n const key = this.keyAt(index);\n if (key !== undefined) {\n return this.delete(key);\n }\n return false;\n }\n\n at(index: number) {\n const key = at(this.#keys, index);\n if (key !== undefined) {\n return this.get(key);\n }\n }\n\n entryAt(index: number): [K, V] | undefined {\n const key = at(this.#keys, index);\n if (key !== undefined) {\n return [key, this.get(key)!];\n }\n }\n\n indexOf(key: K) {\n return this.#keys.indexOf(key);\n }\n\n keyAt(index: number) {\n return at(this.#keys, index);\n }\n\n from(key: K, offset: number) {\n const index = this.indexOf(key);\n if (index === -1) {\n return undefined;\n }\n let dest = index + offset;\n if (dest < 0) dest = 0;\n if (dest >= this.size) dest = this.size - 1;\n return this.at(dest);\n }\n\n keyFrom(key: K, offset: number) {\n const index = this.indexOf(key);\n if (index === -1) {\n return undefined;\n }\n let dest = index + offset;\n if (dest < 0) dest = 0;\n if (dest >= this.size) dest = this.size - 1;\n return this.keyAt(dest);\n }\n\n find(\n predicate: (entry: [K, V], index: number, dictionary: OrderedDict) => boolean,\n thisArg?: any\n ) {\n let index = 0;\n for (const entry of this) {\n if (Reflect.apply(predicate, thisArg, [entry, index, this])) {\n return entry;\n }\n index++;\n }\n return undefined;\n }\n\n findIndex(\n predicate: (entry: [K, V], index: number, dictionary: OrderedDict) => boolean,\n thisArg?: any\n ) {\n let index = 0;\n for (const entry of this) {\n if (Reflect.apply(predicate, thisArg, [entry, index, this])) {\n return index;\n }\n index++;\n }\n return -1;\n }\n\n filter(\n predicate: (entry: [K, V], index: number, dict: OrderedDict) => entry is [KK, VV],\n thisArg?: any\n ): OrderedDict;\n\n filter(\n predicate: (entry: [K, V], index: number, dictionary: OrderedDict) => unknown,\n thisArg?: any\n ): OrderedDict;\n\n filter(\n predicate: (entry: [K, V], index: number, dictionary: OrderedDict) => unknown,\n thisArg?: any\n ) {\n const entries: Array<[K, V]> = [];\n let index = 0;\n for (const entry of this) {\n if (Reflect.apply(predicate, thisArg, [entry, index, this])) {\n entries.push(entry);\n }\n index++;\n }\n return new OrderedDict(entries);\n }\n\n map(\n callbackfn: (entry: [K, V], index: number, dictionary: OrderedDict) => U,\n thisArg?: any\n ): OrderedDict {\n const entries: [K, U][] = [];\n let index = 0;\n for (const entry of this) {\n entries.push([entry[0], Reflect.apply(callbackfn, thisArg, [entry, index, this])]);\n index++;\n }\n return new OrderedDict(entries);\n }\n\n reduce(\n callbackfn: (\n previousValue: [K, V],\n currentEntry: [K, V],\n currentIndex: number,\n dictionary: OrderedDict\n ) => [K, V]\n ): [K, V];\n reduce(\n callbackfn: (\n previousValue: [K, V],\n currentEntry: [K, V],\n currentIndex: number,\n dictionary: OrderedDict\n ) => [K, V],\n initialValue: [K, V]\n ): [K, V];\n reduce(\n callbackfn: (\n previousValue: U,\n currentEntry: [K, V],\n currentIndex: number,\n dictionary: OrderedDict\n ) => U,\n initialValue: U\n ): U;\n\n reduce(\n ...args: [\n (\n previousValue: U,\n currentEntry: [K, V],\n currentIndex: number,\n dictionary: OrderedDict\n ) => U,\n U?,\n ]\n ) {\n const [callbackfn, initialValue] = args;\n let index = 0;\n let accumulator = initialValue ?? this.at(0)!;\n for (const entry of this) {\n if (index === 0 && args.length === 1) {\n accumulator = entry as any;\n } else {\n accumulator = Reflect.apply(callbackfn, this, [accumulator, entry, index, this]);\n }\n index++;\n }\n return accumulator;\n }\n\n reduceRight(\n callbackfn: (\n previousValue: [K, V],\n currentEntry: [K, V],\n currentIndex: number,\n dictionary: OrderedDict\n ) => [K, V]\n ): [K, V];\n reduceRight(\n callbackfn: (\n previousValue: [K, V],\n currentEntry: [K, V],\n currentIndex: number,\n dictionary: OrderedDict\n ) => [K, V],\n initialValue: [K, V]\n ): [K, V];\n reduceRight(\n callbackfn: (\n previousValue: [K, V],\n currentValue: U,\n currentIndex: number,\n dictionary: OrderedDict\n ) => U,\n initialValue: U\n ): U;\n\n reduceRight(\n ...args: [\n (\n previousValue: U,\n currentEntry: [K, V],\n currentIndex: number,\n dictionary: OrderedDict\n ) => U,\n U?,\n ]\n ) {\n const [callbackfn, initialValue] = args;\n let accumulator = initialValue ?? this.at(-1)!;\n for (let index = this.size - 1; index >= 0; index--) {\n const entry = this.at(index)!;\n if (index === this.size - 1 && args.length === 1) {\n accumulator = entry as any;\n } else {\n accumulator = Reflect.apply(callbackfn, this, [accumulator, entry, index, this]);\n }\n }\n return accumulator;\n }\n\n toSorted(compareFn?: (a: [K, V], b: [K, V]) => number): OrderedDict {\n const entries = [...this.entries()].sort(compareFn);\n return new OrderedDict(entries);\n }\n\n toReversed(): OrderedDict {\n const reversed = new OrderedDict();\n for (let index = this.size - 1; index >= 0; index--) {\n const key = this.keyAt(index)!;\n const element = this.get(key)!;\n reversed.set(key, element);\n }\n return reversed;\n }\n\n toSpliced(start: number, deleteCount?: number): OrderedDict;\n toSpliced(start: number, deleteCount: number, ...items: [K, V][]): OrderedDict;\n\n toSpliced(...args: [start: number, deleteCount: number, ...items: [K, V][]]) {\n const entries = [...this.entries()];\n entries.splice(...args);\n return new OrderedDict(entries);\n }\n\n slice(start?: number, end?: number) {\n const result = new OrderedDict();\n let stop = this.size - 1;\n\n if (start === undefined) {\n return result;\n }\n\n if (start < 0) {\n start = start + this.size;\n }\n\n if (end !== undefined && end > 0) {\n stop = end - 1;\n }\n\n for (let index = start; index <= stop; index++) {\n const key = this.keyAt(index)!;\n const element = this.get(key)!;\n result.set(key, element);\n }\n return result;\n }\n\n every(\n predicate: (entry: [K, V], index: number, dictionary: OrderedDict) => unknown,\n thisArg?: any\n ) {\n let index = 0;\n for (const entry of this) {\n if (!Reflect.apply(predicate, thisArg, [entry, index, this])) {\n return false;\n }\n index++;\n }\n return true;\n }\n\n some(\n predicate: (entry: [K, V], index: number, dictionary: OrderedDict) => unknown,\n thisArg?: any\n ) {\n let index = 0;\n for (const entry of this) {\n if (Reflect.apply(predicate, thisArg, [entry, index, this])) {\n return true;\n }\n index++;\n }\n return false;\n }\n}\n\nexport type KeyOf> =\n D extends OrderedDict ? K : never;\nexport type ValueOf> =\n D extends OrderedDict ? V : never;\nexport type EntryOf> = [KeyOf, ValueOf];\nexport type KeyFrom> = E[0];\nexport type ValueFrom> = E[1];\n\nfunction at(array: ArrayLike, index: number): T | undefined {\n if ('at' in Array.prototype) {\n return Array.prototype.at.call(array, index);\n }\n const actualIndex = toSafeIndex(array, index);\n return actualIndex === -1 ? undefined : array[actualIndex];\n}\n\nfunction toSafeIndex(array: ArrayLike, index: number) {\n const length = array.length;\n const relativeIndex = toSafeInteger(index);\n const actualIndex = relativeIndex >= 0 ? relativeIndex : length + relativeIndex;\n return actualIndex < 0 || actualIndex >= length ? -1 : actualIndex;\n}\n\nfunction toSafeInteger(number: number) {\n // eslint-disable-next-line no-self-compare\n return number !== number || number === 0 ? 0 : Math.trunc(number);\n}\n", "import * as React from 'react';\nimport { composeRefs } from '@radix-ui/react-compose-refs';\n\n/* -------------------------------------------------------------------------------------------------\n * Slot\n * -----------------------------------------------------------------------------------------------*/\n\ninterface SlotProps extends React.HTMLAttributes {\n children?: React.ReactNode;\n}\n\n/* @__NO_SIDE_EFFECTS__ */ export function createSlot(ownerName: string) {\n const SlotClone = createSlotClone(ownerName);\n const Slot = React.forwardRef((props, forwardedRef) => {\n const { children, ...slotProps } = props;\n const childrenArray = React.Children.toArray(children);\n const slottable = childrenArray.find(isSlottable);\n\n if (slottable) {\n // the new element to render is the one passed as a child of `Slottable`\n const newElement = slottable.props.children;\n\n const newChildren = childrenArray.map((child) => {\n if (child === slottable) {\n // because the new element will be the one rendered, we are only interested\n // in grabbing its children (`newElement.props.children`)\n if (React.Children.count(newElement) > 1) return React.Children.only(null);\n return React.isValidElement(newElement)\n ? (newElement.props as { children: React.ReactNode }).children\n : null;\n } else {\n return child;\n }\n });\n\n return (\n \n {React.isValidElement(newElement)\n ? React.cloneElement(newElement, undefined, newChildren)\n : null}\n \n );\n }\n\n return (\n \n {children}\n \n );\n });\n\n Slot.displayName = `${ownerName}.Slot`;\n return Slot;\n}\n\nconst Slot = createSlot('Slot');\n\n/* -------------------------------------------------------------------------------------------------\n * SlotClone\n * -----------------------------------------------------------------------------------------------*/\n\ninterface SlotCloneProps {\n children: React.ReactNode;\n}\n\n/* @__NO_SIDE_EFFECTS__ */ function createSlotClone(ownerName: string) {\n const SlotClone = React.forwardRef((props, forwardedRef) => {\n const { children, ...slotProps } = props;\n\n if (React.isValidElement(children)) {\n const childrenRef = getElementRef(children);\n const props = mergeProps(slotProps, children.props as AnyProps);\n // do not pass ref to React.Fragment for React 19 compatibility\n if (children.type !== React.Fragment) {\n props.ref = forwardedRef ? composeRefs(forwardedRef, childrenRef) : childrenRef;\n }\n return React.cloneElement(children, props);\n }\n\n return React.Children.count(children) > 1 ? React.Children.only(null) : null;\n });\n\n SlotClone.displayName = `${ownerName}.SlotClone`;\n return SlotClone;\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Slottable\n * -----------------------------------------------------------------------------------------------*/\n\nconst SLOTTABLE_IDENTIFIER = Symbol('radix.slottable');\n\ninterface SlottableProps {\n children: React.ReactNode;\n}\n\ninterface SlottableComponent extends React.FC {\n __radixId: symbol;\n}\n\n/* @__NO_SIDE_EFFECTS__ */ export function createSlottable(ownerName: string) {\n const Slottable: SlottableComponent = ({ children }) => {\n return <>{children};\n };\n Slottable.displayName = `${ownerName}.Slottable`;\n Slottable.__radixId = SLOTTABLE_IDENTIFIER;\n return Slottable;\n}\n\nconst Slottable = createSlottable('Slottable');\n\n/* ---------------------------------------------------------------------------------------------- */\n\ntype AnyProps = Record;\n\nfunction isSlottable(\n child: React.ReactNode\n): child is React.ReactElement {\n return (\n React.isValidElement(child) &&\n typeof child.type === 'function' &&\n '__radixId' in child.type &&\n child.type.__radixId === SLOTTABLE_IDENTIFIER\n );\n}\n\nfunction mergeProps(slotProps: AnyProps, childProps: AnyProps) {\n // all child props should override\n const overrideProps = { ...childProps };\n\n for (const propName in childProps) {\n const slotPropValue = slotProps[propName];\n const childPropValue = childProps[propName];\n\n const isHandler = /^on[A-Z]/.test(propName);\n if (isHandler) {\n // if the handler exists on both, we compose them\n if (slotPropValue && childPropValue) {\n overrideProps[propName] = (...args: unknown[]) => {\n const result = childPropValue(...args);\n slotPropValue(...args);\n return result;\n };\n }\n // but if it exists only on the slot, we use only this one\n else if (slotPropValue) {\n overrideProps[propName] = slotPropValue;\n }\n }\n // if it's `style`, we merge them\n else if (propName === 'style') {\n overrideProps[propName] = { ...slotPropValue, ...childPropValue };\n } else if (propName === 'className') {\n overrideProps[propName] = [slotPropValue, childPropValue].filter(Boolean).join(' ');\n }\n }\n\n return { ...slotProps, ...overrideProps };\n}\n\n// Before React 19 accessing `element.props.ref` will throw a warning and suggest using `element.ref`\n// After React 19 accessing `element.ref` does the opposite.\n// https://github.com/facebook/react/pull/28348\n//\n// Access the ref using the method that doesn't yield a warning.\nfunction getElementRef(element: React.ReactElement) {\n // React <=18 in DEV\n let getter = Object.getOwnPropertyDescriptor(element.props, 'ref')?.get;\n let mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning;\n if (mayWarn) {\n return (element as any).ref;\n }\n\n // React 19 in DEV\n getter = Object.getOwnPropertyDescriptor(element, 'ref')?.get;\n mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning;\n if (mayWarn) {\n return (element.props as { ref?: React.Ref }).ref;\n }\n\n // Not DEV\n return (element.props as { ref?: React.Ref }).ref || (element as any).ref;\n}\n\nexport {\n Slot,\n Slottable,\n //\n Slot as Root,\n};\nexport type { SlotProps };\n", "import * as React from 'react';\n\ntype Direction = 'ltr' | 'rtl';\nconst DirectionContext = React.createContext(undefined);\n\n/* -------------------------------------------------------------------------------------------------\n * Direction\n * -----------------------------------------------------------------------------------------------*/\n\ninterface DirectionProviderProps {\n children?: React.ReactNode;\n dir: Direction;\n}\nconst DirectionProvider: React.FC = (props) => {\n const { dir, children } = props;\n return {children};\n};\n\n/* -----------------------------------------------------------------------------------------------*/\n\nfunction useDirection(localDir?: Direction) {\n const globalDir = React.useContext(DirectionContext);\n return localDir || globalDir || 'ltr';\n}\n\nconst Provider = DirectionProvider;\n\nexport {\n useDirection,\n //\n Provider,\n //\n DirectionProvider,\n};\n", "import * as React from 'react';\nimport { composeEventHandlers } from '@radix-ui/primitive';\nimport { Primitive, dispatchDiscreteCustomEvent } from '@radix-ui/react-primitive';\nimport { useComposedRefs } from '@radix-ui/react-compose-refs';\nimport { useCallbackRef } from '@radix-ui/react-use-callback-ref';\nimport { useEscapeKeydown } from '@radix-ui/react-use-escape-keydown';\n\n/* -------------------------------------------------------------------------------------------------\n * DismissableLayer\n * -----------------------------------------------------------------------------------------------*/\n\nconst DISMISSABLE_LAYER_NAME = 'DismissableLayer';\nconst CONTEXT_UPDATE = 'dismissableLayer.update';\nconst POINTER_DOWN_OUTSIDE = 'dismissableLayer.pointerDownOutside';\nconst FOCUS_OUTSIDE = 'dismissableLayer.focusOutside';\n\nlet originalBodyPointerEvents: string;\n\nconst DismissableLayerContext = React.createContext({\n layers: new Set(),\n layersWithOutsidePointerEventsDisabled: new Set(),\n branches: new Set(),\n});\n\ntype DismissableLayerElement = React.ComponentRef;\ntype PrimitiveDivProps = React.ComponentPropsWithoutRef;\ninterface DismissableLayerProps extends PrimitiveDivProps {\n /**\n * When `true`, hover/focus/click interactions will be disabled on elements outside\n * the `DismissableLayer`. Users will need to click twice on outside elements to\n * interact with them: once to close the `DismissableLayer`, and again to trigger the element.\n */\n disableOutsidePointerEvents?: boolean;\n /**\n * Event handler called when the escape key is down.\n * Can be prevented.\n */\n onEscapeKeyDown?: (event: KeyboardEvent) => void;\n /**\n * Event handler called when the a `pointerdown` event happens outside of the `DismissableLayer`.\n * Can be prevented.\n */\n onPointerDownOutside?: (event: PointerDownOutsideEvent) => void;\n /**\n * Event handler called when the focus moves outside of the `DismissableLayer`.\n * Can be prevented.\n */\n onFocusOutside?: (event: FocusOutsideEvent) => void;\n /**\n * Event handler called when an interaction happens outside the `DismissableLayer`.\n * Specifically, when a `pointerdown` event happens outside or focus moves outside of it.\n * Can be prevented.\n */\n onInteractOutside?: (event: PointerDownOutsideEvent | FocusOutsideEvent) => void;\n /**\n * Handler called when the `DismissableLayer` should be dismissed\n */\n onDismiss?: () => void;\n}\n\nconst DismissableLayer = React.forwardRef(\n (props, forwardedRef) => {\n const {\n disableOutsidePointerEvents = false,\n onEscapeKeyDown,\n onPointerDownOutside,\n onFocusOutside,\n onInteractOutside,\n onDismiss,\n ...layerProps\n } = props;\n const context = React.useContext(DismissableLayerContext);\n const [node, setNode] = React.useState(null);\n const ownerDocument = node?.ownerDocument ?? globalThis?.document;\n const [, force] = React.useState({});\n const composedRefs = useComposedRefs(forwardedRef, (node) => setNode(node));\n const layers = Array.from(context.layers);\n const [highestLayerWithOutsidePointerEventsDisabled] = [...context.layersWithOutsidePointerEventsDisabled].slice(-1); // prettier-ignore\n const highestLayerWithOutsidePointerEventsDisabledIndex = layers.indexOf(highestLayerWithOutsidePointerEventsDisabled!); // prettier-ignore\n const index = node ? layers.indexOf(node) : -1;\n const isBodyPointerEventsDisabled = context.layersWithOutsidePointerEventsDisabled.size > 0;\n const isPointerEventsEnabled = index >= highestLayerWithOutsidePointerEventsDisabledIndex;\n\n const pointerDownOutside = usePointerDownOutside((event) => {\n const target = event.target as HTMLElement;\n const isPointerDownOnBranch = [...context.branches].some((branch) => branch.contains(target));\n if (!isPointerEventsEnabled || isPointerDownOnBranch) return;\n onPointerDownOutside?.(event);\n onInteractOutside?.(event);\n if (!event.defaultPrevented) onDismiss?.();\n }, ownerDocument);\n\n const focusOutside = useFocusOutside((event) => {\n const target = event.target as HTMLElement;\n const isFocusInBranch = [...context.branches].some((branch) => branch.contains(target));\n if (isFocusInBranch) return;\n onFocusOutside?.(event);\n onInteractOutside?.(event);\n if (!event.defaultPrevented) onDismiss?.();\n }, ownerDocument);\n\n useEscapeKeydown((event) => {\n const isHighestLayer = index === context.layers.size - 1;\n if (!isHighestLayer) return;\n onEscapeKeyDown?.(event);\n if (!event.defaultPrevented && onDismiss) {\n event.preventDefault();\n onDismiss();\n }\n }, ownerDocument);\n\n React.useEffect(() => {\n if (!node) return;\n if (disableOutsidePointerEvents) {\n if (context.layersWithOutsidePointerEventsDisabled.size === 0) {\n originalBodyPointerEvents = ownerDocument.body.style.pointerEvents;\n ownerDocument.body.style.pointerEvents = 'none';\n }\n context.layersWithOutsidePointerEventsDisabled.add(node);\n }\n context.layers.add(node);\n dispatchUpdate();\n return () => {\n if (\n disableOutsidePointerEvents &&\n context.layersWithOutsidePointerEventsDisabled.size === 1\n ) {\n ownerDocument.body.style.pointerEvents = originalBodyPointerEvents;\n }\n };\n }, [node, ownerDocument, disableOutsidePointerEvents, context]);\n\n /**\n * We purposefully prevent combining this effect with the `disableOutsidePointerEvents` effect\n * because a change to `disableOutsidePointerEvents` would remove this layer from the stack\n * and add it to the end again so the layering order wouldn't be _creation order_.\n * We only want them to be removed from context stacks when unmounted.\n */\n React.useEffect(() => {\n return () => {\n if (!node) return;\n context.layers.delete(node);\n context.layersWithOutsidePointerEventsDisabled.delete(node);\n dispatchUpdate();\n };\n }, [node, context]);\n\n React.useEffect(() => {\n const handleUpdate = () => force({});\n document.addEventListener(CONTEXT_UPDATE, handleUpdate);\n return () => document.removeEventListener(CONTEXT_UPDATE, handleUpdate);\n }, []);\n\n return (\n \n );\n }\n);\n\nDismissableLayer.displayName = DISMISSABLE_LAYER_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * DismissableLayerBranch\n * -----------------------------------------------------------------------------------------------*/\n\nconst BRANCH_NAME = 'DismissableLayerBranch';\n\ntype DismissableLayerBranchElement = React.ComponentRef;\ninterface DismissableLayerBranchProps extends PrimitiveDivProps {}\n\nconst DismissableLayerBranch = React.forwardRef<\n DismissableLayerBranchElement,\n DismissableLayerBranchProps\n>((props, forwardedRef) => {\n const context = React.useContext(DismissableLayerContext);\n const ref = React.useRef(null);\n const composedRefs = useComposedRefs(forwardedRef, ref);\n\n React.useEffect(() => {\n const node = ref.current;\n if (node) {\n context.branches.add(node);\n return () => {\n context.branches.delete(node);\n };\n }\n }, [context.branches]);\n\n return ;\n});\n\nDismissableLayerBranch.displayName = BRANCH_NAME;\n\n/* -----------------------------------------------------------------------------------------------*/\n\ntype PointerDownOutsideEvent = CustomEvent<{ originalEvent: PointerEvent }>;\ntype FocusOutsideEvent = CustomEvent<{ originalEvent: FocusEvent }>;\n\n/**\n * Listens for `pointerdown` outside a react subtree. We use `pointerdown` rather than `pointerup`\n * to mimic layer dismissing behaviour present in OS.\n * Returns props to pass to the node we want to check for outside events.\n */\nfunction usePointerDownOutside(\n onPointerDownOutside?: (event: PointerDownOutsideEvent) => void,\n ownerDocument: Document = globalThis?.document\n) {\n const handlePointerDownOutside = useCallbackRef(onPointerDownOutside) as EventListener;\n const isPointerInsideReactTreeRef = React.useRef(false);\n const handleClickRef = React.useRef(() => {});\n\n React.useEffect(() => {\n const handlePointerDown = (event: PointerEvent) => {\n if (event.target && !isPointerInsideReactTreeRef.current) {\n const eventDetail = { originalEvent: event };\n\n function handleAndDispatchPointerDownOutsideEvent() {\n handleAndDispatchCustomEvent(\n POINTER_DOWN_OUTSIDE,\n handlePointerDownOutside,\n eventDetail,\n { discrete: true }\n );\n }\n\n /**\n * On touch devices, we need to wait for a click event because browsers implement\n * a ~350ms delay between the time the user stops touching the display and when the\n * browser executres events. We need to ensure we don't reactivate pointer-events within\n * this timeframe otherwise the browser may execute events that should have been prevented.\n *\n * Additionally, this also lets us deal automatically with cancellations when a click event\n * isn't raised because the page was considered scrolled/drag-scrolled, long-pressed, etc.\n *\n * This is why we also continuously remove the previous listener, because we cannot be\n * certain that it was raised, and therefore cleaned-up.\n */\n if (event.pointerType === 'touch') {\n ownerDocument.removeEventListener('click', handleClickRef.current);\n handleClickRef.current = handleAndDispatchPointerDownOutsideEvent;\n ownerDocument.addEventListener('click', handleClickRef.current, { once: true });\n } else {\n handleAndDispatchPointerDownOutsideEvent();\n }\n } else {\n // We need to remove the event listener in case the outside click has been canceled.\n // See: https://github.com/radix-ui/primitives/issues/2171\n ownerDocument.removeEventListener('click', handleClickRef.current);\n }\n isPointerInsideReactTreeRef.current = false;\n };\n /**\n * if this hook executes in a component that mounts via a `pointerdown` event, the event\n * would bubble up to the document and trigger a `pointerDownOutside` event. We avoid\n * this by delaying the event listener registration on the document.\n * This is not React specific, but rather how the DOM works, ie:\n * ```\n * button.addEventListener('pointerdown', () => {\n * console.log('I will log');\n * document.addEventListener('pointerdown', () => {\n * console.log('I will also log');\n * })\n * });\n */\n const timerId = window.setTimeout(() => {\n ownerDocument.addEventListener('pointerdown', handlePointerDown);\n }, 0);\n return () => {\n window.clearTimeout(timerId);\n ownerDocument.removeEventListener('pointerdown', handlePointerDown);\n ownerDocument.removeEventListener('click', handleClickRef.current);\n };\n }, [ownerDocument, handlePointerDownOutside]);\n\n return {\n // ensures we check React component tree (not just DOM tree)\n onPointerDownCapture: () => (isPointerInsideReactTreeRef.current = true),\n };\n}\n\n/**\n * Listens for when focus happens outside a react subtree.\n * Returns props to pass to the root (node) of the subtree we want to check.\n */\nfunction useFocusOutside(\n onFocusOutside?: (event: FocusOutsideEvent) => void,\n ownerDocument: Document = globalThis?.document\n) {\n const handleFocusOutside = useCallbackRef(onFocusOutside) as EventListener;\n const isFocusInsideReactTreeRef = React.useRef(false);\n\n React.useEffect(() => {\n const handleFocus = (event: FocusEvent) => {\n if (event.target && !isFocusInsideReactTreeRef.current) {\n const eventDetail = { originalEvent: event };\n handleAndDispatchCustomEvent(FOCUS_OUTSIDE, handleFocusOutside, eventDetail, {\n discrete: false,\n });\n }\n };\n ownerDocument.addEventListener('focusin', handleFocus);\n return () => ownerDocument.removeEventListener('focusin', handleFocus);\n }, [ownerDocument, handleFocusOutside]);\n\n return {\n onFocusCapture: () => (isFocusInsideReactTreeRef.current = true),\n onBlurCapture: () => (isFocusInsideReactTreeRef.current = false),\n };\n}\n\nfunction dispatchUpdate() {\n const event = new CustomEvent(CONTEXT_UPDATE);\n document.dispatchEvent(event);\n}\n\nfunction handleAndDispatchCustomEvent(\n name: string,\n handler: ((event: E) => void) | undefined,\n detail: { originalEvent: OriginalEvent } & (E extends CustomEvent ? D : never),\n { discrete }: { discrete: boolean }\n) {\n const target = detail.originalEvent.target;\n const event = new CustomEvent(name, { bubbles: false, cancelable: true, detail });\n if (handler) target.addEventListener(name, handler as EventListener, { once: true });\n\n if (discrete) {\n dispatchDiscreteCustomEvent(target, event);\n } else {\n target.dispatchEvent(event);\n }\n}\n\nconst Root = DismissableLayer;\nconst Branch = DismissableLayerBranch;\n\nexport {\n DismissableLayer,\n DismissableLayerBranch,\n //\n Root,\n Branch,\n};\nexport type { DismissableLayerProps };\n", "import * as React from 'react';\nimport { useCallbackRef } from '@radix-ui/react-use-callback-ref';\n\n/**\n * Listens for when the escape key is down\n */\nfunction useEscapeKeydown(\n onEscapeKeyDownProp?: (event: KeyboardEvent) => void,\n ownerDocument: Document = globalThis?.document\n) {\n const onEscapeKeyDown = useCallbackRef(onEscapeKeyDownProp);\n\n React.useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n onEscapeKeyDown(event);\n }\n };\n ownerDocument.addEventListener('keydown', handleKeyDown, { capture: true });\n return () => ownerDocument.removeEventListener('keydown', handleKeyDown, { capture: true });\n }, [onEscapeKeyDown, ownerDocument]);\n}\n\nexport { useEscapeKeydown };\n", "import * as React from 'react';\n\n/** Number of components which have requested interest to have focus guards */\nlet count = 0;\n\ninterface FocusGuardsProps {\n children?: React.ReactNode;\n}\n\nfunction FocusGuards(props: FocusGuardsProps) {\n useFocusGuards();\n return props.children;\n}\n\n/**\n * Injects a pair of focus guards at the edges of the whole DOM tree\n * to ensure `focusin` & `focusout` events can be caught consistently.\n */\nfunction useFocusGuards() {\n /* eslint-disable no-restricted-globals */\n React.useEffect(() => {\n const edgeGuards = document.querySelectorAll('[data-radix-focus-guard]');\n document.body.insertAdjacentElement('afterbegin', edgeGuards[0] ?? createFocusGuard());\n document.body.insertAdjacentElement('beforeend', edgeGuards[1] ?? createFocusGuard());\n count++;\n\n return () => {\n if (count === 1) {\n document.querySelectorAll('[data-radix-focus-guard]').forEach((node) => node.remove());\n }\n count--;\n };\n }, []);\n /* eslint-enable no-restricted-globals */\n}\n\nfunction createFocusGuard() {\n // eslint-disable-next-line no-restricted-globals\n const element = document.createElement('span');\n element.setAttribute('data-radix-focus-guard', '');\n element.tabIndex = 0;\n element.style.outline = 'none';\n element.style.opacity = '0';\n element.style.position = 'fixed';\n element.style.pointerEvents = 'none';\n return element;\n}\n\nexport {\n FocusGuards,\n //\n FocusGuards as Root,\n //\n useFocusGuards,\n};\n", "import * as React from 'react';\nimport { useComposedRefs } from '@radix-ui/react-compose-refs';\nimport { Primitive } from '@radix-ui/react-primitive';\nimport { useCallbackRef } from '@radix-ui/react-use-callback-ref';\n\nconst AUTOFOCUS_ON_MOUNT = 'focusScope.autoFocusOnMount';\nconst AUTOFOCUS_ON_UNMOUNT = 'focusScope.autoFocusOnUnmount';\nconst EVENT_OPTIONS = { bubbles: false, cancelable: true };\n\ntype FocusableTarget = HTMLElement | { focus(): void };\n\n/* -------------------------------------------------------------------------------------------------\n * FocusScope\n * -----------------------------------------------------------------------------------------------*/\n\nconst FOCUS_SCOPE_NAME = 'FocusScope';\n\ntype FocusScopeElement = React.ComponentRef;\ntype PrimitiveDivProps = React.ComponentPropsWithoutRef;\ninterface FocusScopeProps extends PrimitiveDivProps {\n /**\n * When `true`, tabbing from last item will focus first tabbable\n * and shift+tab from first item will focus last tababble.\n * @defaultValue false\n */\n loop?: boolean;\n\n /**\n * When `true`, focus cannot escape the focus scope via keyboard,\n * pointer, or a programmatic focus.\n * @defaultValue false\n */\n trapped?: boolean;\n\n /**\n * Event handler called when auto-focusing on mount.\n * Can be prevented.\n */\n onMountAutoFocus?: (event: Event) => void;\n\n /**\n * Event handler called when auto-focusing on unmount.\n * Can be prevented.\n */\n onUnmountAutoFocus?: (event: Event) => void;\n}\n\nconst FocusScope = React.forwardRef((props, forwardedRef) => {\n const {\n loop = false,\n trapped = false,\n onMountAutoFocus: onMountAutoFocusProp,\n onUnmountAutoFocus: onUnmountAutoFocusProp,\n ...scopeProps\n } = props;\n const [container, setContainer] = React.useState(null);\n const onMountAutoFocus = useCallbackRef(onMountAutoFocusProp);\n const onUnmountAutoFocus = useCallbackRef(onUnmountAutoFocusProp);\n const lastFocusedElementRef = React.useRef(null);\n const composedRefs = useComposedRefs(forwardedRef, (node) => setContainer(node));\n\n const focusScope = React.useRef({\n paused: false,\n pause() {\n this.paused = true;\n },\n resume() {\n this.paused = false;\n },\n }).current;\n\n // Takes care of trapping focus if focus is moved outside programmatically for example\n React.useEffect(() => {\n if (trapped) {\n function handleFocusIn(event: FocusEvent) {\n if (focusScope.paused || !container) return;\n const target = event.target as HTMLElement | null;\n if (container.contains(target)) {\n lastFocusedElementRef.current = target;\n } else {\n focus(lastFocusedElementRef.current, { select: true });\n }\n }\n\n function handleFocusOut(event: FocusEvent) {\n if (focusScope.paused || !container) return;\n const relatedTarget = event.relatedTarget as HTMLElement | null;\n\n // A `focusout` event with a `null` `relatedTarget` will happen in at least two cases:\n //\n // 1. When the user switches app/tabs/windows/the browser itself loses focus.\n // 2. In Google Chrome, when the focused element is removed from the DOM.\n //\n // We let the browser do its thing here because:\n //\n // 1. The browser already keeps a memory of what's focused for when the page gets refocused.\n // 2. In Google Chrome, if we try to focus the deleted focused element (as per below), it\n // throws the CPU to 100%, so we avoid doing anything for this reason here too.\n if (relatedTarget === null) return;\n\n // If the focus has moved to an actual legitimate element (`relatedTarget !== null`)\n // that is outside the container, we move focus to the last valid focused element inside.\n if (!container.contains(relatedTarget)) {\n focus(lastFocusedElementRef.current, { select: true });\n }\n }\n\n // When the focused element gets removed from the DOM, browsers move focus\n // back to the document.body. In this case, we move focus to the container\n // to keep focus trapped correctly.\n function handleMutations(mutations: MutationRecord[]) {\n const focusedElement = document.activeElement as HTMLElement | null;\n if (focusedElement !== document.body) return;\n for (const mutation of mutations) {\n if (mutation.removedNodes.length > 0) focus(container);\n }\n }\n\n document.addEventListener('focusin', handleFocusIn);\n document.addEventListener('focusout', handleFocusOut);\n const mutationObserver = new MutationObserver(handleMutations);\n if (container) mutationObserver.observe(container, { childList: true, subtree: true });\n\n return () => {\n document.removeEventListener('focusin', handleFocusIn);\n document.removeEventListener('focusout', handleFocusOut);\n mutationObserver.disconnect();\n };\n }\n }, [trapped, container, focusScope.paused]);\n\n React.useEffect(() => {\n if (container) {\n focusScopesStack.add(focusScope);\n const previouslyFocusedElement = document.activeElement as HTMLElement | null;\n const hasFocusedCandidate = container.contains(previouslyFocusedElement);\n\n if (!hasFocusedCandidate) {\n const mountEvent = new CustomEvent(AUTOFOCUS_ON_MOUNT, EVENT_OPTIONS);\n container.addEventListener(AUTOFOCUS_ON_MOUNT, onMountAutoFocus);\n container.dispatchEvent(mountEvent);\n if (!mountEvent.defaultPrevented) {\n focusFirst(removeLinks(getTabbableCandidates(container)), { select: true });\n if (document.activeElement === previouslyFocusedElement) {\n focus(container);\n }\n }\n }\n\n return () => {\n container.removeEventListener(AUTOFOCUS_ON_MOUNT, onMountAutoFocus);\n\n // We hit a react bug (fixed in v17) with focusing in unmount.\n // We need to delay the focus a little to get around it for now.\n // See: https://github.com/facebook/react/issues/17894\n setTimeout(() => {\n const unmountEvent = new CustomEvent(AUTOFOCUS_ON_UNMOUNT, EVENT_OPTIONS);\n container.addEventListener(AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus);\n container.dispatchEvent(unmountEvent);\n if (!unmountEvent.defaultPrevented) {\n focus(previouslyFocusedElement ?? document.body, { select: true });\n }\n // we need to remove the listener after we `dispatchEvent`\n container.removeEventListener(AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus);\n\n focusScopesStack.remove(focusScope);\n }, 0);\n };\n }\n }, [container, onMountAutoFocus, onUnmountAutoFocus, focusScope]);\n\n // Takes care of looping focus (when tabbing whilst at the edges)\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (!loop && !trapped) return;\n if (focusScope.paused) return;\n\n const isTabKey = event.key === 'Tab' && !event.altKey && !event.ctrlKey && !event.metaKey;\n const focusedElement = document.activeElement as HTMLElement | null;\n\n if (isTabKey && focusedElement) {\n const container = event.currentTarget as HTMLElement;\n const [first, last] = getTabbableEdges(container);\n const hasTabbableElementsInside = first && last;\n\n // we can only wrap focus if we have tabbable edges\n if (!hasTabbableElementsInside) {\n if (focusedElement === container) event.preventDefault();\n } else {\n if (!event.shiftKey && focusedElement === last) {\n event.preventDefault();\n if (loop) focus(first, { select: true });\n } else if (event.shiftKey && focusedElement === first) {\n event.preventDefault();\n if (loop) focus(last, { select: true });\n }\n }\n }\n },\n [loop, trapped, focusScope.paused]\n );\n\n return (\n \n );\n});\n\nFocusScope.displayName = FOCUS_SCOPE_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * Utils\n * -----------------------------------------------------------------------------------------------*/\n\n/**\n * Attempts focusing the first element in a list of candidates.\n * Stops when focus has actually moved.\n */\nfunction focusFirst(candidates: HTMLElement[], { select = false } = {}) {\n const previouslyFocusedElement = document.activeElement;\n for (const candidate of candidates) {\n focus(candidate, { select });\n if (document.activeElement !== previouslyFocusedElement) return;\n }\n}\n\n/**\n * Returns the first and last tabbable elements inside a container.\n */\nfunction getTabbableEdges(container: HTMLElement) {\n const candidates = getTabbableCandidates(container);\n const first = findVisible(candidates, container);\n const last = findVisible(candidates.reverse(), container);\n return [first, last] as const;\n}\n\n/**\n * Returns a list of potential tabbable candidates.\n *\n * NOTE: This is only a close approximation. For example it doesn't take into account cases like when\n * elements are not visible. This cannot be worked out easily by just reading a property, but rather\n * necessitate runtime knowledge (computed styles, etc). We deal with these cases separately.\n *\n * See: https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker\n * Credit: https://github.com/discord/focus-layers/blob/master/src/util/wrapFocus.tsx#L1\n */\nfunction getTabbableCandidates(container: HTMLElement) {\n const nodes: HTMLElement[] = [];\n const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {\n acceptNode: (node: any) => {\n const isHiddenInput = node.tagName === 'INPUT' && node.type === 'hidden';\n if (node.disabled || node.hidden || isHiddenInput) return NodeFilter.FILTER_SKIP;\n // `.tabIndex` is not the same as the `tabindex` attribute. It works on the\n // runtime's understanding of tabbability, so this automatically accounts\n // for any kind of element that could be tabbed to.\n return node.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;\n },\n });\n while (walker.nextNode()) nodes.push(walker.currentNode as HTMLElement);\n // we do not take into account the order of nodes with positive `tabIndex` as it\n // hinders accessibility to have tab order different from visual order.\n return nodes;\n}\n\n/**\n * Returns the first visible element in a list.\n * NOTE: Only checks visibility up to the `container`.\n */\nfunction findVisible(elements: HTMLElement[], container: HTMLElement) {\n for (const element of elements) {\n // we stop checking if it's hidden at the `container` level (excluding)\n if (!isHidden(element, { upTo: container })) return element;\n }\n}\n\nfunction isHidden(node: HTMLElement, { upTo }: { upTo?: HTMLElement }) {\n if (getComputedStyle(node).visibility === 'hidden') return true;\n while (node) {\n // we stop at `upTo` (excluding it)\n if (upTo !== undefined && node === upTo) return false;\n if (getComputedStyle(node).display === 'none') return true;\n node = node.parentElement as HTMLElement;\n }\n return false;\n}\n\nfunction isSelectableInput(element: any): element is FocusableTarget & { select: () => void } {\n return element instanceof HTMLInputElement && 'select' in element;\n}\n\nfunction focus(element?: FocusableTarget | null, { select = false } = {}) {\n // only focus if that element is focusable\n if (element && element.focus) {\n const previouslyFocusedElement = document.activeElement;\n // NOTE: we prevent scrolling on focus, to minimize jarring transitions for users\n element.focus({ preventScroll: true });\n // only select if its not the same element, it supports selection and we need to select\n if (element !== previouslyFocusedElement && isSelectableInput(element) && select)\n element.select();\n }\n}\n\n/* -------------------------------------------------------------------------------------------------\n * FocusScope stack\n * -----------------------------------------------------------------------------------------------*/\n\ntype FocusScopeAPI = { paused: boolean; pause(): void; resume(): void };\nconst focusScopesStack = createFocusScopesStack();\n\nfunction createFocusScopesStack() {\n /** A stack of focus scopes, with the active one at the top */\n let stack: FocusScopeAPI[] = [];\n\n return {\n add(focusScope: FocusScopeAPI) {\n // pause the currently active focus scope (at the top of the stack)\n const activeFocusScope = stack[0];\n if (focusScope !== activeFocusScope) {\n activeFocusScope?.pause();\n }\n // remove in case it already exists (because we'll re-add it at the top of the stack)\n stack = arrayRemove(stack, focusScope);\n stack.unshift(focusScope);\n },\n\n remove(focusScope: FocusScopeAPI) {\n stack = arrayRemove(stack, focusScope);\n stack[0]?.resume();\n },\n };\n}\n\nfunction arrayRemove(array: T[], item: T) {\n const updatedArray = [...array];\n const index = updatedArray.indexOf(item);\n if (index !== -1) {\n updatedArray.splice(index, 1);\n }\n return updatedArray;\n}\n\nfunction removeLinks(items: HTMLElement[]) {\n return items.filter((item) => item.tagName !== 'A');\n}\n\nconst Root = FocusScope;\n\nexport {\n FocusScope,\n //\n Root,\n};\nexport type { FocusScopeProps };\n", "import * as React from 'react';\nimport { useLayoutEffect } from '@radix-ui/react-use-layout-effect';\n\n// We spaces with `.trim().toString()` to prevent bundlers from trying to `import { useId } from 'react';`\nconst useReactId = (React as any)[' useId '.trim().toString()] || (() => undefined);\nlet count = 0;\n\nfunction useId(deterministicId?: string): string {\n const [id, setId] = React.useState(useReactId());\n // React versions older than 18 will have client-side ids only.\n useLayoutEffect(() => {\n if (!deterministicId) setId((reactId) => reactId ?? String(count++));\n }, [deterministicId]);\n return deterministicId || (id ? `radix-${id}` : '');\n}\n\nexport { useId };\n", "import * as React from 'react';\nimport {\n useFloating,\n autoUpdate,\n offset,\n shift,\n limitShift,\n hide,\n arrow as floatingUIarrow,\n flip,\n size,\n} from '@floating-ui/react-dom';\nimport * as ArrowPrimitive from '@radix-ui/react-arrow';\nimport { useComposedRefs } from '@radix-ui/react-compose-refs';\nimport { createContextScope } from '@radix-ui/react-context';\nimport { Primitive } from '@radix-ui/react-primitive';\nimport { useCallbackRef } from '@radix-ui/react-use-callback-ref';\nimport { useLayoutEffect } from '@radix-ui/react-use-layout-effect';\nimport { useSize } from '@radix-ui/react-use-size';\n\nimport type { Placement, Middleware } from '@floating-ui/react-dom';\nimport type { Scope } from '@radix-ui/react-context';\nimport type { Measurable } from '@radix-ui/rect';\n\nconst SIDE_OPTIONS = ['top', 'right', 'bottom', 'left'] as const;\nconst ALIGN_OPTIONS = ['start', 'center', 'end'] as const;\n\ntype Side = (typeof SIDE_OPTIONS)[number];\ntype Align = (typeof ALIGN_OPTIONS)[number];\n\n/* -------------------------------------------------------------------------------------------------\n * Popper\n * -----------------------------------------------------------------------------------------------*/\n\nconst POPPER_NAME = 'Popper';\n\ntype ScopedProps

= P & { __scopePopper?: Scope };\nconst [createPopperContext, createPopperScope] = createContextScope(POPPER_NAME);\n\ntype PopperContextValue = {\n anchor: Measurable | null;\n onAnchorChange(anchor: Measurable | null): void;\n};\nconst [PopperProvider, usePopperContext] = createPopperContext(POPPER_NAME);\n\ninterface PopperProps {\n children?: React.ReactNode;\n}\nconst Popper: React.FC = (props: ScopedProps) => {\n const { __scopePopper, children } = props;\n const [anchor, setAnchor] = React.useState(null);\n return (\n \n {children}\n \n );\n};\n\nPopper.displayName = POPPER_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * PopperAnchor\n * -----------------------------------------------------------------------------------------------*/\n\nconst ANCHOR_NAME = 'PopperAnchor';\n\ntype PopperAnchorElement = React.ComponentRef;\ntype PrimitiveDivProps = React.ComponentPropsWithoutRef;\ninterface PopperAnchorProps extends PrimitiveDivProps {\n virtualRef?: React.RefObject;\n}\n\nconst PopperAnchor = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const { __scopePopper, virtualRef, ...anchorProps } = props;\n const context = usePopperContext(ANCHOR_NAME, __scopePopper);\n const ref = React.useRef(null);\n const composedRefs = useComposedRefs(forwardedRef, ref);\n\n const anchorRef = React.useRef(null);\n React.useEffect(() => {\n const previousAnchor = anchorRef.current;\n anchorRef.current = virtualRef?.current || ref.current;\n if (previousAnchor !== anchorRef.current) {\n // Consumer can anchor the popper to something that isn't\n // a DOM node e.g. pointer position, so we override the\n // `anchorRef` with their virtual ref in this case.\n context.onAnchorChange(anchorRef.current);\n }\n });\n\n return virtualRef ? null : ;\n }\n);\n\nPopperAnchor.displayName = ANCHOR_NAME;\n\n/* -------------------------------------------------------------------------------------------------\n * PopperContent\n * -----------------------------------------------------------------------------------------------*/\n\nconst CONTENT_NAME = 'PopperContent';\n\ntype PopperContentContextValue = {\n placedSide: Side;\n onArrowChange(arrow: HTMLSpanElement | null): void;\n arrowX?: number;\n arrowY?: number;\n shouldHideArrow: boolean;\n};\n\nconst [PopperContentProvider, useContentContext] =\n createPopperContext(CONTENT_NAME);\n\ntype Boundary = Element | null;\n\ntype PopperContentElement = React.ComponentRef;\ninterface PopperContentProps extends PrimitiveDivProps {\n side?: Side;\n sideOffset?: number;\n align?: Align;\n alignOffset?: number;\n arrowPadding?: number;\n avoidCollisions?: boolean;\n collisionBoundary?: Boundary | Boundary[];\n collisionPadding?: number | Partial>;\n sticky?: 'partial' | 'always';\n hideWhenDetached?: boolean;\n updatePositionStrategy?: 'optimized' | 'always';\n onPlaced?: () => void;\n}\n\nconst PopperContent = React.forwardRef(\n (props: ScopedProps, forwardedRef) => {\n const {\n __scopePopper,\n side = 'bottom',\n sideOffset = 0,\n align = 'center',\n alignOffset = 0,\n arrowPadding = 0,\n avoidCollisions = true,\n collisionBoundary = [],\n collisionPadding: collisionPaddingProp = 0,\n sticky = 'partial',\n hideWhenDetached = false,\n updatePositionStrategy = 'optimized',\n onPlaced,\n ...contentProps\n } = props;\n\n const context = usePopperContext(CONTENT_NAME, __scopePopper);\n\n const [content, setContent] = React.useState(null);\n const composedRefs = useComposedRefs(forwardedRef, (node) => setContent(node));\n\n const [arrow, setArrow] = React.useState(null);\n const arrowSize = useSize(arrow);\n const arrowWidth = arrowSize?.width ?? 0;\n const arrowHeight = arrowSize?.height ?? 0;\n\n const desiredPlacement = (side + (align !== 'center' ? '-' + align : '')) as Placement;\n\n const collisionPadding =\n typeof collisionPaddingProp === 'number'\n ? collisionPaddingProp\n : { top: 0, right: 0, bottom: 0, left: 0, ...collisionPaddingProp };\n\n const boundary = Array.isArray(collisionBoundary) ? collisionBoundary : [collisionBoundary];\n const hasExplicitBoundaries = boundary.length > 0;\n\n const detectOverflowOptions = {\n padding: collisionPadding,\n boundary: boundary.filter(isNotNull),\n // with `strategy: 'fixed'`, this is the only way to get it to respect boundaries\n altBoundary: hasExplicitBoundaries,\n };\n\n const { refs, floatingStyles, placement, isPositioned, middlewareData } = useFloating({\n // default to `fixed` strategy so users don't have to pick and we also avoid focus scroll issues\n strategy: 'fixed',\n placement: desiredPlacement,\n whileElementsMounted: (...args) => {\n const cleanup = autoUpdate(...args, {\n animationFrame: updatePositionStrategy === 'always',\n });\n return cleanup;\n },\n elements: {\n reference: context.anchor,\n },\n middleware: [\n offset({ mainAxis: sideOffset + arrowHeight, alignmentAxis: alignOffset }),\n avoidCollisions &&\n shift({\n mainAxis: true,\n crossAxis: false,\n limiter: sticky === 'partial' ? limitShift() : undefined,\n ...detectOverflowOptions,\n }),\n avoidCollisions && flip({ ...detectOverflowOptions }),\n size({\n ...detectOverflowOptions,\n apply: ({ elements, rects, availableWidth, availableHeight }) => {\n const { width: anchorWidth, height: anchorHeight } = rects.reference;\n const contentStyle = elements.floating.style;\n contentStyle.setProperty('--radix-popper-available-width', `${availableWidth}px`);\n contentStyle.setProperty('--radix-popper-available-height', `${availableHeight}px`);\n contentStyle.setProperty('--radix-popper-anchor-width', `${anchorWidth}px`);\n contentStyle.setProperty('--radix-popper-anchor-height', `${anchorHeight}px`);\n },\n }),\n arrow && floatingUIarrow({ element: arrow, padding: arrowPadding }),\n transformOrigin({ arrowWidth, arrowHeight }),\n hideWhenDetached && hide({ strategy: 'referenceHidden', ...detectOverflowOptions }),\n ],\n });\n\n const [placedSide, placedAlign] = getSideAndAlignFromPlacement(placement);\n\n const handlePlaced = useCallbackRef(onPlaced);\n useLayoutEffect(() => {\n if (isPositioned) {\n handlePlaced?.();\n }\n }, [isPositioned, handlePlaced]);\n\n const arrowX = middlewareData.arrow?.x;\n const arrowY = middlewareData.arrow?.y;\n const cannotCenterArrow = middlewareData.arrow?.centerOffset !== 0;\n\n const [contentZIndex, setContentZIndex] = React.useState();\n useLayoutEffect(() => {\n if (content) setContentZIndex(window.getComputedStyle(content).zIndex);\n }, [content]);\n\n return (\n