cleanup
This commit is contained in:
@@ -1,120 +0,0 @@
|
|||||||
# Stage 3 Implementation Summary
|
|
||||||
|
|
||||||
## ✅ Completed Backend Features
|
|
||||||
|
|
||||||
### 1. Database Schema & Migrations
|
|
||||||
- ✅ Added `entity_type`, `taxonomy`, `cluster_role` fields to Tasks model
|
|
||||||
- ✅ Created migration `0013_stage3_add_task_metadata.py`
|
|
||||||
- ✅ Updated backfill function in `0012_metadata_mapping_tables.py` to populate existing data
|
|
||||||
|
|
||||||
### 2. Pipeline Updates
|
|
||||||
- ✅ **Ideas → Tasks**: Updated `ContentIdeasViewSet.bulk_queue_to_writer()` to inherit `entity_type`, `taxonomy`, `cluster_role` from Ideas
|
|
||||||
- ✅ **PageBlueprint → Tasks**: Updated `PageGenerationService._create_task_from_page()` to set metadata from blueprint
|
|
||||||
- ✅ **Tasks → Content**: Created `MetadataMappingService` to persist cluster/taxonomy mappings when Content is created
|
|
||||||
|
|
||||||
### 3. Validation Services
|
|
||||||
- ✅ Created `ContentValidationService` with:
|
|
||||||
- `validate_task()` - Validates task metadata
|
|
||||||
- `validate_content()` - Validates content metadata
|
|
||||||
- `validate_for_publish()` - Comprehensive pre-publish validation
|
|
||||||
- `ensure_required_attributes()` - Checks required attributes per entity type
|
|
||||||
|
|
||||||
### 4. Linker & Optimizer Enhancements
|
|
||||||
- ✅ **Linker**: Enhanced `CandidateEngine` to:
|
|
||||||
- Prioritize content from same clusters (50 points)
|
|
||||||
- Match by taxonomy (20 points)
|
|
||||||
- Match by entity type (15 points)
|
|
||||||
- Flag cluster/taxonomy matches in results
|
|
||||||
|
|
||||||
- ✅ **Optimizer**: Enhanced `ContentAnalyzer` to:
|
|
||||||
- Calculate metadata completeness score (0-100)
|
|
||||||
- Check cluster/taxonomy mappings
|
|
||||||
- Include metadata score in overall optimization score (15% weight)
|
|
||||||
|
|
||||||
### 5. API Endpoints
|
|
||||||
- ✅ `GET /api/v1/writer/content/{id}/validation/` - Get validation checklist
|
|
||||||
- ✅ `POST /api/v1/writer/content/{id}/validate/` - Re-run validators
|
|
||||||
- ✅ `GET /api/v1/site-builder/blueprints/{id}/progress/` - Cluster-level completion status
|
|
||||||
|
|
||||||
### 6. Management Commands
|
|
||||||
- ✅ Created `audit_site_metadata` command:
|
|
||||||
- Usage: `python manage.py audit_site_metadata --site {id}`
|
|
||||||
- Shows metadata completeness per site
|
|
||||||
- Includes detailed breakdown with `--detailed` flag
|
|
||||||
|
|
||||||
## ⚠️ Frontend Updates (Pending)
|
|
||||||
|
|
||||||
### Writer UI Enhancements
|
|
||||||
- [ ] Add metadata columns to Content list (entity_type, cluster, taxonomy)
|
|
||||||
- [ ] Add validation panel to Content editor showing:
|
|
||||||
- Validation errors
|
|
||||||
- Metadata completeness indicators
|
|
||||||
- Publish button disabled until valid
|
|
||||||
- [ ] Display cluster/taxonomy chips in Content cards
|
|
||||||
- [ ] Add filters for entity_type and validation status
|
|
||||||
|
|
||||||
### Linker UI Enhancements
|
|
||||||
- [ ] Group link suggestions by cluster role (hub → supporting, hub → attribute)
|
|
||||||
- [ ] Show cluster match indicators
|
|
||||||
- [ ] Display context snippets with cluster information
|
|
||||||
|
|
||||||
### Optimizer UI Enhancements
|
|
||||||
- [ ] Add metadata scorecard to optimization dashboard
|
|
||||||
- [ ] Show cluster coverage indicators
|
|
||||||
- [ ] Display taxonomy alignment status
|
|
||||||
- [ ] Add "next action" cards for missing metadata
|
|
||||||
|
|
||||||
## 📋 Next Steps
|
|
||||||
|
|
||||||
1. **Run Migrations**:
|
|
||||||
```bash
|
|
||||||
python manage.py migrate writer 0013_stage3_add_task_metadata
|
|
||||||
python manage.py migrate writer 0012_metadata_mapping_tables # Re-run to backfill
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Test Backend**:
|
|
||||||
- Test Ideas → Tasks pipeline with metadata inheritance
|
|
||||||
- Test Content validation endpoints
|
|
||||||
- Test Linker/Optimizer with cluster mappings
|
|
||||||
- Run audit command: `python manage.py audit_site_metadata --site 5`
|
|
||||||
|
|
||||||
3. **Frontend Implementation**:
|
|
||||||
- Update Writer Content list to show metadata
|
|
||||||
- Add validation panel to Content editor
|
|
||||||
- Enhance Linker/Optimizer UIs with cluster information
|
|
||||||
|
|
||||||
## 🔧 Files Modified
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
- `backend/igny8_core/business/content/models.py` - Added metadata fields to Tasks
|
|
||||||
- `backend/igny8_core/modules/writer/migrations/0013_stage3_add_task_metadata.py` - New migration
|
|
||||||
- `backend/igny8_core/modules/writer/migrations/0012_metadata_mapping_tables.py` - Updated backfill
|
|
||||||
- `backend/igny8_core/modules/planner/views.py` - Updated Ideas→Tasks pipeline
|
|
||||||
- `backend/igny8_core/business/site_building/services/page_generation_service.py` - Updated PageBlueprint→Tasks
|
|
||||||
- `backend/igny8_core/business/content/services/metadata_mapping_service.py` - New service
|
|
||||||
- `backend/igny8_core/business/content/services/validation_service.py` - New service
|
|
||||||
- `backend/igny8_core/business/linking/services/candidate_engine.py` - Enhanced with cluster matching
|
|
||||||
- `backend/igny8_core/business/optimization/services/analyzer.py` - Enhanced with metadata scoring
|
|
||||||
- `backend/igny8_core/modules/writer/views.py` - Added validation endpoints
|
|
||||||
- `backend/igny8_core/modules/site_builder/views.py` - Added progress endpoint
|
|
||||||
- `backend/igny8_core/modules/writer/management/commands/audit_site_metadata.py` - New command
|
|
||||||
|
|
||||||
## 🎯 Stage 3 Objectives Status
|
|
||||||
|
|
||||||
| Objective | Status |
|
|
||||||
|-----------|--------|
|
|
||||||
| Metadata backfill | ✅ Complete |
|
|
||||||
| Ideas→Tasks pipeline | ✅ Complete |
|
|
||||||
| Tasks→Content pipeline | ✅ Complete |
|
|
||||||
| Validation services | ✅ Complete |
|
|
||||||
| Linker enhancements | ✅ Complete |
|
|
||||||
| Optimizer enhancements | ✅ Complete |
|
|
||||||
| API endpoints | ✅ Complete |
|
|
||||||
| Audit command | ✅ Complete |
|
|
||||||
| Frontend Writer UI | ⚠️ Pending |
|
|
||||||
| Frontend Linker UI | ⚠️ Pending |
|
|
||||||
| Frontend Optimizer UI | ⚠️ Pending |
|
|
||||||
|
|
||||||
**Overall Stage 3 Backend: ~85% Complete**
|
|
||||||
**Overall Stage 3 Frontend: ~0% Complete**
|
|
||||||
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
# Stage 3 Test Results
|
|
||||||
|
|
||||||
## ✅ Migration Tests
|
|
||||||
|
|
||||||
### Migration Execution
|
|
||||||
```bash
|
|
||||||
✅ Migration 0012_metadata_mapping_tables: SUCCESS
|
|
||||||
- Backfill complete:
|
|
||||||
- Tasks entity_type updated: 0
|
|
||||||
- Content entity_type updated: 0
|
|
||||||
- Cluster mappings created: 10
|
|
||||||
- Taxonomy mappings created: 0
|
|
||||||
|
|
||||||
✅ Migration 0013_stage3_add_task_metadata: SUCCESS
|
|
||||||
- Added entity_type, taxonomy, cluster_role fields to Tasks
|
|
||||||
- Added indexes for entity_type and cluster_role
|
|
||||||
```
|
|
||||||
|
|
||||||
## ✅ Backend API Tests
|
|
||||||
|
|
||||||
### 1. Audit Command Test
|
|
||||||
```bash
|
|
||||||
$ python manage.py audit_site_metadata --site 5
|
|
||||||
|
|
||||||
✅ SUCCESS - Results:
|
|
||||||
📋 Tasks Summary:
|
|
||||||
Total Tasks: 11
|
|
||||||
With Cluster: 11/11 (100%)
|
|
||||||
With Entity Type: 11/11 (100%)
|
|
||||||
With Taxonomy: 0/11 (0%)
|
|
||||||
With Cluster Role: 11/11 (100%)
|
|
||||||
|
|
||||||
📄 Content Summary:
|
|
||||||
Total Content: 10
|
|
||||||
With Entity Type: 10/10 (100%)
|
|
||||||
With Cluster Mapping: 10/10 (100%)
|
|
||||||
With Taxonomy Mapping: 0/10 (0%)
|
|
||||||
With Attributes: 0/10 (0%)
|
|
||||||
|
|
||||||
⚠️ Gaps:
|
|
||||||
Tasks missing cluster: 0
|
|
||||||
Tasks missing entity_type: 0
|
|
||||||
Content missing cluster mapping: 0
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Validation Service Test
|
|
||||||
```python
|
|
||||||
✅ ContentValidationService.validate_content() - WORKING
|
|
||||||
- Correctly identifies missing cluster mapping
|
|
||||||
- Returns structured error: "Content must be mapped to at least one cluster"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. API Endpoints (Ready for Testing)
|
|
||||||
- ✅ `GET /api/v1/writer/content/{id}/validation/` - Endpoint added
|
|
||||||
- ✅ `POST /api/v1/writer/content/{id}/validate/` - Endpoint added
|
|
||||||
- ✅ `GET /api/v1/site-builder/blueprints/{id}/progress/` - Endpoint added
|
|
||||||
|
|
||||||
## ✅ Frontend Browser Tests
|
|
||||||
|
|
||||||
### Pages Loaded Successfully
|
|
||||||
1. ✅ **Dashboard** (`/`) - Loaded successfully
|
|
||||||
2. ✅ **Writer Content** (`/writer/content`) - Loaded successfully
|
|
||||||
- API call: `GET /api/v1/writer/content/?site_id=9&page=1&page_size=10&ordering=-generated_at` - 200 OK
|
|
||||||
3. ✅ **Site Builder** (`/sites/builder`) - Loaded successfully
|
|
||||||
4. ✅ **Blueprints** (`/sites/blueprints`) - Loaded successfully
|
|
||||||
|
|
||||||
### Console Status
|
|
||||||
- ✅ No JavaScript errors
|
|
||||||
- ✅ Vite connected successfully
|
|
||||||
- ✅ All API calls returning 200 status
|
|
||||||
|
|
||||||
## 📊 Data Status
|
|
||||||
|
|
||||||
### Current State
|
|
||||||
- **Tasks**: 11 total, all have clusters and entity_type
|
|
||||||
- **Content**: 10 total, all have entity_type and cluster mappings
|
|
||||||
- **Cluster Mappings**: 10 created successfully
|
|
||||||
- **Taxonomy Mappings**: 0 (expected - no taxonomies assigned yet)
|
|
||||||
|
|
||||||
## 🎯 Stage 3 Backend Status: ✅ COMPLETE
|
|
||||||
|
|
||||||
All backend features are implemented and tested:
|
|
||||||
- ✅ Database migrations applied
|
|
||||||
- ✅ Metadata fields added to Tasks
|
|
||||||
- ✅ Backfill completed (10 cluster mappings created)
|
|
||||||
- ✅ Validation service working
|
|
||||||
- ✅ API endpoints added
|
|
||||||
- ✅ Audit command working
|
|
||||||
- ✅ Linker/Optimizer enhancements complete
|
|
||||||
|
|
||||||
## ⚠️ Frontend Status: Pending
|
|
||||||
|
|
||||||
Frontend UI updates for Stage 3 are not yet implemented:
|
|
||||||
- ⚠️ Writer Content list - metadata columns not added
|
|
||||||
- ⚠️ Validation panel - not added to Content editor
|
|
||||||
- ⚠️ Linker UI - cluster-based suggestions not displayed
|
|
||||||
- ⚠️ Optimizer UI - metadata scorecards not displayed
|
|
||||||
|
|
||||||
## 🔄 Next Steps
|
|
||||||
|
|
||||||
1. **Test API Endpoints** (via browser/Postman):
|
|
||||||
- `GET /api/v1/writer/content/{id}/validation/`
|
|
||||||
- `POST /api/v1/writer/content/{id}/validate/`
|
|
||||||
- `GET /api/v1/site-builder/blueprints/{id}/progress/`
|
|
||||||
|
|
||||||
2. **Create Test Blueprint** to test workflow wizard:
|
|
||||||
- Navigate to `/sites/builder`
|
|
||||||
- Create new blueprint
|
|
||||||
- Test workflow wizard at `/sites/builder/workflow/{blueprintId}`
|
|
||||||
|
|
||||||
3. **Frontend Implementation** (when ready):
|
|
||||||
- Add metadata columns to Content list
|
|
||||||
- Add validation panel to Content editor
|
|
||||||
- Enhance Linker/Optimizer UIs
|
|
||||||
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# Planning & Phase Documentation
|
|
||||||
|
|
||||||
The Part 2 planning folder now uses four consolidated docs instead of many phase-specific files. Start here to understand where to look for strategy, workflows, history, and the active roadmap.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Consolidated Docs
|
|
||||||
|
|
||||||
| Doc | Description |
|
|
||||||
| --- | --- |
|
|
||||||
| [`planning/01-strategy.md`](planning/01-strategy.md) | Platform context, architecture principles, module overview, security baseline. |
|
|
||||||
| [`planning/02-workflows.md`](planning/02-workflows.md) | End-to-end workflows for Planner, Site Builder, Ideas/Writer, Publishing, and credit usage. |
|
|
||||||
| [`planning/03-phase-reports.md`](planning/03-phase-reports.md) | Historical phase summaries, verification highlights, lessons learned. |
|
|
||||||
| [`planning/04-roadmap.md`](planning/04-roadmap.md) | Current execution roadmap, stages, milestones, dependencies, risks. |
|
|
||||||
|
|
||||||
All former PHASE-*.md files, Igny8-part-2 plan, and implementation plans have been merged into these references (see git history if you need the raw originals).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## How to Use
|
|
||||||
1. **Align on context** – read `01-strategy.md` to understand the architecture and goals.
|
|
||||||
2. **Review workflows** – `02-workflows.md` explains how Planner → Site Builder → Writer → Publishing connect.
|
|
||||||
3. **Learn from history** – `03-phase-reports.md` documents what’s been completed and verified.
|
|
||||||
4. **Plan execution** – `04-roadmap.md` lists active stages, milestones, and cross-team dependencies.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Status Tracking
|
|
||||||
- Keep this README and the four bundle docs updated as milestones shift.
|
|
||||||
- Any new planning artifact should extend one of the consolidated docs rather than creating a new standalone file.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated:** 2025-11-19
|
|
||||||
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
# IGNY8 Phase 2 Strategy & Architecture
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
Single reference for the “why” and “what” behind Phase 2, combining the original `ARCHITECTURE_CONTEXT.md` and the strategic sections of `IGNY8-HOLISTIC-ARCHITECTURE-PLAN.md`. Use this doc to align stakeholders before diving into workflows, phases, or roadmap specifics.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
- IGNY8 is a multi-tenant SaaS for SEO planning and AI content creation. Accounts own sites, sites contain sectors, and all planner/writer activity is scoped to that hierarchy.
|
|
||||||
- The refreshed strategy focuses on a **cluster-first planning engine**, **state-aware site builder**, and **shared metadata across Planner, Writer, Linker, Optimizer, and WordPress sync**.
|
|
||||||
- Platform foundations: Django 5.2 + DRF backend, React 19 frontend, PostgreSQL, Redis, Celery, Dockerized infra with Caddy reverse proxy.
|
|
||||||
|
|
||||||
### Guiding Principles
|
|
||||||
1. **Multi-tenancy everywhere** – automatic account/site/sector scoping in models & viewsets.
|
|
||||||
2. **Configuration-driven UX** – shared templates, centralized API client, environment-configurable AI prompts.
|
|
||||||
3. **Unified AI engine** – all AI calls flow through `AIEngine` via Celery, with credit tracking and progress reporting.
|
|
||||||
4. **Stage-gated workflows** – users move through guided wizards (Planner → Site Builder → Writer → Publish) with clear prerequisites.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
### Layers
|
|
||||||
| Layer | Responsibilities |
|
|
||||||
| --- | --- |
|
|
||||||
| **Client Apps** | Main SaaS app (`app.igny8`), marketing site, admin tooling. |
|
|
||||||
| **Reverse Proxy** | Caddy terminates TLS and routes traffic. |
|
|
||||||
| **Application Services** | React frontend (8021), Django backend (8011), Celery worker/beat. |
|
|
||||||
| **Data & Storage** | PostgreSQL, Redis, `/data/app/sites-data` for site deployments. |
|
|
||||||
| **External Integrations** | OpenAI/Runware for AI, WordPress for publishing, future Shopify. |
|
|
||||||
|
|
||||||
### Module Snapshot
|
|
||||||
- **Planner**: Keywords, Clusters, Ideas, clustering AI.
|
|
||||||
- **Writer**: Tasks, Content, Images, AI generation, WordPress publishing.
|
|
||||||
- **Site Builder**: Blueprints, page scaffolding, deployment adapters.
|
|
||||||
- **System/Integration**: Settings, API keys, sync adapters.
|
|
||||||
- **Billing**: Credits, transactions, usage logs.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Data Hierarchy & Access
|
|
||||||
```
|
|
||||||
Account → Site → Sector → (Keywords, Clusters, Ideas, Tasks, Content, Images)
|
|
||||||
```
|
|
||||||
- Roles (`developer > owner > admin > editor > viewer > system_bot`) determine automatic access.
|
|
||||||
- Editors/Viewers require explicit `SiteUserAccess`.
|
|
||||||
- Middleware injects `request.account`; viewsets enforce scoping.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Workflows (High-Level)
|
|
||||||
1. **Account Setup** – create account/site/sector, configure integrations, assign roles.
|
|
||||||
2. **Planner** – import keywords, auto-cluster, attach clusters to site builder.
|
|
||||||
3. **Writer** – turn ideas into tasks, run AI content generation, manage reviews.
|
|
||||||
4. **Publishing** – deploy to IGNY8 renderer or sync to WordPress (future Shopify).
|
|
||||||
|
|
||||||
Detailed mechanics live in `02-workflows.md`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## AI Framework Snapshot
|
|
||||||
- Entry point: `run_ai_task(function_name, payload, account_id)`.
|
|
||||||
- Six-phase pipeline (INIT → PREP → AI_CALL → PARSE → SAVE → DONE).
|
|
||||||
- Functions currently: `auto_cluster`, `generate_ideas`, `generate_content`, `generate_image_prompts`, `generate_images`.
|
|
||||||
- Credits deducted post-success; each function declares cost.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security & Ops
|
|
||||||
- JWT auth with 15 min access / 7 day refresh, stored client-side.
|
|
||||||
- Role-based authorization on every request.
|
|
||||||
- Dockerized infra split between `igny8-infra` (Postgres, Redis, Caddy, etc.) and `igny8-app` (backend/frontend/worker).
|
|
||||||
- External services configured per account via Integration Settings.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Current Strategic Priorities
|
|
||||||
1. **Cluster-first planning** – enforce keyword clusters before site planning.
|
|
||||||
2. **Taxonomy-aware site builder** – blog/ecommerce/company flows with state-aware wizard.
|
|
||||||
3. **Unified content metadata** – propagate cluster/taxonomy data through writer, linker, optimizer, and publishing.
|
|
||||||
4. **WordPress parity** – treat synced WP sites as first-class citizens without duplicating site data.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## References
|
|
||||||
- Detailed workflows: `02-workflows.md`
|
|
||||||
- Phase reports & learnings: `03-phase-reports.md`
|
|
||||||
- Execution roadmap: `04-roadmap.md`
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
# IGNY8 Core Workflows & Systems
|
|
||||||
|
|
||||||
Combines the previous `CONTENT-WORKFLOW-DIAGRAM.md`, sample credit usage notes, and scattered workflow descriptions into a single reference.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
1. Planner Workflows
|
|
||||||
2. Site Builder (State-Aware Wizard)
|
|
||||||
3. Ideas & Writer Pipeline
|
|
||||||
4. Publishing & Sync
|
|
||||||
5. Credit & Usage Examples
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Planner Workflows
|
|
||||||
|
|
||||||
### Keyword Intake & Management
|
|
||||||
1. Import keywords via CSV/manual → validate intent, volume, difficulty.
|
|
||||||
2. Keywords inherit account/site/sector context; duplicates prevented via `seed_keyword` + site/sector constraint.
|
|
||||||
3. Filtering/searching available by status, intent, sector, cluster assignment.
|
|
||||||
|
|
||||||
### Auto Clustering
|
|
||||||
```
|
|
||||||
Keyword Selection → POST /planner/keywords/auto-cluster →
|
|
||||||
run_ai_task(auto_cluster) → AI groups keywords →
|
|
||||||
Clusters created + keywords linked → Credits deducted
|
|
||||||
```
|
|
||||||
- Clusters now tagged with `context_type` (`topic`, `attribute`, `service_line`).
|
|
||||||
- Outputs recommendation metadata used by site builder taxonomy step.
|
|
||||||
|
|
||||||
### Cluster Management
|
|
||||||
- Views show per-cluster metrics (keyword count, volume, gap warnings).
|
|
||||||
- Users can assign clusters to site blueprints; gating enforced before sitemap generation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Site Builder Workflow (Self-Guided Wizard)
|
|
||||||
|
|
||||||
| Step | Requirements | Output |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 1. Business Details | Site + sector selected, site type (blog/ecom/company), hosting target (IGNY8 vs WP). | Draft `SiteBlueprint`, workflow state `business_details`. |
|
|
||||||
| 2. Cluster Assignment | ≥1 planner cluster linked; show coverage metrics. | `SiteBlueprintCluster` rows, state `clusters_ready`. |
|
|
||||||
| 3. Taxonomy Builder | Define/import categories, tags, product attributes, service groups; map to clusters. | `SiteBlueprintTaxonomy` records, state `taxonomies_ready`. |
|
|
||||||
| 4. AI Sitemap | Allowed only when clusters + taxonomies ready; AI generates pages w/ entity types + cluster refs. | `PageBlueprint` records, coverage matrix, state `sitemap_ready`. |
|
|
||||||
| 5. Coverage Validation | Confirm each cluster has hub/supporting pages; unresolved items block progress. | Approval flag, state `ideas_ready`. |
|
|
||||||
| 6. Ideas Hand-off | Selected pages pushed to Planner Ideas with optional guidance prompt. | Idea queue seeded, state `ideas_in_progress`. |
|
|
||||||
|
|
||||||
Frontend enforcement:
|
|
||||||
- Zustand `builderWorkflowStore` tracks step state via `/site-builder/workflow/{id}`.
|
|
||||||
- Next buttons disabled until backend returns `step_status = complete`.
|
|
||||||
- Inline tooltips explain missing prerequisites, with links back to Planner.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Ideas & Writer Pipeline
|
|
||||||
|
|
||||||
### Ideas Creation
|
|
||||||
1. Wizard hand-off calls `POST /planner/content-ideas/bulk_from_blueprint`.
|
|
||||||
2. Each idea stores `cluster_id`, `taxonomy_id`, `site_entity_type`, `cluster_role`.
|
|
||||||
3. Ideas appear in Planner UI with badges showing target page type (blog post, product page, service page, taxonomy hub).
|
|
||||||
|
|
||||||
### Task Generation
|
|
||||||
1. `PageGenerationService.generate_all_pages` turns ideas/pages into Writer tasks.
|
|
||||||
2. Tasks carry metadata: `entity_type`, `taxonomy_id`, `cluster_role`, `product_data` (JSON for specs), keywords.
|
|
||||||
|
|
||||||
### AI Content Generation
|
|
||||||
```
|
|
||||||
Task Selection → POST /writer/tasks/generate →
|
|
||||||
run_ai_task(generate_content) → AI produces html/json_blocks →
|
|
||||||
Content saved + linked to tasks → Linker/Optimizer receive metadata
|
|
||||||
```
|
|
||||||
- Content also mapped to clusters/taxonomies via `ContentClusterMap` etc.
|
|
||||||
- Images workflow attaches prompts, usage context (featured, gallery, variant).
|
|
||||||
|
|
||||||
### State Awareness
|
|
||||||
- Writer dashboards show per-site progress bars (e.g., “Cluster Alpha: 2/5 hubs published”).
|
|
||||||
- Editors cannot mark content ready unless required taxonomy/attribute data is filled.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Publishing & Sync
|
|
||||||
|
|
||||||
### IGNY8 Hosting
|
|
||||||
1. Deploy action triggers `SitesRendererAdapter`.
|
|
||||||
2. Adapter merges published `Content.json_blocks` into page definitions, writes to `/data/app/sites-data/clients/{site_id}/v{version}`.
|
|
||||||
3. Renderer serves `https://sites.igny8.com/{siteSlug}`; cluster/taxonomy metadata included for internal linking.
|
|
||||||
|
|
||||||
### WordPress Sync
|
|
||||||
1. Integration settings tested via `WordPressAdapter.test_connection`.
|
|
||||||
2. Sync job (`ContentSyncService`) fetches WP taxonomies/posts/products, maps them to IGNY8 schemas via TaxonomyService.
|
|
||||||
3. Publishing back to WP reuses same metadata: categories/tags/attributes auto-created if missing, pages matched by external IDs.
|
|
||||||
4. Workflow enforces cluster assignment for imported content before allowing optimization tasks.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Credit & Usage Examples
|
|
||||||
|
|
||||||
| Operation | Trigger | Credit Cost | Notes |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| Auto Cluster | Planner keywords | 1 credit / 30 keywords | Minimum 1 credit per request. |
|
|
||||||
| Idea Generation | Cluster selection | 1 credit / idea | Charged when ideas created. |
|
|
||||||
| Content Generation | Writer tasks | 3 credits / content | Includes HTML + structured blocks. |
|
|
||||||
| Image Generation | Image tasks | 1 credit / image | Prompt extraction included in content gen. |
|
|
||||||
| Re-optimization | Optimizer rerun | 1 credit / rerun | Optional step for existing content. |
|
|
||||||
|
|
||||||
Credits deducted post-success via `CreditService`. Usage logs available under Billing > Usage.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Credit-Only Operating Principles
|
|
||||||
- Subscription plans only define credit refills + support tier; every feature stays unlocked.
|
|
||||||
- No per-plan limits (keywords, clusters, tasks, images, sites, users, etc.); credits are the sole limiter.
|
|
||||||
- Actions check credit balance before running; insufficient credits show a blocking warning with CTA to top up.
|
|
||||||
- Frontend should always show remaining credits + estimated cost before execution.
|
|
||||||
- Credits must be purchasable on-demand, with logs + notifications when balances are low.
|
|
||||||
|
|
||||||
These principles come from the former “sample usage limits & credit system” note and govern all future modules.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cross-References
|
|
||||||
- Strategy & architecture context: `01-strategy.md`
|
|
||||||
- Phase-specific learnings & QA logs: `03-phase-reports.md`
|
|
||||||
- Execution roadmap & milestones: `04-roadmap.md`
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
# Phase & Verification Reports (Consolidated)
|
|
||||||
|
|
||||||
This document replaces the individual PHASE-*.md files and verification reports. Use it to review historical execution, QA findings, and lessons learned.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
1. Phase Timeline Overview
|
|
||||||
2. Phase Details
|
|
||||||
- Phases 0–4: Foundation to Linker/Optimizer
|
|
||||||
- Phases 5–7–9: Sites Renderer & UI Integration
|
|
||||||
- Phase 8: Universal Content Types
|
|
||||||
3. Verification & Migration Reports
|
|
||||||
4. Key Lessons & Follow-Ups
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Phase Timeline Overview
|
|
||||||
|
|
||||||
| Phase | Focus | Status | Highlights |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| 0 | Environment & infra readiness | ✅ Complete | Docker stacks, CI hooks, base monitoring. |
|
|
||||||
| 1 | Core planner foundations | ✅ Complete | Keywords import, clustering MVP, credit accounting. |
|
|
||||||
| 2 | Writer baseline | ✅ Complete | Task pipeline, AI content generation, WordPress adapter stub. |
|
|
||||||
| 3 | Automation & Thinker modules | ✅ Complete | Prompt registry, author profiles, AI config UI. |
|
|
||||||
| 4 | Linker & Optimizer foundations | ✅ Complete | Internal linking prototype, optimization scoring. |
|
|
||||||
| 5 | Site builder migration + renderer sync | ✅ Complete | Blueprint models, renderer integration, deployment flow. |
|
|
||||||
| 6 | UI polish & dashboard alignment | ✅ Complete | Unified design system, navigation updates. |
|
|
||||||
| 7 | Sites preview & publisher UX | ✅ Complete | Blueprint preview, publish states, progress indicators. |
|
|
||||||
| 8 | Universal Content Types (UCT) | ✅ Complete | Entity types, structured blocks, taxonomy groundwork. |
|
|
||||||
| 9 | Final renderer alignment & QA | ✅ Complete | Deployment validation, edge-case fixing. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Phase Details
|
|
||||||
|
|
||||||
### Phases 0–4: Foundation to Linker/Optimizer
|
|
||||||
- **Infra Setup:** Split `igny8-infra` vs `igny8-app`, network hardening, secrets management.
|
|
||||||
- **Planner Enhancements:** CSV import, dedupe logic, clustering UI, integration with billing.
|
|
||||||
- **Writer MVP:** Task creation from ideas, Celery-driven AI content, HTML editor baseline.
|
|
||||||
- **AI Framework:** `AIEngine`, function registry, standardized progress reporting.
|
|
||||||
- **Linker & Optimizer:** Early scoring model, automatic anchor suggestion pipeline, storage for link graphs.
|
|
||||||
|
|
||||||
### Phases 5–7–9: Sites Renderer & UI Integration
|
|
||||||
- **Site Builder Migration:** Blueprint CRUD, page generation, deployment service writing to `/sites-data`.
|
|
||||||
- **Renderer Alignment:** Public site routes, navigation auto-generation, content fallback behavior.
|
|
||||||
- **UI Alignment:** Builder wizard moved into main frontend, state managed via new stores, progress modals.
|
|
||||||
- **Publishing Workflow:** Deploy button gating, deployment logs, error surfacing.
|
|
||||||
|
|
||||||
### Phase 8: Universal Content Types
|
|
||||||
- Introduced `entity_type`, `json_blocks`, `structure_data` on `Content`.
|
|
||||||
- Added mapping tables for clusters/taxonomies (foundation for current architecture).
|
|
||||||
- Updated Writer UI to show entity context; prepared for ecommerce/service content.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Verification & Migration Reports
|
|
||||||
|
|
||||||
### Implementation Verification (condensed from `PHASE-IMPLEMENTATION-VERIFICATION-REPORT.md`)
|
|
||||||
- **Database**: Ensured migrations applied for planner, writer, site builder, UCT tables.
|
|
||||||
- **Async Stack**: Verified Redis/Celery worker/beat running; failover mode documented.
|
|
||||||
- **APIs**: Regression-tested planner/writer endpoints, new blueprint actions, publisher endpoints.
|
|
||||||
- **Front-End**: Smoke tests on navigation, wizard steps, planner → writer transitions.
|
|
||||||
|
|
||||||
### Phase 5 Migration Reports
|
|
||||||
- Confirmed blueprint tables, metadata seeds, and renderer outputs after migration.
|
|
||||||
- Validated WordPress adapter still reachable; flagged missing taxonomy sync (addressed in new plan).
|
|
||||||
|
|
||||||
### Final Verification
|
|
||||||
- Deployment smoke test across multiple site types.
|
|
||||||
- Cross-checked credit deductions vs usage logs.
|
|
||||||
- QA sign-off captured with issue follow-ups (see next section).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Key Lessons & Follow-Ups
|
|
||||||
1. **State Awareness Needed** – Users struggled with knowing “what’s next”; resolved via new wizard gating plan.
|
|
||||||
2. **Taxonomy Gaps** – Ecommerce/service taxonomies were ad-hoc; led to current blueprint taxonomy initiative.
|
|
||||||
3. **WordPress Sync Depth** – Publishing worked but taxonomy/product sync missing; prioritized in new roadmap.
|
|
||||||
4. **Monitoring** – Need better visibility into AI task queues; future work: add metrics dashboards tied to Celery and credits.
|
|
||||||
|
|
||||||
Outstanding actions now captured in `04-roadmap.md`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## References
|
|
||||||
- Strategy context: `01-strategy.md`
|
|
||||||
- Operational workflows: `02-workflows.md`
|
|
||||||
- Execution roadmap & milestones: `04-roadmap.md`
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
# IGNY8 Phase 2 Roadmap & Implementation Plan
|
|
||||||
|
|
||||||
Consolidates `Igny8-part-2-plan.md` and `IGNY8-IMPLEMENTATION-PLAN.md` into a single execution roadmap.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Roadmap Overview
|
|
||||||
|
|
||||||
### Strategic Themes
|
|
||||||
1. **Planner Modernization** – richer clustering, taxonomy seeding, keyword governance.
|
|
||||||
2. **Site Builder Evolution** – guided wizard, taxonomy-aware sitemap generation, blueprint ↔ planner sync.
|
|
||||||
3. **Writer & Optimization Depth** – metadata propagation, product/service support, auto-linking improvements.
|
|
||||||
4. **Publishing & Sync** – IGNY8 deployments + full WordPress parity (taxonomies, products, attributes).
|
|
||||||
|
|
||||||
### Stage Rollout
|
|
||||||
| Stage | Focus | Target Outcome |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Stage 1 | Data & services foundation | New schema, clustering upgrades, workflow state APIs. |
|
|
||||||
| Stage 2 | Planner + Wizard UX | State-aware wizard, taxonomy builder, planner UI refresh. |
|
|
||||||
| Stage 3 | Writer / Linker / Optimizer | Metadata-aware tasks/content, improved linking & scoring. |
|
|
||||||
| Stage 4 | Publishing & Sync | WordPress taxonomy sync, deployment polish, QA. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Detailed Workstream Breakdown
|
|
||||||
|
|
||||||
### Workstream A – Data & Services
|
|
||||||
- Apply migrations for blueprint clusters/taxonomies, writer mapping tables.
|
|
||||||
- Implement `WorkflowStateService`, validation endpoints, TaxonomyService.
|
|
||||||
- Upgrade clustering prompt/service for multi-dimensional outputs.
|
|
||||||
- Update API serializers to expose entity metadata.
|
|
||||||
|
|
||||||
### Workstream B – Planner & Wizard UX
|
|
||||||
- New planner views for clusters/taxonomies with matrix visualization.
|
|
||||||
- Wizard steps (business, clusters, taxonomy, sitemap, validation, hand-off) with gating logic.
|
|
||||||
- Toasts/tooltips/inline helpers to keep users oriented.
|
|
||||||
- Resume capability (persisted step state).
|
|
||||||
|
|
||||||
### Workstream C – Writer, Linker, Optimizer
|
|
||||||
- Extend ideas/tasks/content editors with cluster/taxonomy context panels.
|
|
||||||
- Enforce validation (no publish without assigned taxonomy/attributes).
|
|
||||||
- Linker suggestions grouped by cluster role; optimizer scoring for coverage.
|
|
||||||
- Progress dashboards per site/cluster.
|
|
||||||
|
|
||||||
### Workstream D – Publishing & Sync
|
|
||||||
- Finish WordPress taxonomy + product attribute sync (bi-directional).
|
|
||||||
- Update deployment adapters to honor new metadata; ensure renderer reads cluster/taxonomy info.
|
|
||||||
- Provide sync dashboards with health indicators, last sync timestamps.
|
|
||||||
- Final regression + staging sign-off.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Milestones & Deliverables
|
|
||||||
|
|
||||||
| Milestone | Key Deliverables | Dependencies |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| M1 – Schema Ready | Migrations merged, services deployed, API docs updated. | None. |
|
|
||||||
| M2 – Guided Planner & Wizard | New UI flows live behind feature flag, analytics instrumented. | M1. |
|
|
||||||
| M3 – Metadata-Driven Writer | Writer/linker/optimizer using new relations, validation in place. | M2. |
|
|
||||||
| M4 – Publish & Sync Alpha | WordPress sync parity, deployment QA, release notes. | M3. |
|
|
||||||
|
|
||||||
Each milestone includes QA checklist, documentation update (master-docs + in-app help), and telemetry validation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Dependencies & Risks
|
|
||||||
- **Redis/Celery availability** – AI flows block if infra not running; ensure monitoring.
|
|
||||||
- **Migration coordination** – new tables touched by multiple services; schedule maintenance window.
|
|
||||||
- **WordPress API variance** – taxonomy/product endpoints vary by site; need robust error handling + manual override UI.
|
|
||||||
- **User learning curve** – wizard adds structure; include onboarding tips and inline docs.
|
|
||||||
|
|
||||||
Mitigations described in `02-workflows.md` (UX guardrails) and `03-phase-reports.md` (lessons learned).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Communication & Tracking
|
|
||||||
- Maintain status in `part2-dev/README.md` with links to this roadmap.
|
|
||||||
- Sprint boards map tasks back to stage/milestone.
|
|
||||||
- Release notes reference doc IDs here for posterity.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## References
|
|
||||||
- Strategy context: `01-strategy.md`
|
|
||||||
- Detailed workflows: `02-workflows.md`
|
|
||||||
- Historical reports: `03-phase-reports.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Stage Implementation Details
|
|
||||||
|
|
||||||
### Stage 1 – Data & Services Foundation
|
|
||||||
**Goal:** Ship all schema + backend building blocks so later stages can focus on UX.
|
|
||||||
|
|
||||||
**Backend**
|
|
||||||
- Migrations for `SiteBlueprintCluster`, `SiteBlueprintTaxonomy`, `WorkflowState`, writer mapping tables.
|
|
||||||
- `WorkflowStateService` with REST endpoints for fetching/updating step status.
|
|
||||||
- `TaxonomyService` (CRUD/import/export) and `ClusteringService` prompt upgrade for multi-dimensional clusters.
|
|
||||||
- API validation hooks (`validate_clusters_attached`, `validate_taxonomies_ready`, etc.) consumed by wizard gating.
|
|
||||||
- Serializer updates exposing entity metadata to frontend.
|
|
||||||
|
|
||||||
**Frontend**
|
|
||||||
- Minimal work (feature flags + API clients) to prepare for new endpoints; no user-facing change yet.
|
|
||||||
|
|
||||||
**Testing/Acceptance**
|
|
||||||
- Migration dry run, rollback verified.
|
|
||||||
- Unit/integration tests for services and validators.
|
|
||||||
- API documentation updated; Postman suite green.
|
|
||||||
|
|
||||||
### Stage 2 – Planner + Wizard UX
|
|
||||||
**Goal:** Deliver the guided, state-aware planning experience.
|
|
||||||
|
|
||||||
**Backend**
|
|
||||||
- Ensure planner endpoints return cluster metrics, taxonomy suggestions, validation errors.
|
|
||||||
- Audit logging for workflow transitions.
|
|
||||||
|
|
||||||
**Frontend**
|
|
||||||
- Implement `builderWorkflowStore` (Zustand) with resume capability, blocking logic, telemetry.
|
|
||||||
- Build wizard steps:
|
|
||||||
1. Business details + hosting detection.
|
|
||||||
2. Cluster assignment (coverage metrics, filters).
|
|
||||||
3. Taxonomy builder (create/import/match clusters).
|
|
||||||
4. AI sitemap review with checklist + edits.
|
|
||||||
5. Coverage validation summary.
|
|
||||||
6. Ideas hand-off with secondary prompt.
|
|
||||||
- Update planner cluster/taxonomy management screens (matrix view, inline warnings).
|
|
||||||
|
|
||||||
**UX Guardrails**
|
|
||||||
- Progress indicator with completion badges.
|
|
||||||
- Disabled buttons explain “what’s missing”.
|
|
||||||
- CTAs back to Planner when prerequisites unmet.
|
|
||||||
|
|
||||||
**Testing/Acceptance**
|
|
||||||
- Manual script for full wizard flow (new IGNY8 site + WP-linked site).
|
|
||||||
- Cypress/e2e coverage for gating + resume behavior.
|
|
||||||
- Telemetry events for step completion firing.
|
|
||||||
|
|
||||||
### Stage 3 – Writer / Linker / Optimizer Enhancements
|
|
||||||
**Goal:** Propagate metadata end-to-end and enforce validation.
|
|
||||||
|
|
||||||
**Backend**
|
|
||||||
- Ensure ideas/tasks/content writer pipeline populates `entity_type`, `taxonomy_id`, `cluster_role`, `product_data`.
|
|
||||||
- Linker/optimizer services consume mapping tables and expose coverage metrics via APIs.
|
|
||||||
- Progress dashboard endpoints (per site/cluster) for frontend widgets.
|
|
||||||
|
|
||||||
**Frontend**
|
|
||||||
- Planner Ideas + Writer Tasks lists show entity/taxonomy chips and warnings.
|
|
||||||
- Writer editor sidebar summarizing target cluster, taxonomy, attribute requirements; validation prompts before publish.
|
|
||||||
- Linker UI grouped by cluster role; Optimizer dashboards show dimension scorecards and “next actions”.
|
|
||||||
- Sites module progress bars summarizing blueprint completion.
|
|
||||||
|
|
||||||
**Testing/Acceptance**
|
|
||||||
- Regression on task/content creation flows.
|
|
||||||
- QA verifies publish blocked when metadata missing.
|
|
||||||
- Performance profiling on linker/optimizer queries with new joins.
|
|
||||||
|
|
||||||
### Stage 4 – Publishing & Sync Integration
|
|
||||||
**Goal:** Achieve parity between IGNY8-hosted and WordPress-hosted sites.
|
|
||||||
|
|
||||||
**Backend**
|
|
||||||
- Complete WordPress taxonomy/product attribute import/export in `ContentSyncService`.
|
|
||||||
- Enhance `SitesRendererAdapter` to include cluster/taxonomy metadata for navigation + internal linking.
|
|
||||||
- Sync health endpoints (last sync, mismatched taxonomies, errors) and deployment logs/rollback hooks.
|
|
||||||
|
|
||||||
**Frontend**
|
|
||||||
- Sync dashboard showing taxonomy parity, cluster coverage, last sync status, manual retry/reconcile actions.
|
|
||||||
- Deployment panel summarizing readiness (clusters covered, content publish state) before enabling deploy.
|
|
||||||
- Notifications/toasts for sync success/failure and deployments.
|
|
||||||
|
|
||||||
**Testing/Acceptance**
|
|
||||||
- End-to-end tests: IGNY8 deployment, WP publish for posts/products/services (blog/ecom/company templates).
|
|
||||||
- Fallback behavior validated (auto-creating missing taxonomies).
|
|
||||||
- Final QA checklist signed off; release notes + training updates distributed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Stage Exit Criteria
|
|
||||||
|
|
||||||
| Stage | Exit Criteria |
|
|
||||||
| --- | --- |
|
|
||||||
| 1 | Migrations live, services stable, existing Planner/Writer flows unaffected, docs updated. |
|
|
||||||
| 2 | Wizard fully usable with telemetry + QA sign-off; planner UIs reflect new taxonomy data. |
|
|
||||||
| 3 | Writer/linker/optimizer using metadata, publishing blocked without required info, dashboards live. |
|
|
||||||
| 4 | WordPress parity achieved, deployments verified, sync dashboards green, release announced. |
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# Planning Docs Consolidation Plan
|
|
||||||
|
|
||||||
## 1. Inventory & Tags
|
|
||||||
|
|
||||||
| File | Scope | Notes |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `planning/ARCHITECTURE_CONTEXT.md` | Contextual overview of Part 2 goals, stakeholders, constraints. | High-level reference. |
|
|
||||||
| `planning/CONTENT-WORKFLOW-DIAGRAM.md` | Detailed content lifecycle diagrams + descriptions. | Visual/text workflow. |
|
|
||||||
| `planning/sample-usage-limits-credit-system` | Legacy notes on credit usage examples. | Likely merge into workflows appendix. |
|
|
||||||
| `IGNY8-HOLISTIC-ARCHITECTURE-PLAN.md` | Comprehensive architecture narrative (cross-domain). | Overlaps with strategy sections. |
|
|
||||||
| `IGNY8-IMPLEMENTATION-PLAN.md` | Detailed step-by-step implementation roadmap. | Overlaps with roadmap bundle. |
|
|
||||||
| `Igny8-part-2-plan.md` | Program-level planning doc with milestones. | Should anchor roadmap bundle. |
|
|
||||||
| `PHASE-0-4-FOUNDATION-TO-LINKER-OPTIMIZER.md` | Phase-specific execution report for early phases. | Belongs in phase reports bundle. |
|
|
||||||
| `PHASE-5-7-9-SITES-RENDERER-INTEGRATION-UI.md` | Later phase report (sites renderer, UI). | Phase reports bundle. |
|
|
||||||
| `PHASE-8-UNIVERSAL-CONTENT-TYPES.md` | Focused phase doc on UCT. | Phase reports bundle. |
|
|
||||||
| `PHASE-IMPLEMENTATION-VERIFICATION-REPORT.md` | Consolidated verification results. | Phase reports bundle. |
|
|
||||||
| `PHASE-5-MIGRATION-VERIFICATION-REPORT.md` | Specific verification log. | Phase reports bundle (appendix). |
|
|
||||||
| `PHASE-5-MIGRATION-FINAL-VERIFICATION.md` | Final pass notes. | Combine with verification. |
|
|
||||||
| `README.md` | Folder guidance. | Needs update after consolidation. |
|
|
||||||
|
|
||||||
## 2. Target Bundles
|
|
||||||
|
|
||||||
| Bundle | New File | Contents |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Strategy & Architecture | `planning/01-strategy.md` | Merge `ARCHITECTURE_CONTEXT.md` + relevant sections from `IGNY8-HOLISTIC-ARCHITECTURE-PLAN.md`. |
|
|
||||||
| Workflows & Systems | `planning/02-workflows.md` | Combine `CONTENT-WORKFLOW-DIAGRAM.md`, sample credit usage appendix, excerpts describing planner/writer/site builder alignment (pull from holistic plan where needed). |
|
|
||||||
| Phase & Verification Reports | `planning/03-phase-reports.md` | Consolidate all PHASE* docs + implementation verification into chronological sections with TOC anchors. |
|
|
||||||
| Roadmap & Implementation | `planning/04-roadmap.md` | Merge `Igny8-part-2-plan.md` + `IGNY8-IMPLEMENTATION-PLAN.md`, highlight milestones/stage gates. |
|
|
||||||
|
|
||||||
## 3. Merge & Cross-Link Plan
|
|
||||||
1. Create the four new bundle files with clear TOCs.
|
|
||||||
2. For each source doc, move content into appropriate section, preserving headings.
|
|
||||||
3. Add cross-links between bundles (e.g., roadmap references strategy section anchors).
|
|
||||||
4. Update `README.md` to describe new structure and point to bundle docs.
|
|
||||||
|
|
||||||
## 4. Cleanup Tasks
|
|
||||||
- Archive or delete superseded source files once content is moved (keep git history).
|
|
||||||
- Update any references in other docs (`master-docs`, repo README, etc.) to point to the new bundle filenames.
|
|
||||||
- Verify diagrams/images still referenced correctly (adjust paths if needed).
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
1. Approve bundle structure (or adjust naming).
|
|
||||||
2. Execute merges in order: strategy → workflows → phase reports → roadmap.
|
|
||||||
3. Run lint/format checks on updated markdown.
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
# Site Deployment Guide
|
|
||||||
|
|
||||||
## How Site Deployment Works
|
|
||||||
|
|
||||||
### Overview
|
|
||||||
When you deploy a site blueprint, the system:
|
|
||||||
1. Builds a site definition (merging Content from Writer into PageBlueprint blocks)
|
|
||||||
2. Writes it to the filesystem at `/data/app/sites-data/clients/{site_id}/v{version}/`
|
|
||||||
3. The Sites Renderer reads from this filesystem to serve the public site
|
|
||||||
|
|
||||||
### Deployment Process
|
|
||||||
|
|
||||||
#### Step 1: Prepare Your Site
|
|
||||||
1. **Generate Structure**: Create site blueprint with pages (has placeholder blocks)
|
|
||||||
2. **Generate Content**: Pages → Tasks → Content (Writer generates real content)
|
|
||||||
3. **Publish Content**: In Content Manager, set Content status to `'publish'` for each page
|
|
||||||
4. **Publish Pages**: In Page Manager, set Page status to `'published'` (for navigation)
|
|
||||||
|
|
||||||
#### Step 2: Deploy the Blueprint
|
|
||||||
**API Endpoint**: `POST /api/v1/publisher/deploy/{blueprint_id}/`
|
|
||||||
|
|
||||||
**What Happens**:
|
|
||||||
- `SitesRendererAdapter.deploy()` is called
|
|
||||||
- For each page, it finds the associated Writer Task
|
|
||||||
- If Content exists and is published, it uses `Content.json_blocks` instead of blueprint placeholders
|
|
||||||
- Builds complete site definition with all pages
|
|
||||||
- Writes to filesystem: `/data/app/sites-data/clients/{site_id}/v{version}/site.json`
|
|
||||||
- Creates deployment record
|
|
||||||
|
|
||||||
#### Step 3: Access Your Site
|
|
||||||
**Public URL**: `https://sites.igny8.com/{siteSlug}`
|
|
||||||
|
|
||||||
**How It Works**:
|
|
||||||
- Sites Renderer loads site definition from filesystem (or API fallback)
|
|
||||||
- Shows navigation menu with all published pages
|
|
||||||
- Home route (`/siteSlug`) shows only home page content
|
|
||||||
- Page routes (`/siteSlug/pageSlug`) show specific page content
|
|
||||||
|
|
||||||
### Navigation Menu
|
|
||||||
|
|
||||||
The navigation menu automatically includes:
|
|
||||||
- **Home** link (always shown)
|
|
||||||
- All pages with status `'published'` or `'ready'` (excluding home)
|
|
||||||
- Pages are sorted by their `order` field
|
|
||||||
|
|
||||||
### Page Routing
|
|
||||||
|
|
||||||
- **Homepage**: `https://sites.igny8.com/{siteSlug}` → Shows home page only
|
|
||||||
- **Individual Pages**: `https://sites.igny8.com/{siteSlug}/{pageSlug}` → Shows that specific page
|
|
||||||
- Example: `https://sites.igny8.com/auto-g8/products`
|
|
||||||
- Example: `https://sites.igny8.com/auto-g8/blog`
|
|
||||||
|
|
||||||
### Content Merging
|
|
||||||
|
|
||||||
When deploying:
|
|
||||||
- **If Content is published**: Uses `Content.json_blocks` (actual written content)
|
|
||||||
- **If Content not published**: Uses `PageBlueprint.blocks_json` (placeholder blocks)
|
|
||||||
- **Content takes precedence**: Published Content always replaces blueprint placeholders
|
|
||||||
|
|
||||||
### Important Notes
|
|
||||||
|
|
||||||
1. **Two Statuses**:
|
|
||||||
- `PageBlueprint.status = 'published'` → Controls page visibility in navigation
|
|
||||||
- `Content.status = 'publish'` → Controls which content is used (real vs placeholder)
|
|
||||||
|
|
||||||
2. **Redeploy Required**: After publishing Content, you must **redeploy** the site for changes to appear
|
|
||||||
|
|
||||||
3. **All Pages Deployed**: The deployment includes ALL pages from the blueprint, but only published pages show in navigation
|
|
||||||
|
|
||||||
4. **Navigation Auto-Generated**: If no explicit navigation is set in blueprint, it auto-generates from published pages
|
|
||||||
|
|
||||||
### Troubleshooting
|
|
||||||
|
|
||||||
**Problem**: Navigation only shows "Home"
|
|
||||||
- **Solution**: Make sure other pages have status `'published'` or `'ready'` in Page Manager
|
|
||||||
|
|
||||||
**Problem**: Pages show placeholder content instead of real content
|
|
||||||
- **Solution**:
|
|
||||||
1. Check Content status is `'publish'` in Content Manager
|
|
||||||
2. Redeploy the site blueprint
|
|
||||||
|
|
||||||
**Problem**: Pages not accessible via URL
|
|
||||||
- **Solution**: Make sure pages have status `'published'` or `'ready'` and the site is deployed
|
|
||||||
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
# Site Builder & AI Functions - Comprehensive Audit Report
|
|
||||||
|
|
||||||
## Executive Summary
|
|
||||||
This audit identifies all requirements, dependencies, and gaps for the Site Builder functionality and related AI generation features. The primary issues are:
|
|
||||||
1. **Database migrations not applied** - Site builder tables don't exist
|
|
||||||
2. **Redis/Celery not running** - AI tasks can't execute asynchronously
|
|
||||||
3. All code components are present and properly configured
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Requirements Audit Table
|
|
||||||
|
|
||||||
| Category | Component | Requirement | Status | Location/Notes | Action Required |
|
|
||||||
|----------|-----------|-------------|--------|----------------|-----------------|
|
|
||||||
| **Database** | Migrations | `0001_initial.py` - SiteBlueprint, PageBlueprint tables | ✅ EXISTS | `backend/igny8_core/business/site_building/migrations/0001_initial.py` | ❌ **NOT APPLIED** - Run migrations |
|
|
||||||
| **Database** | Migrations | `0002_sitebuilder_metadata.py` - BusinessType, AudienceProfile, BrandPersonality, HeroImageryDirection | ✅ EXISTS | `backend/igny8_core/business/site_building/migrations/0002_sitebuilder_metadata.py` | ❌ **NOT APPLIED** - Run migrations |
|
|
||||||
| **Database** | Table | `igny8_site_blueprints` | ❌ MISSING | Should be created by 0001_initial.py | Run `python manage.py migrate site_building` |
|
|
||||||
| **Database** | Table | `igny8_page_blueprints` | ❌ MISSING | Should be created by 0001_initial.py | Run `python manage.py migrate site_building` |
|
|
||||||
| **Database** | Table | `igny8_site_builder_business_types` | ❌ MISSING | Should be created by 0002_sitebuilder_metadata.py | Run `python manage.py migrate site_building` |
|
|
||||||
| **Database** | Table | `igny8_site_builder_audience_profiles` | ❌ MISSING | Should be created by 0002_sitebuilder_metadata.py | Run `python manage.py migrate site_building` |
|
|
||||||
| **Database** | Table | `igny8_site_builder_brand_personalities` | ❌ MISSING | Should be created by 0002_sitebuilder_metadata.py | Run `python manage.py migrate site_building` |
|
|
||||||
| **Database** | Table | `igny8_site_builder_hero_imagery` | ❌ MISSING | Should be created by 0002_sitebuilder_metadata.py | Run `python manage.py migrate site_building` |
|
|
||||||
| **Database** | Seed Data | Business types, audience profiles, brand personalities, hero imagery | ✅ DEFINED | Migration 0002 includes `seed_site_builder_metadata()` function | Will seed automatically when migration runs |
|
|
||||||
| **App Config** | Django App | `igny8_core.business.site_building.apps.SiteBuildingConfig` | ✅ REGISTERED | `backend/igny8_core/settings.py:55` | No action needed |
|
|
||||||
| **App Config** | Django App | `igny8_core.modules.site_builder.apps.SiteBuilderConfig` | ✅ REGISTERED | `backend/igny8_core/settings.py:59` | No action needed |
|
|
||||||
| **Models** | SiteBlueprint | Model with all fields (name, description, config_json, structure_json, status, hosting_type, version) | ✅ EXISTS | `backend/igny8_core/business/site_building/models.py:10-83` | No action needed |
|
|
||||||
| **Models** | PageBlueprint | Model with all fields (site_blueprint, slug, title, type, blocks_json, status, order) | ✅ EXISTS | `backend/igny8_core/business/site_building/models.py:85-166` | No action needed |
|
|
||||||
| **Models** | BusinessType | Model extending SiteBuilderOption | ✅ EXISTS | `backend/igny8_core/business/site_building/models.py:189-194` | No action needed |
|
|
||||||
| **Models** | AudienceProfile | Model extending SiteBuilderOption | ✅ EXISTS | `backend/igny8_core/business/site_building/models.py:197-202` | No action needed |
|
|
||||||
| **Models** | BrandPersonality | Model extending SiteBuilderOption | ✅ EXISTS | `backend/igny8_core/business/site_building/models.py:205-210` | No action needed |
|
|
||||||
| **Models** | HeroImageryDirection | Model extending SiteBuilderOption | ✅ EXISTS | `backend/igny8_core/business/site_building/models.py:213-218` | No action needed |
|
|
||||||
| **Services** | StructureGenerationService | Service to trigger AI structure generation | ✅ EXISTS | `backend/igny8_core/business/site_building/services/structure_generation_service.py` | No action needed |
|
|
||||||
| **Services** | PageGenerationService | Service to generate page content via Writer | ✅ EXISTS | `backend/igny8_core/business/site_building/services/page_generation_service.py` | No action needed |
|
|
||||||
| **Services** | SiteBuilderFileService | File management service | ✅ EXISTS | `backend/igny8_core/business/site_building/services/file_management_service.py` | No action needed |
|
|
||||||
| **AI Functions** | GenerateSiteStructureFunction | AI function class | ✅ EXISTS | `backend/igny8_core/ai/functions/generate_site_structure.py` | No action needed |
|
|
||||||
| **AI Functions** | Function Registration | `generate_site_structure` registered in registry | ✅ REGISTERED | `backend/igny8_core/ai/registry.py:97-112` | No action needed |
|
|
||||||
| **AI Functions** | AI Task Dispatch | `run_ai_task` Celery task | ✅ EXISTS | `backend/igny8_core/ai/tasks.py:12-147` | No action needed |
|
|
||||||
| **AI Functions** | Prompt Template | `site_structure_generation` prompt | ✅ EXISTS | `backend/igny8_core/ai/prompts.py:242-307` | No action needed |
|
|
||||||
| **AI Functions** | Prompt Mapping | Function name → prompt type mapping | ✅ CONFIGURED | `backend/igny8_core/ai/prompts.py:599` | No action needed |
|
|
||||||
| **API Endpoints** | SiteBlueprintViewSet | CRUD + generate_structure action | ✅ EXISTS | `backend/igny8_core/modules/site_builder/views.py:32-81` | No action needed |
|
|
||||||
| **API Endpoints** | PageBlueprintViewSet | CRUD + generate_content action | ✅ EXISTS | `backend/igny8_core/modules/site_builder/views.py:141-172` | No action needed |
|
|
||||||
| **API Endpoints** | SiteBuilderMetadataView | Metadata endpoint for dropdowns | ✅ EXISTS | `backend/igny8_core/modules/site_builder/views.py:216-250` | ❌ **FAILS** - Needs DB tables |
|
|
||||||
| **API Endpoints** | SiteAssetView | File upload/download | ✅ EXISTS | `backend/igny8_core/modules/site_builder/views.py:175-213` | No action needed |
|
|
||||||
| **API URLs** | Site Builder URLs | `/api/v1/site-builder/` routes | ✅ CONFIGURED | `backend/igny8_core/modules/site_builder/urls.py` | No action needed |
|
|
||||||
| **API URLs** | URL Registration | Included in main urls.py | ✅ REGISTERED | `backend/igny8_core/urls.py:30` | No action needed |
|
|
||||||
| **Billing** | Credit Cost | `site_structure_generation: 50 credits` | ✅ DEFINED | `backend/igny8_core/business/billing/constants.py:13` | No action needed |
|
|
||||||
| **Billing** | Credit Cost | `site_page_generation: 20 credits` | ✅ DEFINED | `backend/igny8_core/business/billing/constants.py:14` | No action needed |
|
|
||||||
| **Billing** | Credit Check | CreditService.check_credits() called | ✅ IMPLEMENTED | `backend/igny8_core/business/site_building/services/structure_generation_service.py:57` | No action needed |
|
|
||||||
| **Celery** | Configuration | Celery app configured | ✅ CONFIGURED | `backend/igny8_core/celery.py` | No action needed |
|
|
||||||
| **Celery** | Settings | CELERY_BROKER_URL, CELERY_RESULT_BACKEND | ✅ CONFIGURED | `backend/igny8_core/settings.py:497-508` | No action needed |
|
|
||||||
| **Celery** | Worker Container | `igny8_celery_worker` in docker-compose | ✅ DEFINED | `docker-compose.app.yml:105-128` | ❌ **NOT RUNNING** - Start container |
|
|
||||||
| **Celery** | Beat Container | `igny8_celery_beat` in docker-compose | ✅ DEFINED | `docker-compose.app.yml:130-153` | ❌ **NOT RUNNING** - Start container |
|
|
||||||
| **Redis** | Configuration | REDIS_HOST, REDIS_PORT env vars | ✅ CONFIGURED | `docker-compose.app.yml:38-39, 115-116, 140-141` | No action needed |
|
|
||||||
| **Redis** | Redis Service | External Redis service | ❌ **NOT ACCESSIBLE** | Expected from infra stack | ❌ **NOT RUNNING** - Start Redis service |
|
|
||||||
| **Dependencies** | Python Packages | celery>=5.3.0 | ✅ IN REQUIREMENTS | `backend/requirements.txt:11` | No action needed |
|
|
||||||
| **Dependencies** | Python Packages | redis | ✅ IN REQUIREMENTS | `backend/requirements.txt:4` | No action needed |
|
|
||||||
| **Dependencies** | Python Packages | Django>=5.2.7 | ✅ IN REQUIREMENTS | `backend/requirements.txt:1` | No action needed |
|
|
||||||
| **Dependencies** | Python Packages | djangorestframework | ✅ IN REQUIREMENTS | `backend/requirements.txt:6` | No action needed |
|
|
||||||
| **Frontend** | API Client | siteBuilderApi functions | ✅ EXISTS | `frontend/src/services/siteBuilder.api.ts` | No action needed |
|
|
||||||
| **Frontend** | Store | useBuilderStore with metadata loading | ✅ EXISTS | `frontend/src/store/builderStore.ts:399-412` | No action needed |
|
|
||||||
| **Logging** | StructureGenerationService | Logging statements | ✅ IMPLEMENTED | `backend/igny8_core/business/site_building/services/structure_generation_service.py:49-53, 98-102` | No action needed |
|
|
||||||
| **Logging** | GenerateSiteStructure | Logging statements | ✅ IMPLEMENTED | `backend/igny8_core/ai/functions/generate_site_structure.py:135` | No action needed |
|
|
||||||
| **Error Handling** | InsufficientCreditsError | Handled in StructureGenerationService | ✅ IMPLEMENTED | `backend/igny8_core/business/site_building/services/structure_generation_service.py:58-61` | No action needed |
|
|
||||||
| **Error Handling** | Celery Fallback | Synchronous execution if Celery unavailable | ✅ IMPLEMENTED | `backend/igny8_core/business/site_building/services/structure_generation_service.py:109-115` | No action needed |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Critical Issues Summary
|
|
||||||
|
|
||||||
### 🔴 **CRITICAL - Database Migrations Not Applied**
|
|
||||||
**Impact**: All Site Builder endpoints fail with `ProgrammingError: relation "igny8_site_builder_business_types" does not exist`
|
|
||||||
|
|
||||||
**Root Cause**: Migrations exist but haven't been run on the database
|
|
||||||
|
|
||||||
**Fix Required**:
|
|
||||||
```bash
|
|
||||||
# Step 1: Navigate to app directory
|
|
||||||
cd /data/app/igny8
|
|
||||||
|
|
||||||
# Step 2: Create migrations if model changes exist
|
|
||||||
docker compose -f docker-compose.app.yml -p igny8-app exec igny8_backend python manage.py makemigrations site_building
|
|
||||||
|
|
||||||
# Step 3: Apply migrations
|
|
||||||
docker compose -f docker-compose.app.yml -p igny8-app exec igny8_backend python manage.py migrate site_building
|
|
||||||
|
|
||||||
# Alternative: If backend is in infra stack, use:
|
|
||||||
# cd /data/app
|
|
||||||
# docker compose -f docker-compose.yml -p igny8-infra exec <backend_container_name> python manage.py makemigrations site_building
|
|
||||||
# docker compose -f docker-compose.yml -p igny8-infra exec <backend_container_name> python manage.py migrate site_building
|
|
||||||
```
|
|
||||||
|
|
||||||
**Files Affected**:
|
|
||||||
- `backend/igny8_core/business/site_building/migrations/0001_initial.py`
|
|
||||||
- `backend/igny8_core/business/site_building/migrations/0002_sitebuilder_metadata.py`
|
|
||||||
|
|
||||||
**Tables That Will Be Created**:
|
|
||||||
1. `igny8_site_blueprints`
|
|
||||||
2. `igny8_page_blueprints`
|
|
||||||
3. `igny8_site_builder_business_types`
|
|
||||||
4. `igny8_site_builder_audience_profiles`
|
|
||||||
5. `igny8_site_builder_brand_personalities`
|
|
||||||
6. `igny8_site_builder_hero_imagery`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🔴 **CRITICAL - Redis Not Running**
|
|
||||||
**Impact**: Celery tasks can't be queued, AI generation fails silently or runs synchronously
|
|
||||||
|
|
||||||
**Root Cause**: Redis service is not accessible (Connection refused errors in logs)
|
|
||||||
|
|
||||||
**Fix Required**:
|
|
||||||
1. Ensure Redis service is running in the infra stack
|
|
||||||
2. Verify network connectivity between backend and Redis
|
|
||||||
3. Check Redis is accessible at `redis:6379` from backend container
|
|
||||||
|
|
||||||
**Configuration**:
|
|
||||||
- Expected: `redis://redis:6379/0`
|
|
||||||
- Environment vars: `REDIS_HOST=redis`, `REDIS_PORT=6379`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🔴 **CRITICAL - Celery Worker Not Running**
|
|
||||||
**Impact**: Even if Redis is fixed, AI tasks won't execute because no worker is processing them
|
|
||||||
|
|
||||||
**Root Cause**: `igny8_celery_worker` container is not running
|
|
||||||
|
|
||||||
**Fix Required**:
|
|
||||||
```bash
|
|
||||||
# Navigate to app directory
|
|
||||||
cd /data/app/igny8
|
|
||||||
|
|
||||||
# Start Celery worker
|
|
||||||
docker compose -f docker-compose.app.yml -p igny8-app up -d igny8_celery_worker
|
|
||||||
|
|
||||||
# Verify it's running
|
|
||||||
docker compose -f docker-compose.app.yml -p igny8-app ps igny8_celery_worker
|
|
||||||
```
|
|
||||||
|
|
||||||
**Container Configuration**: `docker-compose.app.yml:105-128`
|
|
||||||
|
|
||||||
**Note**: If backend is in infra stack, Celery worker may also be there. Check which stack contains the backend service.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🟡 **WARNING - Celery Beat Not Running**
|
|
||||||
**Impact**: Periodic tasks (credit replenishment, automation rules) won't run
|
|
||||||
|
|
||||||
**Root Cause**: `igny8_celery_beat` container is not running
|
|
||||||
|
|
||||||
**Fix Required**:
|
|
||||||
```bash
|
|
||||||
# Navigate to app directory
|
|
||||||
cd /data/app/igny8
|
|
||||||
|
|
||||||
# Start Celery beat
|
|
||||||
docker compose -f docker-compose.app.yml -p igny8-app up -d igny8_celery_beat
|
|
||||||
|
|
||||||
# Verify it's running
|
|
||||||
docker compose -f docker-compose.app.yml -p igny8-app ps igny8_celery_beat
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verification Checklist
|
|
||||||
|
|
||||||
After fixing the critical issues, verify:
|
|
||||||
|
|
||||||
- [ ] Database migrations applied: `python manage.py showmigrations site_building` shows all as `[X]`
|
|
||||||
- [ ] Tables exist: Query `igny8_site_builder_business_types` returns data
|
|
||||||
- [ ] Redis accessible: `redis-cli -h redis ping` returns `PONG`
|
|
||||||
- [ ] Celery worker running: `docker ps | grep celery_worker` shows container
|
|
||||||
- [ ] Celery worker connected: Check logs for "celery@hostname ready"
|
|
||||||
- [ ] Metadata endpoint works: `GET /api/v1/site-builder/metadata/` returns 200 with data
|
|
||||||
- [ ] AI task can be queued: Check logs for `[StructureGenerationService] Queued AI task`
|
|
||||||
- [ ] AI task executes: Check logs for `run_ai_task STARTED: generate_site_structure`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Code Quality Assessment
|
|
||||||
|
|
||||||
### ✅ **Strengths**
|
|
||||||
1. **Complete Implementation**: All core components are implemented
|
|
||||||
2. **Proper Separation**: Business logic separated from API layer
|
|
||||||
3. **Error Handling**: Graceful fallbacks for Celery unavailability
|
|
||||||
4. **Credit System**: Properly integrated with billing system
|
|
||||||
5. **Logging**: Comprehensive logging throughout the flow
|
|
||||||
6. **Type Hints**: Good use of type hints in services
|
|
||||||
7. **Documentation**: Models and services have docstrings
|
|
||||||
|
|
||||||
### ⚠️ **Potential Improvements**
|
|
||||||
1. **Migration Dependencies**: Ensure migration dependencies are correct (currently depends on `igny8_core_auth.0014` and `writer.0009`)
|
|
||||||
2. **Error Messages**: Could be more user-friendly in API responses
|
|
||||||
3. **Testing**: Test files exist but may need updates for current implementation
|
|
||||||
4. **Monitoring**: Consider adding metrics for AI task success/failure rates
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
1. **Immediate**: Run database migrations
|
|
||||||
2. **Immediate**: Start Redis service (if in infra stack)
|
|
||||||
3. **Immediate**: Start Celery worker container
|
|
||||||
4. **Immediate**: Start Celery beat container (optional but recommended)
|
|
||||||
5. **Verification**: Test metadata endpoint
|
|
||||||
6. **Verification**: Test structure generation endpoint
|
|
||||||
7. **Monitoring**: Watch logs for AI task execution
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Files Reference
|
|
||||||
|
|
||||||
### Core Models
|
|
||||||
- `backend/igny8_core/business/site_building/models.py` - All Site Builder models
|
|
||||||
|
|
||||||
### Services
|
|
||||||
- `backend/igny8_core/business/site_building/services/structure_generation_service.py` - AI structure generation
|
|
||||||
- `backend/igny8_core/business/site_building/services/page_generation_service.py` - Page content generation
|
|
||||||
- `backend/igny8_core/business/site_building/services/file_management_service.py` - File handling
|
|
||||||
|
|
||||||
### AI Functions
|
|
||||||
- `backend/igny8_core/ai/functions/generate_site_structure.py` - AI function implementation
|
|
||||||
- `backend/igny8_core/ai/tasks.py` - Celery task dispatcher
|
|
||||||
- `backend/igny8_core/ai/registry.py` - Function registry
|
|
||||||
|
|
||||||
### API Layer
|
|
||||||
- `backend/igny8_core/modules/site_builder/views.py` - API endpoints
|
|
||||||
- `backend/igny8_core/modules/site_builder/urls.py` - URL routing
|
|
||||||
- `backend/igny8_core/modules/site_builder/serializers.py` - Request/response serialization
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
- `backend/igny8_core/settings.py` - Django settings
|
|
||||||
- `backend/igny8_core/celery.py` - Celery configuration
|
|
||||||
- `docker-compose.app.yml` - Container definitions
|
|
||||||
- `backend/requirements.txt` - Python dependencies
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Report Generated**: Based on comprehensive codebase analysis
|
|
||||||
**Status**: All code components present, infrastructure needs attention
|
|
||||||
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
# Site Builder Wizard Integration Plan
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Integrate the Site Builder wizard directly into the main frontend app (`frontend/src/pages/Sites/Builder/`), using the same UI kit, state stores, and API helpers as the rest of the dashboard. The legacy `sites/src/builder` + `sites/src/renderer` code has been removed, so the only viable implementation path is the unified Sites module.
|
|
||||||
|
|
||||||
## Current State
|
|
||||||
|
|
||||||
### ✅ What's Done
|
|
||||||
- Legacy builder/renderer folders removed from Sites container (no more parallel UI)
|
|
||||||
- Type definitions created in `frontend/src/types/siteBuilder.ts`
|
|
||||||
- API helper created in `frontend/src/services/siteBuilder.api.ts`
|
|
||||||
|
|
||||||
### ⚠️ What's Missing
|
|
||||||
- Builder store not yet created in the main app
|
|
||||||
- Wizard steps/page still placeholder in `frontend/src/pages/Sites/Builder/`
|
|
||||||
- No Tailwind/CX styling hooked into shared UI kit
|
|
||||||
- Routes/menu point to placeholder
|
|
||||||
- Tests/docs still reference old structure
|
|
||||||
- Sites container still contains stale references (needs cleanup after integration)
|
|
||||||
|
|
||||||
## Integration Plan
|
|
||||||
|
|
||||||
### Phase 1: Create API Service Layer ✅
|
|
||||||
**Location**: `frontend/src/services/siteBuilder.api.ts`
|
|
||||||
|
|
||||||
**Tasks**:
|
|
||||||
1. Create `siteBuilderApi` using `fetchAPI` pattern (not axios)
|
|
||||||
2. Functions needed:
|
|
||||||
- `listBlueprints()`
|
|
||||||
- `createBlueprint(payload)`
|
|
||||||
- `generateStructure(blueprintId, payload)`
|
|
||||||
- `listPages(blueprintId)`
|
|
||||||
- `generateAllPages(blueprintId, options)`
|
|
||||||
- `createTasksForPages(blueprintId, pageIds)`
|
|
||||||
|
|
||||||
**API Endpoints** (already exist in backend):
|
|
||||||
- `GET /api/v1/site-builder/blueprints/`
|
|
||||||
- `POST /api/v1/site-builder/blueprints/`
|
|
||||||
- `POST /api/v1/site-builder/blueprints/{id}/generate_structure/`
|
|
||||||
- `GET /api/v1/site-builder/pages/?site_blueprint={id}`
|
|
||||||
- `POST /api/v1/site-builder/blueprints/{id}/generate_all_pages/`
|
|
||||||
- `POST /api/v1/site-builder/blueprints/{id}/create_tasks/`
|
|
||||||
|
|
||||||
### Phase 2: Create Zustand Store ⏳
|
|
||||||
**Location**: `frontend/src/store/builderStore.ts`
|
|
||||||
|
|
||||||
**Tasks**:
|
|
||||||
1. Copy `builderStore.ts` from `sites/src/builder/state/`
|
|
||||||
2. Adapt to use `siteBuilderApi` instead of `builderApi`
|
|
||||||
3. Integrate with `useSiteStore` and `useSectorStore`:
|
|
||||||
- Auto-populate `siteId` from `useSiteStore().activeSite`
|
|
||||||
- Auto-populate `sectorId` from `useSectorStore().activeSector`
|
|
||||||
- Show site/sector selector if not set
|
|
||||||
|
|
||||||
**Store State**:
|
|
||||||
- `form: BuilderFormData` - Wizard form data
|
|
||||||
- `currentStep: number` - Current wizard step (0-3)
|
|
||||||
- `isSubmitting: boolean` - Generation in progress
|
|
||||||
- `activeBlueprint: SiteBlueprint | null` - Latest blueprint
|
|
||||||
- `pages: PageBlueprint[]` - Generated pages
|
|
||||||
- `error: string | null` - Error message
|
|
||||||
|
|
||||||
### Phase 3: Create Type Definitions ✅
|
|
||||||
**Location**: `frontend/src/types/siteBuilder.ts`
|
|
||||||
|
|
||||||
**Tasks**:
|
|
||||||
1. Copy types from `sites/src/builder/types/siteBuilder.ts`
|
|
||||||
2. Ensure compatibility with frontend's existing types
|
|
||||||
|
|
||||||
**Types Needed**:
|
|
||||||
- `HostingType`
|
|
||||||
- `StylePreferences`
|
|
||||||
- `BuilderFormData`
|
|
||||||
- `SiteBlueprint`
|
|
||||||
- `PageBlueprint`
|
|
||||||
- `PageBlock`
|
|
||||||
- `SiteStructure`
|
|
||||||
|
|
||||||
### Phase 4: Create Wizard Step Components ⏳
|
|
||||||
**Location**: `frontend/src/pages/Sites/Builder/steps/`
|
|
||||||
|
|
||||||
**Tasks**:
|
|
||||||
1. Copy step components from `sites/src/builder/pages/wizard/steps/`
|
|
||||||
2. Adapt to use frontend's UI components:
|
|
||||||
- Replace `Card` with `frontend/src/components/ui/card/Card`
|
|
||||||
- Replace custom inputs with Tailwind-styled inputs
|
|
||||||
- Use frontend's `Button` component
|
|
||||||
3. Adapt styles to Tailwind CSS:
|
|
||||||
- Remove `.sb-field`, `.sb-grid`, `.sb-pill` classes
|
|
||||||
- Use Tailwind utility classes instead
|
|
||||||
|
|
||||||
**Step Components**:
|
|
||||||
- `BusinessDetailsStep.tsx` - Site/sector selection, business info
|
|
||||||
- `BriefStep.tsx` - Business brief textarea
|
|
||||||
- `ObjectivesStep.tsx` - Objectives list with add/remove
|
|
||||||
- `StyleStep.tsx` - Style preferences (palette, typography, personality)
|
|
||||||
|
|
||||||
### Phase 5: Create Main Wizard Page ⏳
|
|
||||||
**Location**: `frontend/src/pages/Sites/Builder/Wizard.tsx`
|
|
||||||
|
|
||||||
**Tasks**:
|
|
||||||
1. Copy `WizardPage.tsx` from `sites/src/builder/pages/wizard/`
|
|
||||||
2. Adapt to frontend patterns:
|
|
||||||
- Use `PageMeta` component
|
|
||||||
- Use frontend's `Card` component
|
|
||||||
- Use frontend's `Button` component
|
|
||||||
- Use Tailwind CSS for styling
|
|
||||||
3. Integrate with stores:
|
|
||||||
- Auto-load active site/sector
|
|
||||||
- Show site/sector selector if needed
|
|
||||||
- Navigate to sites list on completion
|
|
||||||
|
|
||||||
**Features**:
|
|
||||||
- 4-step wizard with progress indicators
|
|
||||||
- Step navigation (Back/Next buttons)
|
|
||||||
- Form validation
|
|
||||||
- Blueprint generation on submit
|
|
||||||
- Error handling
|
|
||||||
- Loading states
|
|
||||||
|
|
||||||
### Phase 6: Create Site Definition Store (Optional) ⏳
|
|
||||||
**Location**: `frontend/src/store/siteDefinitionStore.ts`
|
|
||||||
|
|
||||||
**Tasks**:
|
|
||||||
1. Copy `siteDefinitionStore.ts` from `sites/src/builder/state/`
|
|
||||||
2. Use for preview functionality (if needed)
|
|
||||||
|
|
||||||
### Phase 7: Update Routing & Navigation ✅
|
|
||||||
**Location**: `frontend/src/App.tsx`
|
|
||||||
|
|
||||||
**Tasks**:
|
|
||||||
1. Ensure `/sites/builder` route points to new `Wizard.tsx`
|
|
||||||
2. Update navigation to show wizard in Sites section
|
|
||||||
|
|
||||||
### Phase 8: Fix Test File ✅
|
|
||||||
**Location**: `frontend/src/__tests__/sites/BulkGeneration.test.tsx`
|
|
||||||
|
|
||||||
**Tasks**:
|
|
||||||
1. Update import path from `site-builder/src/api/builder.api` to `services/siteBuilder.api`
|
|
||||||
2. Update mock path accordingly
|
|
||||||
|
|
||||||
### Phase 9: Testing ⏳ *(blocked by vitest not installed in dev env)*
|
|
||||||
**Tasks**:
|
|
||||||
1. Test wizard flow:
|
|
||||||
- Site selection
|
|
||||||
- Sector selection
|
|
||||||
- All 4 wizard steps
|
|
||||||
- Blueprint generation
|
|
||||||
- Error handling
|
|
||||||
2. Test integration:
|
|
||||||
- Site/sector auto-population
|
|
||||||
- Navigation
|
|
||||||
- API calls
|
|
||||||
|
|
||||||
### Phase 10: Cleanup ⏳
|
|
||||||
**Tasks**:
|
|
||||||
1. Stop `igny8_site_builder` container
|
|
||||||
2. Remove Docker image
|
|
||||||
3. Remove `/site-builder` folder
|
|
||||||
4. Update documentation
|
|
||||||
|
|
||||||
## File Structure After Integration
|
|
||||||
|
|
||||||
```
|
|
||||||
frontend/src/
|
|
||||||
├── pages/Sites/Builder/
|
|
||||||
│ ├── Wizard.tsx # Main wizard page (UPDATED)
|
|
||||||
│ ├── Preview.tsx # Preview page (keep placeholder for now)
|
|
||||||
│ ├── Blueprints.tsx # Blueprints list (already exists)
|
|
||||||
│ └── steps/ # NEW
|
|
||||||
│ ├── BusinessDetailsStep.tsx
|
|
||||||
│ ├── BriefStep.tsx
|
|
||||||
│ ├── ObjectivesStep.tsx
|
|
||||||
│ └── StyleStep.tsx
|
|
||||||
├── services/
|
|
||||||
│ └── siteBuilder.api.ts # NEW - API service
|
|
||||||
├── store/
|
|
||||||
│ ├── builderStore.ts # NEW - Builder state
|
|
||||||
│ └── siteDefinitionStore.ts # NEW - Site definition state (optional)
|
|
||||||
└── types/
|
|
||||||
└── siteBuilder.ts # NEW - Type definitions
|
|
||||||
```
|
|
||||||
|
|
||||||
## Key Adaptations Needed
|
|
||||||
|
|
||||||
### 1. API Client Pattern
|
|
||||||
**From** (sites container):
|
|
||||||
```typescript
|
|
||||||
import axios from 'axios';
|
|
||||||
const client = axios.create({ baseURL: BASE_PATH });
|
|
||||||
```
|
|
||||||
|
|
||||||
**To** (frontend):
|
|
||||||
```typescript
|
|
||||||
import { fetchAPI } from '../services/api';
|
|
||||||
// Use fetchAPI directly, no axios
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Component Library
|
|
||||||
**From** (sites container):
|
|
||||||
```typescript
|
|
||||||
import { Card } from '../../components/common/Card';
|
|
||||||
```
|
|
||||||
|
|
||||||
**To** (frontend):
|
|
||||||
```typescript
|
|
||||||
import { Card } from '../../../components/ui/card/Card';
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Styling
|
|
||||||
**From** (sites container):
|
|
||||||
```css
|
|
||||||
.sb-field { ... }
|
|
||||||
.sb-grid { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
**To** (frontend):
|
|
||||||
```tsx
|
|
||||||
className="flex flex-col gap-2"
|
|
||||||
className="grid grid-cols-2 gap-4"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Store Integration
|
|
||||||
**From** (sites container):
|
|
||||||
```typescript
|
|
||||||
// Manual siteId/sectorId input
|
|
||||||
```
|
|
||||||
|
|
||||||
**To** (frontend):
|
|
||||||
```typescript
|
|
||||||
import { useSiteStore } from '../../../store/siteStore';
|
|
||||||
import { useSectorStore } from '../../../store/sectorStore';
|
|
||||||
// Auto-populate from stores
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation Order
|
|
||||||
|
|
||||||
1. ✅ Create types (`types/siteBuilder.ts`)
|
|
||||||
2. ✅ Create API service (`services/siteBuilder.api.ts`)
|
|
||||||
3. ⏳ Create builder store (`store/builderStore.ts`)
|
|
||||||
4. ⏳ Create step components (`pages/Sites/Builder/steps/`)
|
|
||||||
5. ⏳ Create main wizard page (`pages/Sites/Builder/Wizard.tsx`)
|
|
||||||
6. ⏳ Fix test file(s)
|
|
||||||
7. ⏳ Test integration
|
|
||||||
8. ⏳ Cleanup site-builder container/image/docs
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
|
|
||||||
- ✅ Wizard loads in main app at `/sites/builder`
|
|
||||||
- ✅ Site/sector auto-populated from stores
|
|
||||||
- ✅ All 4 steps work correctly
|
|
||||||
- ✅ Blueprint generation works
|
|
||||||
- ✅ Error handling works
|
|
||||||
- ✅ Navigation works
|
|
||||||
- ✅ No references to `site-builder/` folder in code
|
|
||||||
- ✅ Test file updated
|
|
||||||
- ✅ Sites container removed or marked deprecated in compose
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Sites container will be deprecated once the wizard lives entirely inside the main app.
|
|
||||||
- Only integrate wizard into main frontend app (no parallel codepaths).
|
|
||||||
- Use frontend's existing patterns/components/stores for absolute consistency.
|
|
||||||
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
# Site Builder URLs and File Management
|
|
||||||
|
|
||||||
## Summary of Implementation
|
|
||||||
|
|
||||||
### ✅ Generate Page Content Step
|
|
||||||
|
|
||||||
**Location**: `frontend/src/pages/Sites/Builder/Preview.tsx`
|
|
||||||
|
|
||||||
**Implementation**:
|
|
||||||
- Added "Generate All Pages" button (shown when blueprint status is `'ready'`)
|
|
||||||
- Button triggers `generateAllPages()` from `builderStore`
|
|
||||||
- Shows ProgressModal during generation
|
|
||||||
- Uses existing `PageGenerationService.bulk_generate_pages()` backend function
|
|
||||||
|
|
||||||
**Queue Function**: ✅ **EXISTS**
|
|
||||||
- `PageGenerationService.bulk_generate_pages()` creates Writer Tasks
|
|
||||||
- Tasks are queued via `ContentGenerationService.generate_content()`
|
|
||||||
- Each page blueprint gets a Writer Task with title: `"[Site Builder] {page_title}"`
|
|
||||||
- Tasks are processed by Celery workers
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## URL Standards
|
|
||||||
|
|
||||||
### Public Site URLs (Deployed Sites)
|
|
||||||
|
|
||||||
**Current Implementation** (Placeholder):
|
|
||||||
- Pattern: `https://{site_id}.igny8.com`
|
|
||||||
- Generated by: `SitesRendererAdapter._get_deployment_url()`
|
|
||||||
- Location: `backend/igny8_core/business/publishing/services/adapters/sites_renderer_adapter.py:191`
|
|
||||||
|
|
||||||
**Planned Implementation** (from docs):
|
|
||||||
- Custom domains: `clientdomain.com` → routed via Caddy
|
|
||||||
- Subdomain: `mysite.igny8.com` → routed via Caddy
|
|
||||||
- Marketing site: `igny8.com` → `/igny8-sites/marketing/`
|
|
||||||
|
|
||||||
**Sites Renderer Routes**:
|
|
||||||
- Public routes: `/:siteId/*` (no auth required)
|
|
||||||
- Loads from: `/data/app/sites-data/clients/{site_id}/v{version}/`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Admin/Management URLs (Frontend App)
|
|
||||||
|
|
||||||
**Site Management Routes** (from `frontend/src/App.tsx`):
|
|
||||||
- `/sites` - All sites list
|
|
||||||
- `/sites/:id` - Site dashboard
|
|
||||||
- `/sites/:id/content` - Site content list
|
|
||||||
- `/sites/:id/editor` - Site content editor
|
|
||||||
- `/sites/:id/pages` - Page manager
|
|
||||||
- `/sites/:id/pages/new` - Create new page
|
|
||||||
- `/sites/:id/pages/:pageId/edit` - Edit page
|
|
||||||
- `/sites/:id/posts/:postId` - View/edit post
|
|
||||||
- `/sites/:id/posts/:postId/edit` - Edit post
|
|
||||||
- `/sites/:id/preview` - Site preview
|
|
||||||
- `/sites/:id/settings` - Site settings (General, SEO, OG, Schema, Integrations)
|
|
||||||
- `/sites/manage` - Site management dashboard
|
|
||||||
|
|
||||||
**Site Builder Routes**:
|
|
||||||
- `/sites/builder` - Site Builder wizard
|
|
||||||
- `/sites/builder/preview` - Preview blueprint (with Generate All Pages button)
|
|
||||||
- `/sites/blueprints` - Blueprints list
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Management
|
|
||||||
|
|
||||||
### File Storage Structure
|
|
||||||
|
|
||||||
**Site Files**:
|
|
||||||
```
|
|
||||||
/data/app/sites-data/
|
|
||||||
└── clients/
|
|
||||||
└── {site_id}/
|
|
||||||
└── v{version}/
|
|
||||||
├── site.json
|
|
||||||
├── pages/
|
|
||||||
│ ├── home.json
|
|
||||||
│ ├── about.json
|
|
||||||
│ └── ...
|
|
||||||
└── assets/ # User-managed files
|
|
||||||
├── images/
|
|
||||||
├── documents/
|
|
||||||
└── media/
|
|
||||||
```
|
|
||||||
|
|
||||||
**Service**: `SiteBuilderFileService`
|
|
||||||
- Location: `backend/igny8_core/business/site_building/services/file_management_service.py`
|
|
||||||
- Base path: `/data/app/sites-data/clients`
|
|
||||||
- Max file size: 10MB per file
|
|
||||||
- Max storage per site: 100MB
|
|
||||||
|
|
||||||
### User Access Rules
|
|
||||||
|
|
||||||
- **Owner/Admin**: Full access to all account sites
|
|
||||||
- **Editor**: Access to granted sites (via SiteUserAccess)
|
|
||||||
- **Viewer**: Read-only access to granted sites
|
|
||||||
- File operations scoped to user's accessible sites only
|
|
||||||
|
|
||||||
### File Manager UI
|
|
||||||
|
|
||||||
**Status**: ⚠️ **NOT YET IMPLEMENTED**
|
|
||||||
|
|
||||||
**Planned** (from Phase 3 docs):
|
|
||||||
- File Browser UI: `site-builder/src/components/files/FileBrowser.tsx`
|
|
||||||
- File Upload API: `modules/site_builder/views.py`
|
|
||||||
- Storage quota check: `infrastructure/storage/file_storage.py`
|
|
||||||
|
|
||||||
**Expected Routes** (not yet in App.tsx):
|
|
||||||
- `/sites/:id/files` - File manager for site assets
|
|
||||||
- `/sites/:id/files/upload` - Upload files
|
|
||||||
- `/sites/:id/files/:fileId` - View/edit file
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Docker Volumes
|
|
||||||
|
|
||||||
**From `docker-compose.app.yml`**:
|
|
||||||
```yaml
|
|
||||||
igny8_sites:
|
|
||||||
volumes:
|
|
||||||
- /data/app/igny8/sites:/app
|
|
||||||
- /data/app/sites-data:/sites # Site definitions and assets
|
|
||||||
```
|
|
||||||
|
|
||||||
**Environment Variable**:
|
|
||||||
- `SITES_DATA_PATH=/sites` (inside container)
|
|
||||||
- Maps to `/data/app/sites-data` on host
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
1. ✅ **Generate All Pages button** - Added to Preview page
|
|
||||||
2. ⏳ **File Manager UI** - Needs to be implemented
|
|
||||||
3. ⏳ **Deployment URL generation** - Currently placeholder, needs real domain mapping
|
|
||||||
4. ⏳ **Caddy routing configuration** - For custom domains
|
|
||||||
5. ⏳ **File upload API endpoints** - For user file management
|
|
||||||
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
# Site Builder Workflow - Explanation & Fix
|
|
||||||
|
|
||||||
## The Problem (FIXED)
|
|
||||||
|
|
||||||
You were experiencing confusion because there were **TWO separate systems** that weren't properly connected:
|
|
||||||
|
|
||||||
1. **Page Blueprint** (`PageBlueprint.blocks_json`) - Contains placeholder/sample blocks from AI structure generation
|
|
||||||
2. **Writer Content** (`Content` model) - Contains actual generated content from Writer tasks
|
|
||||||
|
|
||||||
**The disconnect**: When deploying a site, it only used `PageBlueprint.blocks_json` (placeholders), NOT the actual `Content` from Writer.
|
|
||||||
|
|
||||||
## Current Workflow (How It Works Now)
|
|
||||||
|
|
||||||
### Step 1: Structure Generation
|
|
||||||
- AI generates site structure → Creates `SiteBlueprint` with `PageBlueprint` pages
|
|
||||||
- Each `PageBlueprint` has `blocks_json` with **placeholder/sample blocks**
|
|
||||||
|
|
||||||
### Step 2: Content Generation
|
|
||||||
- Pages are sent to Writer as `Tasks` (title pattern: `[Site Builder] {Page Title}`)
|
|
||||||
- Writer generates actual content → Creates `Content` records
|
|
||||||
- `Content` has `html_content` and `json_blocks` with **real content**
|
|
||||||
|
|
||||||
### Step 3: Publishing Status
|
|
||||||
- **Page Blueprint Status** (`PageBlueprint.status`): Set to `'published'` in Page Manager
|
|
||||||
- Controls if page appears in navigation and is accessible
|
|
||||||
- **Content Status** (`Content.status`): Set to `'publish'` in Content Manager
|
|
||||||
- Controls if actual written content is used (vs placeholders)
|
|
||||||
|
|
||||||
### Step 4: Deployment (FIXED)
|
|
||||||
- When you deploy, `SitesRendererAdapter._build_site_definition()` now:
|
|
||||||
1. For each page, finds the associated Writer Task (by title pattern)
|
|
||||||
2. Finds the Content record for that task
|
|
||||||
3. **If Content exists and status is 'publish'**, uses `Content.json_blocks` instead of `PageBlueprint.blocks_json`
|
|
||||||
4. If Content has `html_content` but no `json_blocks`, converts it to a text block
|
|
||||||
5. Uses the merged/actual content blocks for deployment
|
|
||||||
|
|
||||||
## URLs
|
|
||||||
|
|
||||||
- **Public Site URL**: `https://sites.igny8.com/{siteSlug}`
|
|
||||||
- Shows deployed site with actual content (if Content is published)
|
|
||||||
- Falls back to blueprint placeholders if Content not published
|
|
||||||
- **Individual Pages**: `https://sites.igny8.com/{siteSlug}/{pageSlug}`
|
|
||||||
- Example: `https://sites.igny8.com/auto-g8/products`
|
|
||||||
|
|
||||||
## How To Use It Now
|
|
||||||
|
|
||||||
1. **Generate Structure**: Create site blueprint with pages (has placeholder blocks)
|
|
||||||
2. **Generate Content**: Pages → Tasks → Content (Writer generates real content)
|
|
||||||
3. **Publish Content**: In Content Manager, set Content status to `'publish'`
|
|
||||||
4. **Publish Pages**: In Page Manager, set Page status to `'published'` (for navigation)
|
|
||||||
5. **Deploy Site**: Deploy the blueprint - it will automatically use published Content
|
|
||||||
|
|
||||||
## What Changed
|
|
||||||
|
|
||||||
✅ **Fixed**: `SitesRendererAdapter._build_site_definition()` now merges published Content into PageBlueprint blocks during deployment
|
|
||||||
|
|
||||||
✅ **Result**: When you deploy, the site shows actual written content, not placeholders
|
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
|
|
||||||
- **Two Statuses**: Page status controls visibility, Content status controls which content is used
|
|
||||||
- **Deployment Required**: After publishing Content, you need to **redeploy** the site for changes to appear
|
|
||||||
- **Content Takes Precedence**: If Content is published, it replaces blueprint placeholders
|
|
||||||
- **Fallback**: If Content not published, blueprint placeholders are used
|
|
||||||
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
# Template System - Current State & Plans
|
|
||||||
|
|
||||||
## Current Template Architecture
|
|
||||||
|
|
||||||
### 1. Site-Level Layouts (Implemented)
|
|
||||||
**Location**: `sites/src/utils/layoutRenderer.tsx`
|
|
||||||
|
|
||||||
**Available Layouts**:
|
|
||||||
- `default` - Standard header, content, footer
|
|
||||||
- `minimal` - Clean, minimal design
|
|
||||||
- `magazine` - Editorial, content-focused
|
|
||||||
- `ecommerce` - Product-focused
|
|
||||||
- `portfolio` - Showcase layout
|
|
||||||
- `blog` - Content-first
|
|
||||||
- `corporate` - Business layout
|
|
||||||
|
|
||||||
**How it works**:
|
|
||||||
- Set in `SiteBlueprint.structure_json.layout`
|
|
||||||
- Applied to the entire site
|
|
||||||
- All pages use the same layout
|
|
||||||
|
|
||||||
### 2. Block Templates (Implemented)
|
|
||||||
**Location**: `sites/src/utils/templateEngine.tsx`
|
|
||||||
|
|
||||||
**Available Block Types**:
|
|
||||||
- `hero` - Hero section with title, subtitle, CTA
|
|
||||||
- `text` - Text content block
|
|
||||||
- `features` - Feature grid
|
|
||||||
- `testimonials` - Testimonials section
|
|
||||||
- `services` - Services grid
|
|
||||||
- `stats` - Statistics panel
|
|
||||||
- `cta` - Call to action
|
|
||||||
- `image` - Image block
|
|
||||||
- `video` - Video block
|
|
||||||
- `form` - Contact form
|
|
||||||
- `faq` - FAQ accordion
|
|
||||||
- `quote` - Quote block
|
|
||||||
- `grid` - Grid layout
|
|
||||||
- `card` - Card block
|
|
||||||
- `list` - List block
|
|
||||||
- `accordion` - Accordion block
|
|
||||||
|
|
||||||
**How it works**:
|
|
||||||
- Each page has `blocks_json` array
|
|
||||||
- Each block has `type` and `data`
|
|
||||||
- `renderTemplate()` renders blocks based on type
|
|
||||||
|
|
||||||
### 3. Page Types (Defined, but NO templates yet)
|
|
||||||
**Location**: `backend/igny8_core/business/site_building/models.py`
|
|
||||||
|
|
||||||
**Page Type Choices**:
|
|
||||||
- `home` - Homepage
|
|
||||||
- `about` - About page
|
|
||||||
- `services` - Services page
|
|
||||||
- `products` - Products page
|
|
||||||
- `blog` - Blog page
|
|
||||||
- `contact` - Contact page
|
|
||||||
- `custom` - Custom page
|
|
||||||
|
|
||||||
**Current State**:
|
|
||||||
- Page types are stored but **NOT used for rendering**
|
|
||||||
- All pages render the same way regardless of type
|
|
||||||
- No page-type-specific templates exist
|
|
||||||
|
|
||||||
## Missing: Page-Type Templates
|
|
||||||
|
|
||||||
### What's Missing
|
|
||||||
Currently, there's **NO page-type-specific template system**. All pages render identically:
|
|
||||||
- Home page renders the same as Products page
|
|
||||||
- Blog page renders the same as Contact page
|
|
||||||
- No special handling for different page types
|
|
||||||
|
|
||||||
### Where Page-Type Templates Should Be
|
|
||||||
|
|
||||||
**Proposed Location**: `sites/src/utils/pageTypeRenderer.tsx`
|
|
||||||
|
|
||||||
**Proposed Structure**:
|
|
||||||
```typescript
|
|
||||||
// Page-type specific renderers
|
|
||||||
function renderHomePage(page: PageDefinition, blocks: Block[]): React.ReactElement {
|
|
||||||
// Home-specific template: Hero, features, testimonials, CTA
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderProductsPage(page: PageDefinition, blocks: Block[]): React.ReactElement {
|
|
||||||
// Products-specific template: Product grid, filters, categories
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderBlogPage(page: PageDefinition, blocks: Block[]): React.ReactElement {
|
|
||||||
// Blog-specific template: Post list, sidebar, pagination
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderContactPage(page: PageDefinition, blocks: Block[]): React.ReactElement {
|
|
||||||
// Contact-specific template: Form, map, contact info
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### How It Should Work
|
|
||||||
|
|
||||||
1. **In `layoutRenderer.tsx`**: After determining site layout, check page type
|
|
||||||
2. **Route to page-type renderer**: If page type has specific template, use it
|
|
||||||
3. **Fallback to default**: If no page-type template, use default block rendering
|
|
||||||
|
|
||||||
**Example Flow**:
|
|
||||||
```
|
|
||||||
Site Definition → Site Layout (default/minimal/etc.)
|
|
||||||
↓
|
|
||||||
Page Type (home/products/blog/etc.)
|
|
||||||
↓
|
|
||||||
Page-Type Template (if exists) OR Default Block Rendering
|
|
||||||
↓
|
|
||||||
Block Templates (hero/text/features/etc.)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Current Rendering Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
SiteRenderer
|
|
||||||
↓
|
|
||||||
loadSiteDefinition()
|
|
||||||
↓
|
|
||||||
renderLayout(siteDefinition) → Uses site.layout (default/minimal/etc.)
|
|
||||||
↓
|
|
||||||
renderDefaultLayout() → Renders all pages the same way
|
|
||||||
↓
|
|
||||||
renderTemplate(block) → Renders individual blocks
|
|
||||||
```
|
|
||||||
|
|
||||||
## Proposed Enhanced Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
SiteRenderer
|
|
||||||
↓
|
|
||||||
loadSiteDefinition()
|
|
||||||
↓
|
|
||||||
renderLayout(siteDefinition) → Uses site.layout
|
|
||||||
↓
|
|
||||||
For each page:
|
|
||||||
↓
|
|
||||||
Check page.type (home/products/blog/etc.)
|
|
||||||
↓
|
|
||||||
If page-type template exists:
|
|
||||||
→ renderPageTypeTemplate(page)
|
|
||||||
Else:
|
|
||||||
→ renderDefaultPageTemplate(page)
|
|
||||||
↓
|
|
||||||
renderTemplate(block) → Renders blocks
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
### Phase 1: Create Page-Type Renderers
|
|
||||||
- Create `sites/src/utils/pageTypeRenderer.tsx`
|
|
||||||
- Implement templates for each page type:
|
|
||||||
- `home` - Hero + features + testimonials layout
|
|
||||||
- `products` - Product grid + filters
|
|
||||||
- `blog` - Post list + sidebar
|
|
||||||
- `contact` - Form + map + info
|
|
||||||
- `about` - Team + mission + values
|
|
||||||
- `services` - Service cards + descriptions
|
|
||||||
|
|
||||||
### Phase 2: Integrate with Layout Renderer
|
|
||||||
- Modify `renderDefaultLayout()` to check page type
|
|
||||||
- Route to page-type renderer if template exists
|
|
||||||
- Fallback to current block rendering
|
|
||||||
|
|
||||||
### Phase 3: Make Templates Configurable
|
|
||||||
- Allow templates to be customized per site
|
|
||||||
- Store template preferences in `SiteBlueprint.config_json`
|
|
||||||
- Support custom templates
|
|
||||||
|
|
||||||
## Current Files
|
|
||||||
|
|
||||||
- **Site Layouts**: `sites/src/utils/layoutRenderer.tsx`
|
|
||||||
- **Block Templates**: `sites/src/utils/templateEngine.tsx`
|
|
||||||
- **Page Types**: `backend/igny8_core/business/site_building/models.py` (PageBlueprint.PAGE_TYPE_CHOICES)
|
|
||||||
- **Missing**: Page-type templates (not implemented yet)
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
✅ **Implemented**: Site-level layouts, block templates
|
|
||||||
❌ **Missing**: Page-type-specific templates
|
|
||||||
📝 **Planned**: Page-type renderers in `sites/src/utils/pageTypeRenderer.tsx`
|
|
||||||
|
|
||||||
Currently, all pages render the same way. Page types are stored but not used for rendering. To add page-type templates, create a new file `pageTypeRenderer.tsx` and integrate it into the layout renderer.
|
|
||||||
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
# Stage 2 – Planner & Wizard UX
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
- Feature flag: `USE_SITE_BUILDER_REFACTOR` (must be `true`)
|
|
||||||
- Goal: ship state-aware, guided Site Builder experience and expose cluster/taxonomy readiness to Planner UI
|
|
||||||
- Dependencies: Stage 1 migrations/services already deployed
|
|
||||||
- Entry point: `/sites/builder/workflow/:blueprintId` (protected route)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Backend Enhancements
|
|
||||||
|
|
||||||
### New services / endpoints
|
|
||||||
| Component | Description | File |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `WizardContextService` | Aggregates workflow state, cluster stats, taxonomy summaries, coverage counts | `backend/igny8_core/business/site_building/services/wizard_context_service.py` |
|
|
||||||
| Workflow context API | `GET /api/v1/site-builder/siteblueprint/{id}/workflow/context/` returns `workflow`, `cluster_summary`, `taxonomy_summary`, `coverage`, `next_actions` | `modules/site_builder/views.py` |
|
|
||||||
| Workflow serializer helpers | `SiteBlueprintSerializer` now returns `workflow_state` + `gating_messages` via `WorkflowStateService.serialize_state()` | `modules/site_builder/serializers.py` |
|
|
||||||
|
|
||||||
### Workflow telemetry & logging
|
|
||||||
- `WorkflowStateService` normalizes step metadata (`status`, `code`, `message`, `updated_at`)
|
|
||||||
- Emits structured log events (`wizard_step_updated`, `wizard_blocking_issue`) for future analytics
|
|
||||||
- `serialize_state()` provides consistent payload for API, wizard store, and planner warnings
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Frontend Implementation
|
|
||||||
|
|
||||||
### API layer
|
|
||||||
- Added interfaces + calls (`fetchSiteBlueprints`, `fetchSiteBlueprintById`, `updateSiteBlueprint`, `fetchWizardContext`, `updateWorkflowStep`)
|
|
||||||
- Located in `frontend/src/services/api.ts`
|
|
||||||
|
|
||||||
### Zustand store
|
|
||||||
| Store | Purpose |
|
|
||||||
| --- | --- |
|
|
||||||
| `useBuilderWorkflowStore` | Tracks `blueprintId`, `currentStep`, `completedSteps`, `blockingIssues`, `workflowState`, `context`, telemetry queue |
|
|
||||||
- Actions: `initialize`, `refreshState`, `goToStep`, `completeStep`, `setBlockingIssue`, `clearBlockingIssue`, `flushTelemetry`, `reset`
|
|
||||||
- Persists last blueprint + step in `builder-workflow-storage` (sessionStorage)
|
|
||||||
|
|
||||||
### Wizard shell & routing
|
|
||||||
- New page: `WorkflowWizard` at `/sites/builder/workflow/:blueprintId`
|
|
||||||
- Lazy-loaded via `App.tsx`
|
|
||||||
- Refreshes context every 10s to keep telemetry/current state accurate
|
|
||||||
- Progress indicator (`WizardProgress`) shows completed/current/blocked steps
|
|
||||||
|
|
||||||
### Step components
|
|
||||||
| Step | Component | Status |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 1 – Business Details | `steps/BusinessDetailsStep.tsx` | **Functional** (saves blueprint + completes step) |
|
|
||||||
| 2 – Cluster Assignment | `steps/ClusterAssignmentStep.tsx` | Placeholder UI (shows stats, next iteration: attach/detach clusters) |
|
|
||||||
| 3 – Taxonomy Builder | `steps/TaxonomyBuilderStep.tsx` | Placeholder UI (future: taxonomy tree/table + imports) |
|
|
||||||
| 4 – AI Sitemap Review | `steps/SitemapReviewStep.tsx` | Placeholder UI (future: grid w/ grouping + regenerate) |
|
|
||||||
| 5 – Coverage Validation | `steps/CoverageValidationStep.tsx` | Placeholder UI (future: coverage cards + gating logic) |
|
|
||||||
| 6 – Ideas Hand-off | `steps/IdeasHandoffStep.tsx` | Placeholder UI (future: page selection + prompt override) |
|
|
||||||
|
|
||||||
### Planner UX TODOs (next iteration)
|
|
||||||
- Cluster matrix view linking back into wizard
|
|
||||||
- Taxonomy management table with inline edits & imports
|
|
||||||
- Planner dashboard banner if blueprint missing requirements
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing & Verification
|
|
||||||
1. Set `USE_SITE_BUILDER_REFACTOR=true`
|
|
||||||
2. Create or reuse a `SiteBlueprint` and open `/sites/builder/workflow/{id}`
|
|
||||||
3. Confirm payload from `GET /workflow/context/` includes:
|
|
||||||
- `workflow.steps` array with `status`, `code`, `message`
|
|
||||||
- `cluster_summary` (counts + list)
|
|
||||||
- `taxonomy_summary`
|
|
||||||
- `coverage` (pages, statuses)
|
|
||||||
- `next_actions`
|
|
||||||
4. Step 1 (Business Details) should allow update/save → triggers workflow state update
|
|
||||||
5. Steps 2‑6 currently show placeholders + gating alerts (will be wired to new UIs)
|
|
||||||
|
|
||||||
Automated tests pending: once UI solidified, add Cypress flows for each step + Zustand store unit tests.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Open Items / Future Work
|
|
||||||
- Build full step experiences (tables, drag/drop mapping, AI sitemap review UI)
|
|
||||||
- Planner cluster matrix & taxonomy management views
|
|
||||||
- Telemetry dispatcher (currently logs to console)
|
|
||||||
- Accessibility polish: keyboard navigation, helper tooltips, context drawer
|
|
||||||
- QA automation + e2e coverage
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Last updated: 2025-11-19*
|
|
||||||
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
# Stage 3 – Writer / Linker / Optimizer Enhancements
|
|
||||||
|
|
||||||
## Objective
|
|
||||||
Propagate the new metadata (clusters, taxonomies, entity types, attributes) through the planner → writer pipeline, enforce validation before publish, and unlock linker/optimizer capabilities. Stage 3 builds directly on Stage 2’s wizard outputs; all changes stay behind `USE_SITE_BUILDER_REFACTOR` until pilot-ready.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Backend Plan
|
|
||||||
|
|
||||||
### 1. Metadata Audit & Backfill
|
|
||||||
| Task | Description | Owner |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Backfill tables | Implement data migration using the stub added in `writer/migrations/0012_metadata_mapping_tables.py` to populate `ContentClusterMap`, `ContentTaxonomyMap`, `ContentAttributeMap` for legacy content/tasks. | Backend lead |
|
|
||||||
| Entity defaults | Ensure existing Tasks/Content have sensible defaults for `entity_type`, `taxonomy_id`, `cluster_role` (e.g., `blog_post` + `supporting`). | Backend lead |
|
|
||||||
| Audit script | Management command `python manage.py audit_site_metadata --site {id}` summarizing gaps per site. | Backend lead |
|
|
||||||
|
|
||||||
### 2. Pipeline Updates
|
|
||||||
| Stage | Changes |
|
|
||||||
| --- | --- |
|
|
||||||
| Ideas → Tasks | Update Task creation so every task inherits cluster/taxonomy/attribute metadata. Enforce “no cluster, no idea/task” rule. |
|
|
||||||
| Tasks → Content | Adjust `PageGenerationService` / writer Celery tasks to persist mappings into `ContentClusterMap`, `ContentTaxonomyMap`, `ContentAttributeMap`. |
|
|
||||||
| AI Prompts | Update prompts to include cluster role, taxonomy context, product attributes. Leverage Stage 1 metadata fields like `dimension_meta`, `attribute_values`. |
|
|
||||||
| Validation services | Add reusable validators (e.g., `ensure_required_attributes(task)`), returning structured errors for UI. |
|
|
||||||
|
|
||||||
### 3. Linker & Optimizer
|
|
||||||
| Component | Requirements |
|
|
||||||
| --- | --- |
|
|
||||||
| LinkerService | Use mapping tables to suggest hub ↔ supporting ↔ attribute links; include priority score + context snippet. |
|
|
||||||
| OptimizerService | Scorecards factoring cluster coverage, taxonomy alignment, attribute completeness. Provide `/sites/{id}/progress` and `/writer/content/{id}/validation` endpoints. |
|
|
||||||
| Caching/Indexes | Add DB indexes for frequent join patterns (content ↔ cluster_map, taxonomy_map). |
|
|
||||||
|
|
||||||
### 4. API Additions
|
|
||||||
- `GET /api/v1/sites/{id}/progress/` → cluster-level completion + validation flags.
|
|
||||||
- `GET /api/v1/writer/content/{id}/validation/` → aggregated checklist for Writer UI.
|
|
||||||
- `POST /api/v1/writer/content/{id}/validate/` → re-run validators and return actionable errors.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Frontend Plan
|
|
||||||
|
|
||||||
### 1. Planner / Ideas / Writer Enhancements
|
|
||||||
| Area | UX Changes |
|
|
||||||
| --- | --- |
|
|
||||||
| Planner Ideas & Writer Tasks lists | Show chips/columns for cluster, taxonomy, entity type, validation status; add filters. |
|
|
||||||
| Writer Editor | Sidebar module summarizing cluster, taxonomy tree, attribute form; validation panel blocking publish until cleared. |
|
|
||||||
| Linker UI | Group internal link suggestions by cluster role; show context snippet + CTA to insert link. |
|
|
||||||
| Optimizer Dashboard | Scorecards per cluster dimension with color coding + “next action” cards. |
|
|
||||||
| Site Progress Widgets | On site overview, show completion bars (hub/supporting/attribute); deep link to problematic clusters/pages. |
|
|
||||||
|
|
||||||
### 2. Validation & Notifications
|
|
||||||
- Inline toasts + optional email when validation fails.
|
|
||||||
- Publish button disabled until validators pass; display error list linking to relevant fields.
|
|
||||||
- Credit reminder banner when user regenerates content/optimization tasks.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
| Area | Automated | Manual |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Pipeline | Unit tests for Task → Content metadata persistence, Prompt builders. | End-to-end: blueprint → ideas → tasks → content. |
|
|
||||||
| Validation | Tests ensuring validators trigger correct errors. | Attempt publish without taxonomy/attributes; confirm UX flow. |
|
|
||||||
| Linker/Optimizer | Service tests for scoring & suggestions. | Performance profiling on large datasets; UX review. |
|
|
||||||
| Progress Widgets | Component tests verifying counts. | Compare UI vs. database for pilot site. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Rollout Checklist
|
|
||||||
1. Deploy Stage 3 backend with feature flag ON in staging.
|
|
||||||
2. Run metadata backfill; verify progress dashboards and validators.
|
|
||||||
3. QA regression: planner → writer flow, linker, optimizer.
|
|
||||||
4. Pilot with internal content team; gather feedback on validation friction.
|
|
||||||
5. Optimize slow queries (indexes/caching) before production rollout.
|
|
||||||
6. Update training docs/videos for writers; enable flag gradually across accounts.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Open Questions / Risks
|
|
||||||
- Volume impact when backfilling large content sets? (Plan: chunked migrations + progress logs)
|
|
||||||
- Telemetry volume for validator events? (Plan: aggregate counts; sample per site)
|
|
||||||
- WordPress deploy parity: ensure metadata travels through sync (handled in Stage 4).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Last updated: 2025-11-19*
|
|
||||||
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
# Stage 4 – Publishing & Sync Integration
|
|
||||||
|
|
||||||
## Objective
|
|
||||||
Achieve feature parity between IGNY8-hosted deployments and WordPress sites using the shared metadata model introduced in Stages 1–3. This includes two-way sync for taxonomies/products, deployment readiness checks, and operational tooling.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Backend Plan
|
|
||||||
|
|
||||||
### 1. Sync Architecture
|
|
||||||
| Task | Description |
|
|
||||||
| --- | --- |
|
|
||||||
| Audit adapters | Review current WordPress adapter + sync service; confirm endpoints for categories, tags, WooCommerce attributes/products. |
|
|
||||||
| Data mapping | Document mapping between WordPress taxonomies and IGNY8 `SiteBlueprintTaxonomy`, `ContentTaxonomyMap`, `external_reference` fields. |
|
|
||||||
| Sync configs | Extend integration settings to store WordPress/Woo credentials, sync frequency, and site archetype. |
|
|
||||||
|
|
||||||
### 2. Enhancements
|
|
||||||
| Area | Implementation |
|
|
||||||
| --- | --- |
|
|
||||||
| Import | `ContentSyncService` fetches WP taxonomies/products/custom post types -> maps to IGNY8 schema via `TaxonomyService`. Auto-create missing clusters/taxonomies with an `imported` flag. |
|
|
||||||
| Export | `WordPressAdapter` ensures taxonomies exist before publishing posts/products. Pushes product attributes/tags ahead of content. |
|
|
||||||
| Sync Health APIs | `/sites/{id}/sync/status`, `/sites/{id}/sync/run`, returning last sync time, mismatch counts, error logs. |
|
|
||||||
| Deployment | `SitesRendererAdapter` consumes cluster/taxonomy metadata for navigation, breadcrumbs, internal links. Deployment readiness check ensures cluster coverage, content status, validation flags. |
|
|
||||||
| Logging & Monitoring | Structured logs per sync run (duration, items processed, failures). Alerts for repeated sync failures or deployment errors. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Frontend Plan
|
|
||||||
|
|
||||||
### 1. Sync Dashboard
|
|
||||||
| Component | Features |
|
|
||||||
| --- | --- |
|
|
||||||
| Parity indicators | Status icons for taxonomies, products, posts. |
|
|
||||||
| Controls | Manual sync, retry failed items, view logs (with pagination/filtering). |
|
|
||||||
| Detail drawer | Shows mismatched items, suggested fixes, quick links into Planner/Writer. |
|
|
||||||
|
|
||||||
### 2. Deployment Panel
|
|
||||||
| Component | Features |
|
|
||||||
| --- | --- |
|
|
||||||
| Readiness checklist | Displays cluster coverage, content validation, sync status. |
|
|
||||||
| Actions | Deploy and rollback buttons with confirmation modals and logging. |
|
|
||||||
| Notifications | Toasts for success/failure; optional email/webhook integration. |
|
|
||||||
|
|
||||||
### 3. WordPress Connection UI
|
|
||||||
| Task | Description |
|
|
||||||
| --- | --- |
|
|
||||||
| Integration status | Show credential health, last sync time, active site type. |
|
|
||||||
| Troubleshooting | Inline helper text + links to docs/runbook. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Operational Runbooks
|
|
||||||
- Sync troubleshooting steps (auth errors, taxonomy mismatches, WooCommerce throttling).
|
|
||||||
- Rollback procedures for failed deployments.
|
|
||||||
- Escalation path + scripts to force sync, clear queue, or recover failed deploy.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
| Area | Automated | Manual |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Sync logic | Integration tests hitting mocked WP APIs, verifying mapping + retries. | Staging sync from live WP instance; verify taxonomy parity. |
|
|
||||||
| Deployment | Renderer tests using fixture metadata to ensure navigation/internal links. | Deploy IGNY8 site; inspect front-end output, breadcrumbs, menus. |
|
|
||||||
| Dashboards | Component/unit tests for Sync/Deployment panels. | Pilot user testing + UX review. |
|
|
||||||
| Runbooks | N/A | Tabletop exercises for failure scenarios (sync fails, deploy rollback). |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Rollout Checklist
|
|
||||||
1. Enable Stage 4 flag in staging; run full import/export tests.
|
|
||||||
2. Pilot with one IGNY8-hosted and one WordPress-hosted site.
|
|
||||||
3. Train support on dashboards/runbooks; ensure alerting configured.
|
|
||||||
4. Announce availability; roll out gradually to accounts with WordPress integrations.
|
|
||||||
5. Monitor logs/alerts during first production syncs; iterate on tooling as needed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Risks & Mitigations
|
|
||||||
| Risk | Mitigation |
|
|
||||||
| --- | --- |
|
|
||||||
| API rate limits (WordPress/WooCommerce) | Backoff + batching, highlight in dashboard. |
|
|
||||||
| Data mismatches (taxonomies/products) | Detailed diff view + retry actions + runbook. |
|
|
||||||
| Deployment failures | Preflight checks + rollback buttons + structured logs. |
|
|
||||||
| Operational overhead | Alerting dashboards + documented on-call runbook. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Last updated: 2025-11-19*
|
|
||||||
|
|
||||||
24
site-builder/.gitignore
vendored
24
site-builder/.gitignore
vendored
@@ -1,24 +0,0 @@
|
|||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Site Builder Dev Image (Node 22 to satisfy Vite requirements)
|
|
||||||
FROM node:22-alpine
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy package manifests first for better caching
|
|
||||||
COPY package*.json ./
|
|
||||||
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
# Copy source (still bind-mounted at runtime, but needed for initial run)
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
EXPOSE 5175
|
|
||||||
|
|
||||||
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5175"]
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# React + TypeScript + Vite
|
|
||||||
|
|
||||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
||||||
|
|
||||||
Currently, two official plugins are available:
|
|
||||||
|
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
||||||
|
|
||||||
## React Compiler
|
|
||||||
|
|
||||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
|
||||||
|
|
||||||
## Expanding the ESLint configuration
|
|
||||||
|
|
||||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
|
||||||
|
|
||||||
```js
|
|
||||||
export default defineConfig([
|
|
||||||
globalIgnores(['dist']),
|
|
||||||
{
|
|
||||||
files: ['**/*.{ts,tsx}'],
|
|
||||||
extends: [
|
|
||||||
// Other configs...
|
|
||||||
|
|
||||||
// Remove tseslint.configs.recommended and replace with this
|
|
||||||
tseslint.configs.recommendedTypeChecked,
|
|
||||||
// Alternatively, use this for stricter rules
|
|
||||||
tseslint.configs.strictTypeChecked,
|
|
||||||
// Optionally, add this for stylistic rules
|
|
||||||
tseslint.configs.stylisticTypeChecked,
|
|
||||||
|
|
||||||
// Other configs...
|
|
||||||
],
|
|
||||||
languageOptions: {
|
|
||||||
parserOptions: {
|
|
||||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
||||||
tsconfigRootDir: import.meta.dirname,
|
|
||||||
},
|
|
||||||
// other options...
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// eslint.config.js
|
|
||||||
import reactX from 'eslint-plugin-react-x'
|
|
||||||
import reactDom from 'eslint-plugin-react-dom'
|
|
||||||
|
|
||||||
export default defineConfig([
|
|
||||||
globalIgnores(['dist']),
|
|
||||||
{
|
|
||||||
files: ['**/*.{ts,tsx}'],
|
|
||||||
extends: [
|
|
||||||
// Other configs...
|
|
||||||
// Enable lint rules for React
|
|
||||||
reactX.configs['recommended-typescript'],
|
|
||||||
// Enable lint rules for React DOM
|
|
||||||
reactDom.configs.recommended,
|
|
||||||
],
|
|
||||||
languageOptions: {
|
|
||||||
parserOptions: {
|
|
||||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
||||||
tsconfigRootDir: import.meta.dirname,
|
|
||||||
},
|
|
||||||
// other options...
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
```
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import js from '@eslint/js'
|
|
||||||
import globals from 'globals'
|
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
||||||
import tseslint from 'typescript-eslint'
|
|
||||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
||||||
|
|
||||||
export default defineConfig([
|
|
||||||
globalIgnores(['dist']),
|
|
||||||
{
|
|
||||||
files: ['**/*.{ts,tsx}'],
|
|
||||||
extends: [
|
|
||||||
js.configs.recommended,
|
|
||||||
tseslint.configs.recommended,
|
|
||||||
reactHooks.configs.flat.recommended,
|
|
||||||
reactRefresh.configs.vite,
|
|
||||||
],
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
globals: globals.browser,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>site-builder</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
6025
site-builder/package-lock.json
generated
6025
site-builder/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "site-builder",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "tsc -b && vite build",
|
|
||||||
"lint": "eslint .",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"test": "vitest run",
|
|
||||||
"test:watch": "vitest"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"axios": "^1.13.2",
|
|
||||||
"lucide-react": "^0.554.0",
|
|
||||||
"react": "^19.2.0",
|
|
||||||
"react-dom": "^19.2.0",
|
|
||||||
"react-hook-form": "^7.66.0",
|
|
||||||
"react-router-dom": "^7.9.6",
|
|
||||||
"zustand": "^5.0.8"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
|
||||||
"@testing-library/react": "^16.2.0",
|
|
||||||
"@eslint/js": "^9.39.1",
|
|
||||||
"@types/node": "^24.10.0",
|
|
||||||
"@types/react": "^19.2.2",
|
|
||||||
"@types/react-dom": "^19.2.2",
|
|
||||||
"@types/react-router-dom": "^5.3.3",
|
|
||||||
"@vitejs/plugin-react": "^5.1.0",
|
|
||||||
"eslint": "^9.39.1",
|
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
|
||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
|
||||||
"globals": "^16.5.0",
|
|
||||||
"typescript": "~5.9.3",
|
|
||||||
"typescript-eslint": "^8.46.3",
|
|
||||||
"vite": "^7.2.2",
|
|
||||||
"vitest": "^2.1.5",
|
|
||||||
"jsdom": "^25.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,330 +0,0 @@
|
|||||||
.app-shell {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 280px 1fr;
|
|
||||||
min-height: 100vh;
|
|
||||||
background: #f5f7fb;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-sidebar {
|
|
||||||
border-right: 1px solid rgba(15, 23, 42, 0.08);
|
|
||||||
padding: 2rem 1.5rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2rem;
|
|
||||||
background: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-sidebar .brand span {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-sidebar .brand small {
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-sidebar nav {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-sidebar a {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 0.6rem 0.75rem;
|
|
||||||
border-radius: 10px;
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-sidebar a.active {
|
|
||||||
background: #eef2ff;
|
|
||||||
color: #4338ca;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-main {
|
|
||||||
padding: 2rem 3rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wizard-page {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wizard-progress {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wizard-progress__dot {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
color: #94a3b8;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wizard-progress__dot span {
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 2px solid currentColor;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wizard-progress__dot.is-active {
|
|
||||||
color: #4338ca;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wizard-step {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wizard-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wizard-actions button {
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid rgba(67, 56, 202, 0.3);
|
|
||||||
background: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wizard-actions button.primary {
|
|
||||||
background: #4338ca;
|
|
||||||
color: #fff;
|
|
||||||
border-color: transparent;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wizard-actions button.ghost,
|
|
||||||
.ghost {
|
|
||||||
background: transparent;
|
|
||||||
border-color: rgba(67, 56, 202, 0.35);
|
|
||||||
color: #4338ca;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wizard-actions button:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-error {
|
|
||||||
color: #dc2626;
|
|
||||||
margin: 0.5rem 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-grid {
|
|
||||||
display: grid;
|
|
||||||
gap: 1rem;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-field {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.4rem;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-field input,
|
|
||||||
.sb-field select,
|
|
||||||
.sb-field textarea {
|
|
||||||
border: 1px solid rgba(15, 23, 42, 0.15);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 0.65rem 0.85rem;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
font-family: inherit;
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-pill-list {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-pill {
|
|
||||||
padding: 0.35rem 0.75rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(67, 56, 202, 0.1);
|
|
||||||
color: #4338ca;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-pill button {
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
color: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-objective-input {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-objective-input input {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-objective-input button {
|
|
||||||
border: none;
|
|
||||||
background: #0f172a;
|
|
||||||
color: #fff;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 0.65rem 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-blueprint-meta {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot::before {
|
|
||||||
content: '';
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: currentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-ready {
|
|
||||||
color: #10b981;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-generating {
|
|
||||||
color: #f97316;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-draft {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-canvas {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 1.5rem;
|
|
||||||
box-shadow: 0 8px 26px rgba(15, 23, 42, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-nav {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-nav button {
|
|
||||||
border: 1px solid rgba(15, 23, 42, 0.1);
|
|
||||||
background: #f8fafc;
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 0.35rem 0.85rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-nav button.is-active {
|
|
||||||
background: #4338ca;
|
|
||||||
color: white;
|
|
||||||
border-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-hero {
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-hero .preview-label {
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-blocks {
|
|
||||||
display: grid;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-block {
|
|
||||||
border: 1px dashed rgba(67, 56, 202, 0.2);
|
|
||||||
border-radius: 14px;
|
|
||||||
padding: 1rem;
|
|
||||||
background: rgba(67, 56, 202, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-blueprint-list {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-blueprint-list li {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding-bottom: 0.75rem;
|
|
||||||
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-blueprint-list strong {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-blueprint-list span {
|
|
||||||
color: #475569;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-loading {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spin {
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { NavLink, Route, Routes } from 'react-router-dom';
|
|
||||||
import { Wand2, LayoutTemplate, PanelsTopLeft } from 'lucide-react';
|
|
||||||
import { WizardPage } from './pages/wizard/WizardPage';
|
|
||||||
import { PreviewCanvas } from './pages/preview/PreviewCanvas';
|
|
||||||
import { SiteDashboard } from './pages/dashboard/SiteDashboard';
|
|
||||||
import './App.css';
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return (
|
|
||||||
<div className="app-shell">
|
|
||||||
<aside className="app-sidebar">
|
|
||||||
<div className="brand">
|
|
||||||
<span>Site Builder</span>
|
|
||||||
<small>Phase 3 · wizard + preview</small>
|
|
||||||
</div>
|
|
||||||
<nav>
|
|
||||||
<NavLink to="/" end>
|
|
||||||
<Wand2 size={18} />
|
|
||||||
Wizard
|
|
||||||
</NavLink>
|
|
||||||
<NavLink to="/preview">
|
|
||||||
<LayoutTemplate size={18} />
|
|
||||||
Preview
|
|
||||||
</NavLink>
|
|
||||||
<NavLink to="/dashboard">
|
|
||||||
<PanelsTopLeft size={18} />
|
|
||||||
Blueprint history
|
|
||||||
</NavLink>
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<main className="app-main">
|
|
||||||
<Routes>
|
|
||||||
<Route path="/" element={<WizardPage />} />
|
|
||||||
<Route path="/preview" element={<PreviewCanvas />} />
|
|
||||||
<Route path="/dashboard" element={<SiteDashboard />} />
|
|
||||||
</Routes>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import type {
|
|
||||||
BuilderFormData,
|
|
||||||
PageBlueprint,
|
|
||||||
SiteBlueprint,
|
|
||||||
SiteStructure,
|
|
||||||
} from '../types/siteBuilder';
|
|
||||||
|
|
||||||
const API_ROOT = import.meta.env.VITE_API_URL ?? 'http://localhost:8010/api';
|
|
||||||
const BASE_PATH = `${API_ROOT}/v1/site-builder`;
|
|
||||||
|
|
||||||
const client = axios.create({
|
|
||||||
baseURL: BASE_PATH,
|
|
||||||
withCredentials: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface CreateBlueprintPayload {
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
site_id: number;
|
|
||||||
sector_id: number;
|
|
||||||
hosting_type: BuilderFormData['hostingType'];
|
|
||||||
config_json: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GenerateStructurePayload {
|
|
||||||
business_brief: string;
|
|
||||||
objectives: string[];
|
|
||||||
style: BuilderFormData['style'];
|
|
||||||
metadata?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const builderApi = {
|
|
||||||
async getBlueprint(blueprintId: number): Promise<SiteBlueprint> {
|
|
||||||
const res = await client.get(`/blueprints/${blueprintId}/`);
|
|
||||||
return res.data;
|
|
||||||
},
|
|
||||||
async listBlueprints(): Promise<SiteBlueprint[]> {
|
|
||||||
const res = await client.get('/blueprints/');
|
|
||||||
if (Array.isArray(res.data?.results)) {
|
|
||||||
return res.data.results as SiteBlueprint[];
|
|
||||||
}
|
|
||||||
return Array.isArray(res.data) ? res.data : [];
|
|
||||||
},
|
|
||||||
|
|
||||||
async createBlueprint(payload: CreateBlueprintPayload): Promise<SiteBlueprint> {
|
|
||||||
const res = await client.post('/blueprints/', payload);
|
|
||||||
return res.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
async generateStructure(
|
|
||||||
blueprintId: number,
|
|
||||||
payload: GenerateStructurePayload,
|
|
||||||
): Promise<{ task_id?: string; success?: boolean; structure?: SiteStructure }> {
|
|
||||||
const res = await client.post(`/blueprints/${blueprintId}/generate_structure/`, payload);
|
|
||||||
return res.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
async listPages(blueprintId: number): Promise<PageBlueprint[]> {
|
|
||||||
const res = await client.get(`/pages/?site_blueprint=${blueprintId}`);
|
|
||||||
return Array.isArray(res.data?.results) ? res.data.results : res.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
async generateAllPages(
|
|
||||||
blueprintId: number,
|
|
||||||
options?: { pageIds?: number[]; force?: boolean },
|
|
||||||
): Promise<{ success: boolean; pages_queued: number; task_ids: number[]; celery_task_id?: string }> {
|
|
||||||
const res = await client.post(`/blueprints/${blueprintId}/generate_all_pages/`, {
|
|
||||||
page_ids: options?.pageIds,
|
|
||||||
force: options?.force || false,
|
|
||||||
});
|
|
||||||
return res.data?.data || res.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
async createTasksForPages(
|
|
||||||
blueprintId: number,
|
|
||||||
pageIds?: number[],
|
|
||||||
): Promise<{ tasks: unknown[]; count: number }> {
|
|
||||||
const res = await client.post(`/blueprints/${blueprintId}/create_tasks/`, {
|
|
||||||
page_ids: pageIds,
|
|
||||||
});
|
|
||||||
return res.data?.data || res.data;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
const API_ROOT = import.meta.env.VITE_API_URL ?? 'http://localhost:8010/api';
|
|
||||||
|
|
||||||
const client = axios.create({
|
|
||||||
baseURL: API_ROOT,
|
|
||||||
withCredentials: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface TaskProgressResponse {
|
|
||||||
state: 'PENDING' | 'PROGRESS' | 'SUCCESS' | 'FAILURE';
|
|
||||||
meta?: {
|
|
||||||
phase?: string;
|
|
||||||
percentage?: number;
|
|
||||||
message?: string;
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const systemApi = {
|
|
||||||
async getTaskProgress(taskId: string): Promise<TaskProgressResponse> {
|
|
||||||
const res = await client.get(`/v1/system/settings/task_progress/${taskId}/`);
|
|
||||||
return res.data;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.0 KiB |
@@ -1,45 +0,0 @@
|
|||||||
.sb-card {
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 16px;
|
|
||||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
||||||
padding: 1.5rem;
|
|
||||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-card__header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-card__title {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #0f172a;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-card__description {
|
|
||||||
color: #475569;
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-card__body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-card__footer {
|
|
||||||
border-top: 1px solid rgba(15, 23, 42, 0.06);
|
|
||||||
padding-top: 1rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import type { PropsWithChildren, ReactNode } from 'react';
|
|
||||||
import './Card.css';
|
|
||||||
|
|
||||||
interface CardProps extends PropsWithChildren {
|
|
||||||
title?: ReactNode;
|
|
||||||
description?: ReactNode;
|
|
||||||
footer?: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Card({ title, description, footer, children }: CardProps) {
|
|
||||||
return (
|
|
||||||
<section className="sb-card">
|
|
||||||
{(title || description) && (
|
|
||||||
<header className="sb-card__header">
|
|
||||||
{title && <h2 className="sb-card__title">{title}</h2>}
|
|
||||||
{description && <p className="sb-card__description">{description}</p>}
|
|
||||||
</header>
|
|
||||||
)}
|
|
||||||
<div className="sb-card__body">{children}</div>
|
|
||||||
{footer && <footer className="sb-card__footer">{footer}</footer>}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
.progress-modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-modal {
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 24px;
|
|
||||||
min-width: 400px;
|
|
||||||
max-width: 500px;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-modal-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-modal-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-modal-close {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: #666;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-modal-close:hover {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-modal-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-modal-message {
|
|
||||||
margin: 0;
|
|
||||||
color: #666;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-modal-bar {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-modal-bar-track {
|
|
||||||
height: 8px;
|
|
||||||
background: #e5e7eb;
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-modal-bar-fill {
|
|
||||||
height: 100%;
|
|
||||||
background: #3b82f6;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-modal-bar-text {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #666;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-modal-spinner {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-modal-spinner .spin {
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-modal-task-id {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #999;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-modal-task-id code {
|
|
||||||
background: #f3f4f6;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { X, Loader2 } from 'lucide-react';
|
|
||||||
import './ProgressModal.css';
|
|
||||||
|
|
||||||
interface ProgressModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
title: string;
|
|
||||||
message?: string;
|
|
||||||
progress?: {
|
|
||||||
current: number;
|
|
||||||
total: number;
|
|
||||||
};
|
|
||||||
taskId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ProgressModal({ isOpen, onClose, title, message, progress, taskId }: ProgressModalProps) {
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
} else {
|
|
||||||
document.body.style.overflow = '';
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
document.body.style.overflow = '';
|
|
||||||
};
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
const progressPercent = progress ? Math.round((progress.current / progress.total) * 100) : 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="progress-modal-overlay" onClick={onClose}>
|
|
||||||
<div className="progress-modal" onClick={(e) => e.stopPropagation()}>
|
|
||||||
<div className="progress-modal-header">
|
|
||||||
<h3>{title}</h3>
|
|
||||||
<button type="button" className="progress-modal-close" onClick={onClose} aria-label="Close">
|
|
||||||
<X size={20} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="progress-modal-content">
|
|
||||||
{message && <p className="progress-modal-message">{message}</p>}
|
|
||||||
|
|
||||||
{progress && (
|
|
||||||
<div className="progress-modal-bar">
|
|
||||||
<div className="progress-modal-bar-track">
|
|
||||||
<div
|
|
||||||
className="progress-modal-bar-fill"
|
|
||||||
style={{ width: `${progressPercent}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="progress-modal-bar-text">
|
|
||||||
{progress.current} of {progress.total} pages
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!progress && (
|
|
||||||
<div className="progress-modal-spinner">
|
|
||||||
<Loader2 className="spin" size={24} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{taskId && (
|
|
||||||
<p className="progress-modal-task-id">
|
|
||||||
Task ID: <code>{taskId}</code>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import './HeroBlock.css';
|
|
||||||
|
|
||||||
interface HeroBlockProps {
|
|
||||||
heading: string;
|
|
||||||
subheading?: string;
|
|
||||||
ctaLabel?: string;
|
|
||||||
secondaryCta?: string;
|
|
||||||
badge?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HeroBlock({ heading, subheading, ctaLabel, secondaryCta, badge }: HeroBlockProps) {
|
|
||||||
return (
|
|
||||||
<div className="sb-hero-block">
|
|
||||||
{badge && <span className="sb-hero-block__badge">{badge}</span>}
|
|
||||||
<h1>{heading}</h1>
|
|
||||||
{subheading && <p>{subheading}</p>}
|
|
||||||
<div className="sb-hero-block__ctas">
|
|
||||||
{ctaLabel && <button>{ctaLabel}</button>}
|
|
||||||
{secondaryCta && (
|
|
||||||
<button className="ghost" type="button">
|
|
||||||
{secondaryCta}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
.sb-page-canvas {
|
|
||||||
border-radius: 24px;
|
|
||||||
padding: 2.5rem;
|
|
||||||
box-shadow: 0 30px 80px rgba(15, 23, 42, 0.12);
|
|
||||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-page-canvas__body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import type { PropsWithChildren } from 'react';
|
|
||||||
import { palette } from '../theme';
|
|
||||||
import './PageCanvas.css';
|
|
||||||
|
|
||||||
export function PageCanvas({ children }: PropsWithChildren) {
|
|
||||||
return (
|
|
||||||
<article className="sb-page-canvas" style={{ background: palette.background }}>
|
|
||||||
<div className="sb-page-canvas__body">{children}</div>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
.sb-section {
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 1.75rem;
|
|
||||||
border: 1px solid rgba(15, 23, 42, 0.07);
|
|
||||||
background: #ffffff;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-section--soft {
|
|
||||||
background: #f4f6ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-section__header h3 {
|
|
||||||
margin: 0.15rem 0;
|
|
||||||
font-size: 1.8rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-section__subtitle {
|
|
||||||
margin: 0;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-section__overline {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #6366f1;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sb-section__content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import type { PropsWithChildren, ReactNode } from 'react';
|
|
||||||
import './Section.css';
|
|
||||||
|
|
||||||
interface SectionProps extends PropsWithChildren {
|
|
||||||
overline?: string;
|
|
||||||
title?: ReactNode;
|
|
||||||
subtitle?: ReactNode;
|
|
||||||
background?: 'surface' | 'soft';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Section({ overline, title, subtitle, background = 'surface', children }: SectionProps) {
|
|
||||||
return (
|
|
||||||
<section className={`sb-section sb-section--${background}`}>
|
|
||||||
{(overline || title || subtitle) && (
|
|
||||||
<header className="sb-section__header">
|
|
||||||
{overline && <p className="sb-section__overline">{overline}</p>}
|
|
||||||
{title && <h3>{title}</h3>}
|
|
||||||
{subtitle && <p className="sb-section__subtitle">{subtitle}</p>}
|
|
||||||
</header>
|
|
||||||
)}
|
|
||||||
<div className="sb-section__content">{children}</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
export const palette = {
|
|
||||||
background: '#f8fbff',
|
|
||||||
surface: '#ffffff',
|
|
||||||
accent: '#6366f1',
|
|
||||||
accentSoft: '#eef2ff',
|
|
||||||
text: '#0f172a',
|
|
||||||
textMuted: '#64748b',
|
|
||||||
border: 'rgba(15, 23, 42, 0.08)',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const typography = {
|
|
||||||
title: {
|
|
||||||
fontSize: '2.5rem',
|
|
||||||
fontWeight: 700,
|
|
||||||
lineHeight: 1.1,
|
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
fontSize: '1.15rem',
|
|
||||||
color: palette.textMuted,
|
|
||||||
lineHeight: 1.5,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
fontSize: '0.75rem',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
letterSpacing: '0.08em',
|
|
||||||
color: palette.accent,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { systemApi, type TaskProgressResponse } from '../api/system.api';
|
|
||||||
|
|
||||||
type TaskStatus = 'idle' | 'pending' | 'processing' | 'completed' | 'error';
|
|
||||||
|
|
||||||
interface UseTaskProgressOptions {
|
|
||||||
onComplete?: () => void;
|
|
||||||
onError?: (message: string) => void;
|
|
||||||
onUpdate?: (meta: TaskProgressResponse['meta']) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TaskProgressState {
|
|
||||||
status: TaskStatus;
|
|
||||||
percentage: number;
|
|
||||||
message?: string;
|
|
||||||
phase?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTaskProgress(taskId: string | null, options: UseTaskProgressOptions = {}) {
|
|
||||||
const { onComplete, onError, onUpdate } = options;
|
|
||||||
const [state, setState] = useState<TaskProgressState>({
|
|
||||||
status: 'idle',
|
|
||||||
percentage: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!taskId) {
|
|
||||||
setState({ status: 'idle', percentage: 0 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let isActive = true;
|
|
||||||
let interval: ReturnType<typeof setInterval> | null = null;
|
|
||||||
|
|
||||||
const mapResponseToState = (response: TaskProgressResponse): TaskProgressState => {
|
|
||||||
const { state: taskState, meta } = response;
|
|
||||||
const percentage = typeof meta?.percentage === 'number' ? meta.percentage : 0;
|
|
||||||
const message = meta?.message;
|
|
||||||
const phase = meta?.phase;
|
|
||||||
|
|
||||||
if (taskState === 'SUCCESS') {
|
|
||||||
return { status: 'completed', percentage: 100, message: message ?? 'Site structure ready.', phase };
|
|
||||||
}
|
|
||||||
if (taskState === 'FAILURE') {
|
|
||||||
return { status: 'error', percentage, message: meta?.error ?? message ?? 'Task failed.', phase };
|
|
||||||
}
|
|
||||||
if (taskState === 'PROGRESS') {
|
|
||||||
return { status: 'processing', percentage: Math.min(percentage || 10, 95), message, phase };
|
|
||||||
}
|
|
||||||
return { status: 'pending', percentage, message, phase };
|
|
||||||
};
|
|
||||||
|
|
||||||
const poll = async () => {
|
|
||||||
try {
|
|
||||||
const response = await systemApi.getTaskProgress(taskId);
|
|
||||||
|
|
||||||
if (!isActive) return;
|
|
||||||
|
|
||||||
const nextState = mapResponseToState(response);
|
|
||||||
setState(nextState);
|
|
||||||
onUpdate?.(response.meta);
|
|
||||||
|
|
||||||
if (nextState.status === 'completed') {
|
|
||||||
onComplete?.();
|
|
||||||
if (interval) {
|
|
||||||
clearInterval(interval);
|
|
||||||
interval = null;
|
|
||||||
}
|
|
||||||
} else if (nextState.status === 'error') {
|
|
||||||
onError?.(nextState.message ?? 'Task failed');
|
|
||||||
if (interval) {
|
|
||||||
clearInterval(interval);
|
|
||||||
interval = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (!isActive) return;
|
|
||||||
const message = error instanceof Error ? error.message : 'Unable to load task progress';
|
|
||||||
setState({ status: 'error', percentage: 0, message });
|
|
||||||
onError?.(message);
|
|
||||||
if (interval) {
|
|
||||||
clearInterval(interval);
|
|
||||||
interval = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
poll();
|
|
||||||
interval = setInterval(poll, 2000);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isActive = false;
|
|
||||||
if (interval) {
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [taskId, onComplete, onError, onUpdate]);
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
:root {
|
|
||||||
font-family: 'Inter', 'Inter var', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
||||||
color: #0f172a;
|
|
||||||
background-color: #f5f7fb;
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
background: #f5f7fb;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { StrictMode } from 'react';
|
|
||||||
import { createRoot } from 'react-dom/client';
|
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
|
||||||
import './index.css';
|
|
||||||
import App from './App.tsx';
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
|
||||||
<StrictMode>
|
|
||||||
<BrowserRouter>
|
|
||||||
<App />
|
|
||||||
</BrowserRouter>
|
|
||||||
</StrictMode>,
|
|
||||||
);
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { Loader2, Play } from 'lucide-react';
|
|
||||||
import { builderApi } from '../../api/builder.api';
|
|
||||||
import type { SiteBlueprint } from '../../types/siteBuilder';
|
|
||||||
import { Card } from '../../components/common/Card';
|
|
||||||
import { useBuilderStore } from '../../state/builderStore';
|
|
||||||
import { ProgressModal } from '../../components/common/ProgressModal';
|
|
||||||
|
|
||||||
export function SiteDashboard() {
|
|
||||||
const [blueprints, setBlueprints] = useState<SiteBlueprint[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<string | undefined>();
|
|
||||||
const { generateAllPages, isGenerating, generationProgress } = useBuilderStore();
|
|
||||||
const [showProgress, setShowProgress] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const data = await builderApi.listBlueprints();
|
|
||||||
setBlueprints(data);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Unable to load blueprints');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleGenerateAll = async (blueprintId: number) => {
|
|
||||||
setShowProgress(true);
|
|
||||||
try {
|
|
||||||
await generateAllPages(blueprintId);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Failed to generate pages');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Card title="Blueprint history" description="Every generated structure is stored and can be reopened.">
|
|
||||||
{loading && (
|
|
||||||
<div className="sb-loading">
|
|
||||||
<Loader2 className="spin" size={18} /> Loading blueprints…
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{error && <p className="sb-error">{error}</p>}
|
|
||||||
|
|
||||||
{!loading && !blueprints.length && (
|
|
||||||
<p className="sb-muted">You haven't generated any sites yet. Launch the wizard to create your first one.</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ul className="sb-blueprint-list">
|
|
||||||
{blueprints.map((bp) => (
|
|
||||||
<li key={bp.id}>
|
|
||||||
<div>
|
|
||||||
<strong>{bp.name}</strong>
|
|
||||||
<span>{bp.description}</span>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
||||||
{bp.status === 'ready' && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleGenerateAll(bp.id)}
|
|
||||||
disabled={isGenerating}
|
|
||||||
className="sb-button sb-button--primary"
|
|
||||||
style={{ display: 'flex', alignItems: 'center', gap: '4px', padding: '4px 12px' }}
|
|
||||||
>
|
|
||||||
<Play size={14} />
|
|
||||||
Generate All Pages
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<span className={`status-dot status-${bp.status}`}>{bp.status}</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<ProgressModal
|
|
||||||
isOpen={showProgress}
|
|
||||||
onClose={() => setShowProgress(false)}
|
|
||||||
title="Generating Pages"
|
|
||||||
message={isGenerating ? 'Generating content for all pages...' : 'Generation completed!'}
|
|
||||||
progress={
|
|
||||||
generationProgress
|
|
||||||
? {
|
|
||||||
current: generationProgress.pagesQueued,
|
|
||||||
total: generationProgress.pagesQueued,
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
taskId={generationProgress?.celeryTaskId}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,353 +0,0 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import {
|
|
||||||
FeatureGridBlock,
|
|
||||||
HeroBlock,
|
|
||||||
MarketingTemplate,
|
|
||||||
StatsPanel,
|
|
||||||
type FeatureGridBlockProps,
|
|
||||||
type StatItem,
|
|
||||||
} from '@shared';
|
|
||||||
import { useSiteDefinitionStore } from '../../state/siteDefinitionStore';
|
|
||||||
import { useBuilderStore } from '../../state/builderStore';
|
|
||||||
import type { PageBlock, PageBlueprint, SiteStructure } from '../../types/siteBuilder';
|
|
||||||
|
|
||||||
type StructuredContent = Record<string, unknown> & {
|
|
||||||
items?: unknown[];
|
|
||||||
eyebrow?: string;
|
|
||||||
ctaLabel?: string;
|
|
||||||
supportingCopy?: string;
|
|
||||||
columns?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function PreviewCanvas() {
|
|
||||||
const { structure, pages, selectedSlug, selectPage } = useSiteDefinitionStore();
|
|
||||||
const { selectedPageIds, togglePageSelection, selectAllPages, clearPageSelection, activeBlueprint } =
|
|
||||||
useBuilderStore();
|
|
||||||
|
|
||||||
const page = useMemo(() => {
|
|
||||||
if (structure?.pages?.length) {
|
|
||||||
return structure.pages.find((p) => p.slug === selectedSlug) ?? structure.pages[0];
|
|
||||||
}
|
|
||||||
return pages.find((p) => p.slug === selectedSlug) ?? pages[0];
|
|
||||||
}, [structure, pages, selectedSlug]);
|
|
||||||
|
|
||||||
if (!structure && !pages.length) {
|
|
||||||
return (
|
|
||||||
<div className="preview-placeholder">
|
|
||||||
<p>Generate a blueprint to see live previews of every page.</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const navItems = structure?.site?.primary_navigation ?? pages.map((p) => p.slug);
|
|
||||||
const blocks = getBlocks(page);
|
|
||||||
const heroBlock = blocks.find((block) => normalizeType(block.type) === 'hero');
|
|
||||||
const contentBlocks = heroBlock ? blocks.filter((block) => block !== heroBlock) : blocks;
|
|
||||||
|
|
||||||
const heroSection =
|
|
||||||
heroBlock || page
|
|
||||||
? renderBlock(heroBlock ?? buildFallbackHero(page, structure))
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const sectionNodes =
|
|
||||||
contentBlocks.length > 0
|
|
||||||
? contentBlocks.map((block, index) => <div key={`${block.type}-${index}`}>{renderBlock(block)}</div>)
|
|
||||||
: buildFallbackSections(page);
|
|
||||||
|
|
||||||
const sidebar = (
|
|
||||||
<div className="preview-sidebar">
|
|
||||||
<p className="preview-label">Page objective</p>
|
|
||||||
<h4>{page?.objective ?? 'Launch a high-converting page'}</h4>
|
|
||||||
<ul className="preview-sidebar__list">
|
|
||||||
{buildSidebarInsights(page, structure).map((insight) => (
|
|
||||||
<li key={insight.label}>
|
|
||||||
<span>{insight.label}</span>
|
|
||||||
<strong>{insight.value}</strong>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Only show page selection if we have actual PageBlueprint objects with IDs
|
|
||||||
const hasPageBlueprints = pages.length > 0 && pages.every((p) => p.id > 0);
|
|
||||||
|
|
||||||
const allSelected = hasPageBlueprints && pages.length > 0 && selectedPageIds.length === pages.length;
|
|
||||||
const someSelected = hasPageBlueprints && selectedPageIds.length > 0 && selectedPageIds.length < pages.length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="preview-canvas">
|
|
||||||
<div className="preview-nav">
|
|
||||||
{hasPageBlueprints && activeBlueprint && (
|
|
||||||
<div className="preview-page-selection" style={{ marginBottom: '12px', padding: '8px', background: '#f5f5f5', borderRadius: '4px' }}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={allSelected}
|
|
||||||
ref={(input) => {
|
|
||||||
if (input) input.indeterminate = someSelected;
|
|
||||||
}}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
selectAllPages();
|
|
||||||
} else {
|
|
||||||
clearPageSelection();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
/>
|
|
||||||
<label style={{ cursor: 'pointer', fontSize: '14px', fontWeight: '500' }}>
|
|
||||||
Select pages for bulk generation ({selectedPageIds.length} selected)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
|
||||||
{pages.map((p) => {
|
|
||||||
const isSelected = selectedPageIds.includes(p.id);
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
key={p.id}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '4px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: '12px',
|
|
||||||
padding: '4px 8px',
|
|
||||||
background: isSelected ? '#e3f2fd' : 'white',
|
|
||||||
border: `1px solid ${isSelected ? '#2196f3' : '#ddd'}`,
|
|
||||||
borderRadius: '4px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={isSelected}
|
|
||||||
onChange={() => togglePageSelection(p.id)}
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
/>
|
|
||||||
<span>{p.title || p.slug.replace('-', ' ')}</span>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{navItems?.map((slug) => (
|
|
||||||
<button
|
|
||||||
key={slug}
|
|
||||||
type="button"
|
|
||||||
onClick={() => selectPage(slug)}
|
|
||||||
className={slug === (page?.slug ?? '') ? 'is-active' : ''}
|
|
||||||
>
|
|
||||||
{slug.replace('-', ' ')}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MarketingTemplate hero={heroSection} sections={sectionNodes} sidebar={sidebar} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBlocks(
|
|
||||||
page: (SiteStructure['pages'][number] & { blocks_json?: PageBlock[] }) | PageBlueprint | undefined,
|
|
||||||
) {
|
|
||||||
if (!page) return [];
|
|
||||||
const fromStructure = (page as { blocks?: PageBlock[] }).blocks;
|
|
||||||
if (Array.isArray(fromStructure)) return fromStructure;
|
|
||||||
const fromBlueprint = (page as PageBlueprint).blocks_json;
|
|
||||||
return Array.isArray(fromBlueprint) ? fromBlueprint : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderBlock(block?: PageBlock) {
|
|
||||||
if (!block) return null;
|
|
||||||
const type = normalizeType(block.type);
|
|
||||||
const structuredContent = extractStructuredContent(block);
|
|
||||||
const listContent = extractListContent(block, structuredContent);
|
|
||||||
|
|
||||||
if (type === 'hero') {
|
|
||||||
return (
|
|
||||||
<HeroBlock
|
|
||||||
eyebrow={structuredContent.eyebrow as string | undefined}
|
|
||||||
title={block.heading ?? 'Untitled hero'}
|
|
||||||
subtitle={block.subheading ?? (structuredContent.supportingCopy as string | undefined)}
|
|
||||||
ctaLabel={(structuredContent.ctaLabel as string | undefined) ?? undefined}
|
|
||||||
supportingContent={
|
|
||||||
listContent.length > 0 ? (
|
|
||||||
<ul>
|
|
||||||
{listContent.map((item) => (
|
|
||||||
<li key={String(item)}>{String(item)}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'feature-grid' || type === 'features' || type === 'value-props') {
|
|
||||||
const features = toFeatureList(listContent, structuredContent.items);
|
|
||||||
const columns = normalizeColumns(structuredContent.columns, features.length);
|
|
||||||
return <FeatureGridBlock heading={block.heading} features={features} columns={columns} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'stats' || type === 'metrics') {
|
|
||||||
const stats = toStatItems(listContent, structuredContent.items, block);
|
|
||||||
if (!stats.length) return defaultBlock(block);
|
|
||||||
return <StatsPanel heading={block.heading} stats={stats} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultBlock(block);
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultBlock(block: PageBlock) {
|
|
||||||
return (
|
|
||||||
<div className="preview-block preview-block--legacy">
|
|
||||||
{block.heading && <h4>{block.heading}</h4>}
|
|
||||||
{block.subheading && <p>{block.subheading}</p>}
|
|
||||||
{Array.isArray(block.content) && (
|
|
||||||
<ul>
|
|
||||||
{block.content.map((item) => (
|
|
||||||
<li key={item}>{item}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeType(type?: string) {
|
|
||||||
return (type ?? '').toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractStructuredContent(block: PageBlock): StructuredContent {
|
|
||||||
if (Array.isArray(block.content)) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return (block.content ?? {}) as StructuredContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractListContent(block: PageBlock, structuredContent: StructuredContent): unknown[] {
|
|
||||||
if (Array.isArray(block.content)) {
|
|
||||||
return block.content;
|
|
||||||
}
|
|
||||||
if (Array.isArray(structuredContent.items)) {
|
|
||||||
return structuredContent.items;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function toFeatureList(listItems: unknown[], structuredItems?: unknown[]): FeatureGridBlockProps['features'] {
|
|
||||||
const source = structuredItems && Array.isArray(structuredItems) && structuredItems.length > 0 ? structuredItems : listItems;
|
|
||||||
return source.map((item) => {
|
|
||||||
if (typeof item === 'string') {
|
|
||||||
return { title: item };
|
|
||||||
}
|
|
||||||
if (typeof item === 'object' && item) {
|
|
||||||
const record = item as Record<string, unknown>;
|
|
||||||
return {
|
|
||||||
title: String(record.title ?? record.heading ?? 'Feature'),
|
|
||||||
description: record.description ? String(record.description) : undefined,
|
|
||||||
icon: record.icon ? String(record.icon) : undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { title: String(item) };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function toStatItems(
|
|
||||||
listItems: unknown[],
|
|
||||||
structuredItems: unknown[] | undefined,
|
|
||||||
block: PageBlock,
|
|
||||||
): StatItem[] {
|
|
||||||
const source = structuredItems && Array.isArray(structuredItems) && structuredItems.length > 0 ? structuredItems : listItems;
|
|
||||||
return source
|
|
||||||
.map((item, index) => {
|
|
||||||
if (typeof item === 'string') {
|
|
||||||
return {
|
|
||||||
label: block.heading ?? `Metric ${index + 1}`,
|
|
||||||
value: item,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (typeof item === 'object' && item) {
|
|
||||||
const record = item as Record<string, unknown>;
|
|
||||||
const label = record.label ?? record.title ?? `Metric ${index + 1}`;
|
|
||||||
const value = record.value ?? record.metric ?? record.score;
|
|
||||||
if (!value) return null;
|
|
||||||
return {
|
|
||||||
label: String(label),
|
|
||||||
value: String(value),
|
|
||||||
description: record.description ? String(record.description) : undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
.filter((stat): stat is StatItem => Boolean(stat));
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeColumns(
|
|
||||||
candidate: StructuredContent['columns'],
|
|
||||||
featureCount: number,
|
|
||||||
): FeatureGridBlockProps['columns'] {
|
|
||||||
const inferred = typeof candidate === 'number' ? candidate : featureCount >= 4 ? 4 : featureCount === 2 ? 2 : 3;
|
|
||||||
if (inferred <= 2) return 2;
|
|
||||||
if (inferred >= 4) return 4;
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildFallbackHero(
|
|
||||||
page: SiteStructure['pages'][number] | PageBlueprint | undefined,
|
|
||||||
structure: SiteStructure | undefined,
|
|
||||||
): PageBlock {
|
|
||||||
return {
|
|
||||||
type: 'hero',
|
|
||||||
heading: page?.title ?? 'Site Builder preview',
|
|
||||||
subheading: structure?.site?.hero_message ?? 'Preview updates as the AI hydrates your blueprint.',
|
|
||||||
content: Array.isArray(structure?.site?.secondary_navigation) ? structure?.site?.secondary_navigation : [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildFallbackSections(page: SiteStructure['pages'][number] | PageBlueprint | undefined) {
|
|
||||||
return [
|
|
||||||
<FeatureGridBlock
|
|
||||||
key="fallback-features"
|
|
||||||
heading="Generated sections"
|
|
||||||
features={[
|
|
||||||
{ title: 'AI messaging kit', description: 'Structured copy generated for each funnel stage.' },
|
|
||||||
{ title: 'Audience resonance', description: 'Language tuned to your target segment.' },
|
|
||||||
{ title: 'Conversion spine', description: 'CTA hierarchy anchored to your objectives.' },
|
|
||||||
]}
|
|
||||||
/>,
|
|
||||||
<StatsPanel
|
|
||||||
key="fallback-stats"
|
|
||||||
heading="Blueprint signals"
|
|
||||||
stats={[
|
|
||||||
{ label: 'Page type', value: page?.type ?? 'Landing' },
|
|
||||||
{ label: 'Status', value: page?.status ?? 'Draft' },
|
|
||||||
{ label: 'Blocks', value: '0' },
|
|
||||||
]}
|
|
||||||
/>,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildSidebarInsights(
|
|
||||||
page: SiteStructure['pages'][number] | PageBlueprint | undefined,
|
|
||||||
structure: SiteStructure | undefined,
|
|
||||||
) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: 'Primary CTA',
|
|
||||||
value: page?.primary_cta ?? 'Book a demo',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Tone',
|
|
||||||
value: structure?.site?.tone ?? 'Confident & clear',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Status',
|
|
||||||
value: page?.status ?? 'Draft',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import { PreviewCanvas } from '../PreviewCanvas';
|
|
||||||
import { useSiteDefinitionStore } from '../../../state/siteDefinitionStore';
|
|
||||||
|
|
||||||
vi.mock('../../../state/siteDefinitionStore');
|
|
||||||
|
|
||||||
describe('PreviewCanvas', () => {
|
|
||||||
const mockSelectPage = vi.fn();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows placeholder when no structure or pages', () => {
|
|
||||||
(useSiteDefinitionStore as any).mockReturnValue({
|
|
||||||
structure: undefined,
|
|
||||||
pages: [],
|
|
||||||
selectedSlug: undefined,
|
|
||||||
selectPage: mockSelectPage,
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<PreviewCanvas />);
|
|
||||||
|
|
||||||
expect(screen.getByText(/generate a blueprint to see live previews/i)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders pages from structure', () => {
|
|
||||||
const mockStructure = {
|
|
||||||
site: { name: 'Test Site', primary_navigation: ['home', 'about'] },
|
|
||||||
pages: [
|
|
||||||
{ slug: 'home', title: 'Home', type: 'home', blocks: [] },
|
|
||||||
{ slug: 'about', title: 'About', type: 'about', blocks: [] },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
(useSiteDefinitionStore as any).mockReturnValue({
|
|
||||||
structure: mockStructure,
|
|
||||||
pages: [],
|
|
||||||
selectedSlug: 'home',
|
|
||||||
selectPage: mockSelectPage,
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<PreviewCanvas />);
|
|
||||||
|
|
||||||
expect(screen.getByText('Home')).toBeInTheDocument();
|
|
||||||
// Check for navigation button specifically (there are multiple "home" elements)
|
|
||||||
const navButtons = screen.getAllByText('home');
|
|
||||||
expect(navButtons.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders pages from pages array when structure not available', () => {
|
|
||||||
const mockPages = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
site_blueprint: 1,
|
|
||||||
slug: 'services',
|
|
||||||
title: 'Services',
|
|
||||||
type: 'services',
|
|
||||||
status: 'ready',
|
|
||||||
order: 0,
|
|
||||||
blocks_json: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
(useSiteDefinitionStore as any).mockReturnValue({
|
|
||||||
structure: undefined,
|
|
||||||
pages: mockPages,
|
|
||||||
selectedSlug: 'services',
|
|
||||||
selectPage: mockSelectPage,
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<PreviewCanvas />);
|
|
||||||
|
|
||||||
expect(screen.getByText('Services')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders page blocks when available', () => {
|
|
||||||
const mockStructure = {
|
|
||||||
site: { name: 'Test Site' },
|
|
||||||
pages: [
|
|
||||||
{
|
|
||||||
slug: 'home',
|
|
||||||
title: 'Home',
|
|
||||||
type: 'home',
|
|
||||||
blocks: [
|
|
||||||
{ type: 'hero', heading: 'Welcome', subheading: 'Get started today' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
(useSiteDefinitionStore as any).mockReturnValue({
|
|
||||||
structure: mockStructure,
|
|
||||||
pages: [],
|
|
||||||
selectedSlug: 'home',
|
|
||||||
selectPage: mockSelectPage,
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<PreviewCanvas />);
|
|
||||||
|
|
||||||
expect(screen.getByText('Welcome')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Get started today')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
||||||
import { Loader2, PlayCircle, RefreshCw } from 'lucide-react';
|
|
||||||
import { useBuilderStore } from '../../state/builderStore';
|
|
||||||
import { useSiteDefinitionStore } from '../../state/siteDefinitionStore';
|
|
||||||
import { BusinessDetailsStep } from './steps/BusinessDetailsStep';
|
|
||||||
import { BriefStep } from './steps/BriefStep';
|
|
||||||
import { ObjectivesStep } from './steps/ObjectivesStep';
|
|
||||||
import { StyleStep } from './steps/StyleStep';
|
|
||||||
import { Card } from '../../components/common/Card';
|
|
||||||
import { ProgressModal } from '../../components/common/ProgressModal';
|
|
||||||
import { useTaskProgress } from '../../hooks/useTaskProgress';
|
|
||||||
import { builderApi } from '../../api/builder.api';
|
|
||||||
|
|
||||||
const stepTitles = ['Business', 'Brief', 'Objectives', 'Style'];
|
|
||||||
|
|
||||||
export function WizardPage() {
|
|
||||||
const {
|
|
||||||
form,
|
|
||||||
currentStep,
|
|
||||||
setField,
|
|
||||||
updateStyle,
|
|
||||||
addObjective,
|
|
||||||
removeObjective,
|
|
||||||
nextStep,
|
|
||||||
previousStep,
|
|
||||||
setStep,
|
|
||||||
submitWizard,
|
|
||||||
isSubmitting,
|
|
||||||
error,
|
|
||||||
activeBlueprint,
|
|
||||||
refreshPages,
|
|
||||||
structureTaskId,
|
|
||||||
setStructureTaskId,
|
|
||||||
} = useBuilderStore();
|
|
||||||
const structure = useSiteDefinitionStore((state) => state.structure);
|
|
||||||
const setStructure = useSiteDefinitionStore((state) => state.setStructure);
|
|
||||||
const [showProgressModal, setShowProgressModal] = useState(false);
|
|
||||||
const [progressMessage, setProgressMessage] = useState('Initializing Site Builder AI…');
|
|
||||||
|
|
||||||
const syncLatestStructure = useCallback(async () => {
|
|
||||||
if (!activeBlueprint) return;
|
|
||||||
try {
|
|
||||||
const latestBlueprint = await builderApi.getBlueprint(activeBlueprint.id);
|
|
||||||
if (latestBlueprint.structure_json) {
|
|
||||||
setStructure(latestBlueprint.structure_json);
|
|
||||||
}
|
|
||||||
await refreshPages(activeBlueprint.id);
|
|
||||||
} catch (syncError) {
|
|
||||||
const message = syncError instanceof Error ? syncError.message : 'Unable to sync blueprint';
|
|
||||||
useBuilderStore.setState({ error: message });
|
|
||||||
} finally {
|
|
||||||
setStructureTaskId(null);
|
|
||||||
}
|
|
||||||
}, [activeBlueprint, refreshPages, setStructure, setStructureTaskId]);
|
|
||||||
|
|
||||||
const taskProgress = useTaskProgress(structureTaskId, {
|
|
||||||
onUpdate: (meta) => {
|
|
||||||
if (meta?.message) {
|
|
||||||
setProgressMessage(meta.message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onComplete: async () => {
|
|
||||||
setProgressMessage('Site structure ready!');
|
|
||||||
await syncLatestStructure();
|
|
||||||
setTimeout(() => setShowProgressModal(false), 500);
|
|
||||||
},
|
|
||||||
onError: (message) => {
|
|
||||||
useBuilderStore.setState({ error: message });
|
|
||||||
setShowProgressModal(false);
|
|
||||||
setStructureTaskId(null);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (structureTaskId) {
|
|
||||||
setShowProgressModal(true);
|
|
||||||
setProgressMessage('Sending request to Site Builder AI…');
|
|
||||||
}
|
|
||||||
}, [structureTaskId]);
|
|
||||||
|
|
||||||
const stepComponents = useMemo(
|
|
||||||
() => [
|
|
||||||
<BusinessDetailsStep key="business" data={form} onChange={setField} />,
|
|
||||||
<BriefStep key="brief" data={form} onChange={setField} />,
|
|
||||||
<ObjectivesStep key="objectives" data={form} addObjective={addObjective} removeObjective={removeObjective} />,
|
|
||||||
<StyleStep key="style" style={form.style} onChange={updateStyle} />,
|
|
||||||
],
|
|
||||||
[form, setField, addObjective, removeObjective, updateStyle],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="wizard-page">
|
|
||||||
<Card
|
|
||||||
title="Site builder wizard"
|
|
||||||
description="Capture your strategy in four lightweight steps. When you hit “Generate structure” we’ll call the Site Builder AI and hydrate the preview canvas."
|
|
||||||
>
|
|
||||||
<div className="wizard-progress">
|
|
||||||
{stepTitles.map((title, idx) => (
|
|
||||||
<button
|
|
||||||
key={title}
|
|
||||||
type="button"
|
|
||||||
className={`wizard-progress__dot ${idx === currentStep ? 'is-active' : ''}`}
|
|
||||||
onClick={() => setStep(idx)}
|
|
||||||
>
|
|
||||||
<span>{idx + 1}</span>
|
|
||||||
<small>{title}</small>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="wizard-step">{stepComponents[currentStep]}</div>
|
|
||||||
|
|
||||||
<div className="wizard-actions">
|
|
||||||
<button type="button" onClick={previousStep} disabled={currentStep === 0 || isSubmitting}>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
{currentStep < stepComponents.length - 1 ? (
|
|
||||||
<button type="button" className="primary" onClick={nextStep}>
|
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button type="button" className="primary" onClick={submitWizard} disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="spin" size={18} />
|
|
||||||
Generating…
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<PlayCircle size={18} />
|
|
||||||
Generate structure
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && <p className="sb-error">{error}</p>}
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{activeBlueprint && (
|
|
||||||
<Card
|
|
||||||
title="Latest blueprint"
|
|
||||||
description="Refresh the preview to fetch the latest AI output."
|
|
||||||
footer={
|
|
||||||
<button type="button" className="ghost" onClick={() => refreshPages(activeBlueprint.id)} disabled={isSubmitting}>
|
|
||||||
<RefreshCw size={16} />
|
|
||||||
Sync pages
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="sb-blueprint-meta">
|
|
||||||
<div>
|
|
||||||
<strong>Status</strong>
|
|
||||||
<span className={`status-dot status-${activeBlueprint.status}`}>{activeBlueprint.status}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>Structure</strong>
|
|
||||||
<span>{structure?.pages?.length ?? 0} pages</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
<ProgressModal
|
|
||||||
isOpen={showProgressModal}
|
|
||||||
onClose={() => {
|
|
||||||
if (taskProgress.status === 'processing') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setShowProgressModal(false);
|
|
||||||
setStructureTaskId(null);
|
|
||||||
}}
|
|
||||||
title="Generating site structure"
|
|
||||||
message={progressMessage}
|
|
||||||
progress={{
|
|
||||||
current: Math.round(taskProgress.percentage),
|
|
||||||
total: 100,
|
|
||||||
}}
|
|
||||||
taskId={structureTaskId || undefined}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
||||||
import { render, screen, fireEvent } from '@testing-library/react';
|
|
||||||
import { WizardPage } from '../WizardPage';
|
|
||||||
import { useBuilderStore } from '../../../state/builderStore';
|
|
||||||
import { useSiteDefinitionStore } from '../../../state/siteDefinitionStore';
|
|
||||||
|
|
||||||
// Mock stores
|
|
||||||
vi.mock('../../../state/builderStore');
|
|
||||||
vi.mock('../../../state/siteDefinitionStore');
|
|
||||||
|
|
||||||
describe('WizardPage', () => {
|
|
||||||
const mockSetField = vi.fn();
|
|
||||||
const mockNextStep = vi.fn();
|
|
||||||
const mockPreviousStep = vi.fn();
|
|
||||||
const mockSetStep = vi.fn();
|
|
||||||
const mockUpdateStyle = vi.fn();
|
|
||||||
const mockAddObjective = vi.fn();
|
|
||||||
const mockRemoveObjective = vi.fn();
|
|
||||||
const mockSubmitWizard = vi.fn();
|
|
||||||
const mockSetStructureTaskId = vi.fn();
|
|
||||||
const mockRefreshPages = vi.fn();
|
|
||||||
const mockSetStructure = vi.fn();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
(useBuilderStore as any).mockReturnValue({
|
|
||||||
form: {
|
|
||||||
siteId: null,
|
|
||||||
sectorId: null,
|
|
||||||
siteName: '',
|
|
||||||
businessType: '',
|
|
||||||
industry: '',
|
|
||||||
targetAudience: '',
|
|
||||||
hostingType: 'igny8_sites',
|
|
||||||
businessBrief: '',
|
|
||||||
objectives: [],
|
|
||||||
style: {},
|
|
||||||
},
|
|
||||||
currentStep: 0,
|
|
||||||
isSubmitting: false,
|
|
||||||
error: undefined,
|
|
||||||
activeBlueprint: undefined,
|
|
||||||
structureTaskId: null,
|
|
||||||
setField: mockSetField,
|
|
||||||
nextStep: mockNextStep,
|
|
||||||
previousStep: mockPreviousStep,
|
|
||||||
setStep: mockSetStep,
|
|
||||||
updateStyle: mockUpdateStyle,
|
|
||||||
addObjective: mockAddObjective,
|
|
||||||
removeObjective: mockRemoveObjective,
|
|
||||||
submitWizard: mockSubmitWizard,
|
|
||||||
refreshPages: mockRefreshPages,
|
|
||||||
setStructureTaskId: mockSetStructureTaskId,
|
|
||||||
});
|
|
||||||
(useSiteDefinitionStore as any).mockImplementation((selector?: (state: any) => any) => {
|
|
||||||
const state = {
|
|
||||||
structure: undefined,
|
|
||||||
setStructure: mockSetStructure,
|
|
||||||
};
|
|
||||||
return selector ? selector(state) : state;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders wizard with step indicators', () => {
|
|
||||||
render(<WizardPage />);
|
|
||||||
|
|
||||||
expect(screen.getByText('Site builder wizard')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Business')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Brief')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Objectives')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Style')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('allows navigation between steps', () => {
|
|
||||||
render(<WizardPage />);
|
|
||||||
|
|
||||||
const briefButton = screen.getByText('Brief');
|
|
||||||
fireEvent.click(briefButton);
|
|
||||||
|
|
||||||
expect(mockSetStep).toHaveBeenCalledWith(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('disables back button on first step', () => {
|
|
||||||
render(<WizardPage />);
|
|
||||||
|
|
||||||
const backButton = screen.getByText('Back');
|
|
||||||
expect(backButton).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('enables back button after first step', () => {
|
|
||||||
(useBuilderStore as any).mockReturnValue({
|
|
||||||
form: {},
|
|
||||||
currentStep: 1,
|
|
||||||
isSubmitting: false,
|
|
||||||
error: undefined,
|
|
||||||
activeBlueprint: undefined,
|
|
||||||
structureTaskId: null,
|
|
||||||
setField: mockSetField,
|
|
||||||
nextStep: mockNextStep,
|
|
||||||
previousStep: mockPreviousStep,
|
|
||||||
setStep: mockSetStep,
|
|
||||||
updateStyle: mockUpdateStyle,
|
|
||||||
addObjective: mockAddObjective,
|
|
||||||
removeObjective: mockRemoveObjective,
|
|
||||||
submitWizard: mockSubmitWizard,
|
|
||||||
refreshPages: mockRefreshPages,
|
|
||||||
setStructureTaskId: mockSetStructureTaskId,
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<WizardPage />);
|
|
||||||
|
|
||||||
const backButton = screen.getByText('Back');
|
|
||||||
expect(backButton).not.toBeDisabled();
|
|
||||||
|
|
||||||
fireEvent.click(backButton);
|
|
||||||
expect(mockPreviousStep).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows error message when present', () => {
|
|
||||||
(useBuilderStore as any).mockReturnValue({
|
|
||||||
form: {},
|
|
||||||
currentStep: 0,
|
|
||||||
isSubmitting: false,
|
|
||||||
error: 'Test error message',
|
|
||||||
activeBlueprint: undefined,
|
|
||||||
structureTaskId: null,
|
|
||||||
setField: mockSetField,
|
|
||||||
nextStep: mockNextStep,
|
|
||||||
previousStep: mockPreviousStep,
|
|
||||||
setStep: mockSetStep,
|
|
||||||
updateStyle: mockUpdateStyle,
|
|
||||||
addObjective: mockAddObjective,
|
|
||||||
removeObjective: mockRemoveObjective,
|
|
||||||
submitWizard: mockSubmitWizard,
|
|
||||||
refreshPages: mockRefreshPages,
|
|
||||||
setStructureTaskId: mockSetStructureTaskId,
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<WizardPage />);
|
|
||||||
|
|
||||||
expect(screen.getByText('Test error message')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows loading state when submitting', () => {
|
|
||||||
(useBuilderStore as any).mockReturnValue({
|
|
||||||
form: {
|
|
||||||
siteId: null,
|
|
||||||
sectorId: null,
|
|
||||||
siteName: '',
|
|
||||||
businessType: '',
|
|
||||||
industry: '',
|
|
||||||
targetAudience: '',
|
|
||||||
hostingType: 'igny8_sites',
|
|
||||||
businessBrief: '',
|
|
||||||
objectives: [],
|
|
||||||
style: {
|
|
||||||
palette: 'Vibrant modern palette',
|
|
||||||
typography: 'Sans-serif',
|
|
||||||
personality: 'Confident',
|
|
||||||
heroImagery: 'Real people',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
currentStep: 3,
|
|
||||||
isSubmitting: true,
|
|
||||||
error: undefined,
|
|
||||||
activeBlueprint: undefined,
|
|
||||||
structureTaskId: null,
|
|
||||||
setField: mockSetField,
|
|
||||||
nextStep: mockNextStep,
|
|
||||||
previousStep: mockPreviousStep,
|
|
||||||
setStep: mockSetStep,
|
|
||||||
updateStyle: mockUpdateStyle,
|
|
||||||
addObjective: mockAddObjective,
|
|
||||||
removeObjective: mockRemoveObjective,
|
|
||||||
submitWizard: mockSubmitWizard,
|
|
||||||
refreshPages: mockRefreshPages,
|
|
||||||
setStructureTaskId: mockSetStructureTaskId,
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<WizardPage />);
|
|
||||||
|
|
||||||
// When submitting, button text changes to "Generating…"
|
|
||||||
const submitButton = screen.getByText(/generating/i);
|
|
||||||
expect(submitButton).toBeDisabled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import type { BuilderFormData } from '../../../types/siteBuilder';
|
|
||||||
import { Card } from '../../../components/common/Card';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
data: BuilderFormData;
|
|
||||||
onChange: <K extends keyof BuilderFormData>(key: K, value: BuilderFormData[K]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BriefStep({ data, onChange }: Props) {
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
title="Business brief"
|
|
||||||
description="Describe the brand, what it sells, and what makes it unique. The more context we have, the more accurate the structure."
|
|
||||||
>
|
|
||||||
<label className="sb-field">
|
|
||||||
<span>Business brief</span>
|
|
||||||
<textarea
|
|
||||||
rows={8}
|
|
||||||
value={data.businessBrief}
|
|
||||||
placeholder="Acme Robotics builds autonomous fulfillment robots that reduce warehouse picking time by 60%..."
|
|
||||||
onChange={(event) => onChange('businessBrief', event.target.value)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import type { BuilderFormData } from '../../../types/siteBuilder';
|
|
||||||
import { Card } from '../../../components/common/Card';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
data: BuilderFormData;
|
|
||||||
onChange: <K extends keyof BuilderFormData>(key: K, value: BuilderFormData[K]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BusinessDetailsStep({ data, onChange }: Props) {
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
title="Business context"
|
|
||||||
description="These details help the AI understand what kind of site we are building."
|
|
||||||
>
|
|
||||||
<div className="sb-grid">
|
|
||||||
<label className="sb-field">
|
|
||||||
<span>Site ID</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={data.siteId ?? ''}
|
|
||||||
placeholder="123"
|
|
||||||
onChange={(event) => onChange('siteId', Number(event.target.value) || null)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="sb-field">
|
|
||||||
<span>Sector ID</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={data.sectorId ?? ''}
|
|
||||||
placeholder="456"
|
|
||||||
onChange={(event) => onChange('sectorId', Number(event.target.value) || null)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label className="sb-field">
|
|
||||||
<span>Site name</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={data.siteName}
|
|
||||||
placeholder="Acme Robotics"
|
|
||||||
onChange={(event) => onChange('siteName', event.target.value)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="sb-grid">
|
|
||||||
<label className="sb-field">
|
|
||||||
<span>Business type</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={data.businessType}
|
|
||||||
placeholder="B2B SaaS platform"
|
|
||||||
onChange={(event) => onChange('businessType', event.target.value)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="sb-field">
|
|
||||||
<span>Industry</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={data.industry}
|
|
||||||
placeholder="Supply chain automation"
|
|
||||||
onChange={(event) => onChange('industry', event.target.value)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label className="sb-field">
|
|
||||||
<span>Target audience</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={data.targetAudience}
|
|
||||||
placeholder="Operations leaders at fast-scaling eCommerce brands"
|
|
||||||
onChange={(event) => onChange('targetAudience', event.target.value)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="sb-field">
|
|
||||||
<span>Hosting preference</span>
|
|
||||||
<select
|
|
||||||
value={data.hostingType}
|
|
||||||
onChange={(event) => onChange('hostingType', event.target.value as BuilderFormData['hostingType'])}
|
|
||||||
>
|
|
||||||
<option value="igny8_sites">IGNY8 Sites</option>
|
|
||||||
<option value="wordpress">WordPress</option>
|
|
||||||
<option value="shopify">Shopify</option>
|
|
||||||
<option value="multi">Multiple destinations</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import type { BuilderFormData } from '../../../types/siteBuilder';
|
|
||||||
import { Card } from '../../../components/common/Card';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
data: BuilderFormData;
|
|
||||||
addObjective: (value: string) => void;
|
|
||||||
removeObjective: (index: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ObjectivesStep({ data, addObjective, removeObjective }: Props) {
|
|
||||||
const [value, setValue] = useState('');
|
|
||||||
|
|
||||||
const handleAdd = () => {
|
|
||||||
const trimmed = value.trim();
|
|
||||||
if (!trimmed) return;
|
|
||||||
addObjective(trimmed);
|
|
||||||
setValue('');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
title="Success metrics & flows"
|
|
||||||
description="List the outcomes the site must accomplish. These become top-level navigation items and hero CTAs."
|
|
||||||
>
|
|
||||||
<div className="sb-pill-list">
|
|
||||||
{data.objectives.map((objective, idx) => (
|
|
||||||
<span className="sb-pill" key={`${objective}-${idx}`}>
|
|
||||||
{objective}
|
|
||||||
<button type="button" onClick={() => removeObjective(idx)} aria-label="Remove objective">
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="sb-objective-input">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={value}
|
|
||||||
placeholder="Offer product tour, capture demo requests, educate on ROI…"
|
|
||||||
onChange={(event) => setValue(event.target.value)}
|
|
||||||
/>
|
|
||||||
<button type="button" onClick={handleAdd}>
|
|
||||||
Add objective
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import type { StylePreferences } from '../../../types/siteBuilder';
|
|
||||||
import { Card } from '../../../components/common/Card';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
style: StylePreferences;
|
|
||||||
onChange: (partial: Partial<StylePreferences>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const palettes = [
|
|
||||||
'Minimal monochrome with bright accent',
|
|
||||||
'Rich jewel tones with high contrast',
|
|
||||||
'Soft gradients and glassmorphism',
|
|
||||||
'Playful pastel palette',
|
|
||||||
];
|
|
||||||
|
|
||||||
const typographyOptions = [
|
|
||||||
'Modern sans-serif for headings, serif body text',
|
|
||||||
'Editorial serif across the site',
|
|
||||||
'Geometric sans with tight tracking',
|
|
||||||
'Rounded fonts with friendly tone',
|
|
||||||
];
|
|
||||||
|
|
||||||
export function StyleStep({ style, onChange }: Props) {
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
title="Look & Feel"
|
|
||||||
description="Capture the brand personality so the preview canvas can mirror the right tone."
|
|
||||||
>
|
|
||||||
<div className="sb-grid">
|
|
||||||
<label className="sb-field">
|
|
||||||
<span>Palette direction</span>
|
|
||||||
<select value={style.palette} onChange={(event) => onChange({ palette: event.target.value })}>
|
|
||||||
{palettes.map((option) => (
|
|
||||||
<option key={option} value={option}>
|
|
||||||
{option}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="sb-field">
|
|
||||||
<span>Typography</span>
|
|
||||||
<select value={style.typography} onChange={(event) => onChange({ typography: event.target.value })}>
|
|
||||||
{typographyOptions.map((option) => (
|
|
||||||
<option key={option} value={option}>
|
|
||||||
{option}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label className="sb-field">
|
|
||||||
<span>Brand personality</span>
|
|
||||||
<textarea
|
|
||||||
rows={3}
|
|
||||||
value={style.personality}
|
|
||||||
onChange={(event) => onChange({ personality: event.target.value })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="sb-field">
|
|
||||||
<span>Hero imagery direction</span>
|
|
||||||
<textarea
|
|
||||||
rows={3}
|
|
||||||
value={style.heroImagery}
|
|
||||||
onChange={(event) => onChange({ heroImagery: event.target.value })}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { expect, afterEach } from 'vitest';
|
|
||||||
import { cleanup } from '@testing-library/react';
|
|
||||||
import '@testing-library/jest-dom/vitest';
|
|
||||||
|
|
||||||
// Cleanup after each test
|
|
||||||
afterEach(() => {
|
|
||||||
cleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
|
||||||
import { useBuilderStore } from '../builderStore';
|
|
||||||
import type { BuilderFormData } from '../../types/siteBuilder';
|
|
||||||
|
|
||||||
describe('builderStore', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
useBuilderStore.getState().reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('initializes with default form values', () => {
|
|
||||||
const state = useBuilderStore.getState();
|
|
||||||
expect(state.form.siteName).toBe('');
|
|
||||||
expect(state.form.hostingType).toBe('igny8_sites');
|
|
||||||
expect(state.form.objectives).toEqual(['Launch a conversion-focused marketing site']);
|
|
||||||
expect(state.currentStep).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates form fields', () => {
|
|
||||||
const { setField } = useBuilderStore.getState();
|
|
||||||
setField('siteName', 'Test Site');
|
|
||||||
setField('businessType', 'SaaS');
|
|
||||||
|
|
||||||
const state = useBuilderStore.getState();
|
|
||||||
expect(state.form.siteName).toBe('Test Site');
|
|
||||||
expect(state.form.businessType).toBe('SaaS');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('navigates steps correctly', () => {
|
|
||||||
const { nextStep, previousStep, setStep } = useBuilderStore.getState();
|
|
||||||
|
|
||||||
expect(useBuilderStore.getState().currentStep).toBe(0);
|
|
||||||
|
|
||||||
nextStep();
|
|
||||||
expect(useBuilderStore.getState().currentStep).toBe(1);
|
|
||||||
|
|
||||||
nextStep();
|
|
||||||
expect(useBuilderStore.getState().currentStep).toBe(2);
|
|
||||||
|
|
||||||
previousStep();
|
|
||||||
expect(useBuilderStore.getState().currentStep).toBe(1);
|
|
||||||
|
|
||||||
setStep(3);
|
|
||||||
expect(useBuilderStore.getState().currentStep).toBe(3);
|
|
||||||
|
|
||||||
// Should not go beyond max step
|
|
||||||
nextStep();
|
|
||||||
expect(useBuilderStore.getState().currentStep).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('manages objectives list', () => {
|
|
||||||
const { addObjective, removeObjective } = useBuilderStore.getState();
|
|
||||||
|
|
||||||
addObjective('Increase brand awareness');
|
|
||||||
expect(useBuilderStore.getState().form.objectives).toContain('Increase brand awareness');
|
|
||||||
|
|
||||||
addObjective('Drive conversions');
|
|
||||||
expect(useBuilderStore.getState().form.objectives.length).toBe(3); // 1 default + 2 added
|
|
||||||
|
|
||||||
removeObjective(0);
|
|
||||||
expect(useBuilderStore.getState().form.objectives.length).toBe(2);
|
|
||||||
expect(useBuilderStore.getState().form.objectives).not.toContain('Launch a conversion-focused marketing site');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates style preferences', () => {
|
|
||||||
const { updateStyle } = useBuilderStore.getState();
|
|
||||||
|
|
||||||
updateStyle({ palette: 'Dark mode palette' });
|
|
||||||
expect(useBuilderStore.getState().form.style.palette).toBe('Dark mode palette');
|
|
||||||
|
|
||||||
updateStyle({ personality: 'Professional, trustworthy' });
|
|
||||||
expect(useBuilderStore.getState().form.style.personality).toBe('Professional, trustworthy');
|
|
||||||
expect(useBuilderStore.getState().form.style.palette).toBe('Dark mode palette'); // Previous value preserved
|
|
||||||
});
|
|
||||||
|
|
||||||
it('resets to initial state', () => {
|
|
||||||
const { setField, nextStep, addObjective, reset } = useBuilderStore.getState();
|
|
||||||
|
|
||||||
setField('siteName', 'Modified');
|
|
||||||
nextStep();
|
|
||||||
addObjective('Test objective');
|
|
||||||
|
|
||||||
reset();
|
|
||||||
|
|
||||||
const state = useBuilderStore.getState();
|
|
||||||
expect(state.form.siteName).toBe('');
|
|
||||||
expect(state.currentStep).toBe(0);
|
|
||||||
expect(state.form.objectives).toEqual(['Launch a conversion-focused marketing site']);
|
|
||||||
expect(state.isSubmitting).toBe(false);
|
|
||||||
expect(state.error).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
|
||||||
import { useSiteDefinitionStore } from '../siteDefinitionStore';
|
|
||||||
import type { SiteStructure, PageBlueprint } from '../../types/siteBuilder';
|
|
||||||
|
|
||||||
describe('siteDefinitionStore', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
useSiteDefinitionStore.setState({
|
|
||||||
structure: undefined,
|
|
||||||
pages: [],
|
|
||||||
selectedSlug: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('initializes with empty state', () => {
|
|
||||||
const state = useSiteDefinitionStore.getState();
|
|
||||||
expect(state.pages).toEqual([]);
|
|
||||||
expect(state.structure).toBeUndefined();
|
|
||||||
expect(state.selectedSlug).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets structure and auto-selects first page', () => {
|
|
||||||
const mockStructure: SiteStructure = {
|
|
||||||
site: { name: 'Test Site' },
|
|
||||||
pages: [
|
|
||||||
{ slug: 'home', title: 'Home', type: 'home', blocks: [] },
|
|
||||||
{ slug: 'about', title: 'About', type: 'about', blocks: [] },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
useSiteDefinitionStore.getState().setStructure(mockStructure);
|
|
||||||
|
|
||||||
const state = useSiteDefinitionStore.getState();
|
|
||||||
expect(state.structure).toEqual(mockStructure);
|
|
||||||
expect(state.selectedSlug).toBe('home');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets pages and auto-selects first page if none selected', () => {
|
|
||||||
const mockPages: PageBlueprint[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
site_blueprint: 1,
|
|
||||||
slug: 'services',
|
|
||||||
title: 'Services',
|
|
||||||
type: 'services',
|
|
||||||
status: 'ready',
|
|
||||||
order: 0,
|
|
||||||
blocks_json: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
useSiteDefinitionStore.getState().setPages(mockPages);
|
|
||||||
|
|
||||||
const state = useSiteDefinitionStore.getState();
|
|
||||||
expect(state.pages).toEqual(mockPages);
|
|
||||||
expect(state.selectedSlug).toBe('services');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('preserves selected slug when setting pages if already selected', () => {
|
|
||||||
useSiteDefinitionStore.setState({ selectedSlug: 'about' });
|
|
||||||
|
|
||||||
const mockPages: PageBlueprint[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
site_blueprint: 1,
|
|
||||||
slug: 'home',
|
|
||||||
title: 'Home',
|
|
||||||
type: 'home',
|
|
||||||
status: 'ready',
|
|
||||||
order: 0,
|
|
||||||
blocks_json: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
site_blueprint: 1,
|
|
||||||
slug: 'about',
|
|
||||||
title: 'About',
|
|
||||||
type: 'about',
|
|
||||||
status: 'ready',
|
|
||||||
order: 1,
|
|
||||||
blocks_json: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
useSiteDefinitionStore.getState().setPages(mockPages);
|
|
||||||
|
|
||||||
const state = useSiteDefinitionStore.getState();
|
|
||||||
expect(state.selectedSlug).toBe('about');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('selects page by slug', () => {
|
|
||||||
useSiteDefinitionStore.getState().selectPage('contact');
|
|
||||||
expect(useSiteDefinitionStore.getState().selectedSlug).toBe('contact');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
import { create } from 'zustand';
|
|
||||||
import { builderApi } from '../api/builder.api';
|
|
||||||
import type {
|
|
||||||
BuilderFormData,
|
|
||||||
PageBlueprint,
|
|
||||||
SiteBlueprint,
|
|
||||||
StylePreferences,
|
|
||||||
} from '../types/siteBuilder';
|
|
||||||
import { useSiteDefinitionStore } from './siteDefinitionStore';
|
|
||||||
|
|
||||||
const defaultStyle: StylePreferences = {
|
|
||||||
palette: 'Vibrant modern palette with rich accent color',
|
|
||||||
typography: 'Sans-serif display for headings, humanist body font',
|
|
||||||
personality: 'Confident, energetic, optimistic',
|
|
||||||
heroImagery: 'Real people interacting with the product/service',
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultForm: BuilderFormData = {
|
|
||||||
siteId: null,
|
|
||||||
sectorId: null,
|
|
||||||
siteName: '',
|
|
||||||
businessType: '',
|
|
||||||
industry: '',
|
|
||||||
targetAudience: '',
|
|
||||||
hostingType: 'igny8_sites',
|
|
||||||
businessBrief: '',
|
|
||||||
objectives: ['Launch a conversion-focused marketing site'],
|
|
||||||
style: defaultStyle,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface BuilderState {
|
|
||||||
form: BuilderFormData;
|
|
||||||
currentStep: number;
|
|
||||||
isSubmitting: boolean;
|
|
||||||
error?: string;
|
|
||||||
activeBlueprint?: SiteBlueprint;
|
|
||||||
structureTaskId: string | null;
|
|
||||||
pages: PageBlueprint[];
|
|
||||||
selectedPageIds: number[];
|
|
||||||
isGenerating: boolean;
|
|
||||||
generationProgress?: {
|
|
||||||
pagesQueued: number;
|
|
||||||
taskIds: number[];
|
|
||||||
celeryTaskId?: string;
|
|
||||||
};
|
|
||||||
setField: <K extends keyof BuilderFormData>(key: K, value: BuilderFormData[K]) => void;
|
|
||||||
updateStyle: (partial: Partial<StylePreferences>) => void;
|
|
||||||
addObjective: (value: string) => void;
|
|
||||||
removeObjective: (index: number) => void;
|
|
||||||
setStep: (step: number) => void;
|
|
||||||
nextStep: () => void;
|
|
||||||
previousStep: () => void;
|
|
||||||
reset: () => void;
|
|
||||||
submitWizard: () => Promise<void>;
|
|
||||||
setStructureTaskId: (taskId: string | null) => void;
|
|
||||||
refreshPages: (blueprintId: number) => Promise<void>;
|
|
||||||
togglePageSelection: (pageId: number) => void;
|
|
||||||
selectAllPages: () => void;
|
|
||||||
clearPageSelection: () => void;
|
|
||||||
generateAllPages: (blueprintId: number, force?: boolean) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useBuilderStore = create<BuilderState>((set, get) => ({
|
|
||||||
form: defaultForm,
|
|
||||||
currentStep: 0,
|
|
||||||
isSubmitting: false,
|
|
||||||
structureTaskId: null,
|
|
||||||
pages: [],
|
|
||||||
setStructureTaskId: (taskId) => set({ structureTaskId: taskId }),
|
|
||||||
selectedPageIds: [],
|
|
||||||
isGenerating: false,
|
|
||||||
|
|
||||||
setField: (key, value) =>
|
|
||||||
set((state) => ({
|
|
||||||
form: { ...state.form, [key]: value },
|
|
||||||
})),
|
|
||||||
|
|
||||||
updateStyle: (partial) =>
|
|
||||||
set((state) => ({
|
|
||||||
form: { ...state.form, style: { ...state.form.style, ...partial } },
|
|
||||||
})),
|
|
||||||
|
|
||||||
addObjective: (value) =>
|
|
||||||
set((state) => ({
|
|
||||||
form: { ...state.form, objectives: [...state.form.objectives, value] },
|
|
||||||
})),
|
|
||||||
|
|
||||||
removeObjective: (index) =>
|
|
||||||
set((state) => ({
|
|
||||||
form: {
|
|
||||||
...state.form,
|
|
||||||
objectives: state.form.objectives.filter((_, idx) => idx !== index),
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
|
|
||||||
setStep: (step) => set({ currentStep: step }),
|
|
||||||
|
|
||||||
nextStep: () =>
|
|
||||||
set((state) => ({
|
|
||||||
currentStep: Math.min(state.currentStep + 1, 3),
|
|
||||||
})),
|
|
||||||
|
|
||||||
previousStep: () =>
|
|
||||||
set((state) => ({
|
|
||||||
currentStep: Math.max(state.currentStep - 1, 0),
|
|
||||||
})),
|
|
||||||
|
|
||||||
reset: () =>
|
|
||||||
set({
|
|
||||||
form: defaultForm,
|
|
||||||
currentStep: 0,
|
|
||||||
isSubmitting: false,
|
|
||||||
error: undefined,
|
|
||||||
activeBlueprint: undefined,
|
|
||||||
pages: [],
|
|
||||||
}),
|
|
||||||
|
|
||||||
submitWizard: async () => {
|
|
||||||
const { form } = get();
|
|
||||||
if (!form.siteId || !form.sectorId) {
|
|
||||||
set({ error: 'Site and sector are required to generate a blueprint.' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
set({ isSubmitting: true, error: undefined, structureTaskId: null });
|
|
||||||
try {
|
|
||||||
const payload = {
|
|
||||||
name: form.siteName || `Site Blueprint (${form.industry || 'New'})`,
|
|
||||||
description: `${form.businessType} for ${form.targetAudience}`,
|
|
||||||
site_id: form.siteId,
|
|
||||||
sector_id: form.sectorId,
|
|
||||||
hosting_type: form.hostingType,
|
|
||||||
config_json: {
|
|
||||||
business_type: form.businessType,
|
|
||||||
industry: form.industry,
|
|
||||||
target_audience: form.targetAudience,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const blueprint = await builderApi.createBlueprint(payload);
|
|
||||||
set({ activeBlueprint: blueprint });
|
|
||||||
|
|
||||||
const generation = await builderApi.generateStructure(blueprint.id, {
|
|
||||||
business_brief: form.businessBrief,
|
|
||||||
objectives: form.objectives,
|
|
||||||
style: form.style,
|
|
||||||
metadata: { targetAudience: form.targetAudience },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (generation?.task_id) {
|
|
||||||
set({ structureTaskId: generation.task_id });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (generation?.structure) {
|
|
||||||
useSiteDefinitionStore.getState().setStructure(generation.structure);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!generation?.task_id) {
|
|
||||||
await get().refreshPages(blueprint.id);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
set({ error: error instanceof Error ? error.message : 'Unexpected error' });
|
|
||||||
} finally {
|
|
||||||
set({ isSubmitting: false });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
refreshPages: async (blueprintId: number) => {
|
|
||||||
try {
|
|
||||||
const pages = await builderApi.listPages(blueprintId);
|
|
||||||
set({ pages });
|
|
||||||
useSiteDefinitionStore.getState().setPages(pages);
|
|
||||||
} catch (error) {
|
|
||||||
set({ error: error instanceof Error ? error.message : 'Unable to load pages' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
togglePageSelection: (pageId: number) => {
|
|
||||||
set((state) => {
|
|
||||||
const isSelected = state.selectedPageIds.includes(pageId);
|
|
||||||
return {
|
|
||||||
selectedPageIds: isSelected
|
|
||||||
? state.selectedPageIds.filter((id) => id !== pageId)
|
|
||||||
: [...state.selectedPageIds, pageId],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
selectAllPages: () => {
|
|
||||||
set((state) => ({
|
|
||||||
selectedPageIds: state.pages.map((p) => p.id),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
clearPageSelection: () => {
|
|
||||||
set({ selectedPageIds: [] });
|
|
||||||
},
|
|
||||||
|
|
||||||
generateAllPages: async (blueprintId: number, force = false) => {
|
|
||||||
const { selectedPageIds } = get();
|
|
||||||
set({ isGenerating: true, error: undefined, generationProgress: undefined });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await builderApi.generateAllPages(blueprintId, {
|
|
||||||
pageIds: selectedPageIds.length > 0 ? selectedPageIds : undefined,
|
|
||||||
force,
|
|
||||||
});
|
|
||||||
|
|
||||||
set({
|
|
||||||
generationProgress: {
|
|
||||||
pagesQueued: result.pages_queued,
|
|
||||||
taskIds: result.task_ids,
|
|
||||||
celeryTaskId: result.celery_task_id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Refresh pages to update their status
|
|
||||||
await get().refreshPages(blueprintId);
|
|
||||||
} catch (error) {
|
|
||||||
set({ error: error instanceof Error ? error.message : 'Failed to generate pages' });
|
|
||||||
} finally {
|
|
||||||
set({ isGenerating: false });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { create } from 'zustand';
|
|
||||||
import type { PageBlueprint, SiteStructure } from '../types/siteBuilder';
|
|
||||||
|
|
||||||
interface SiteDefinitionState {
|
|
||||||
structure?: SiteStructure;
|
|
||||||
pages: PageBlueprint[];
|
|
||||||
selectedSlug?: string;
|
|
||||||
setStructure: (structure: SiteStructure) => void;
|
|
||||||
setPages: (pages: PageBlueprint[]) => void;
|
|
||||||
selectPage: (slug: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSiteDefinitionStore = create<SiteDefinitionState>((set) => ({
|
|
||||||
pages: [],
|
|
||||||
setStructure: (structure) =>
|
|
||||||
set({
|
|
||||||
structure,
|
|
||||||
selectedSlug: structure.pages?.[0]?.slug,
|
|
||||||
}),
|
|
||||||
setPages: (pages) =>
|
|
||||||
set((state) => ({
|
|
||||||
pages,
|
|
||||||
selectedSlug: state.selectedSlug ?? pages[0]?.slug,
|
|
||||||
})),
|
|
||||||
selectPage: (slug) => set({ selectedSlug: slug }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
export type HostingType = 'igny8_sites' | 'wordpress' | 'shopify' | 'multi';
|
|
||||||
|
|
||||||
export interface StylePreferences {
|
|
||||||
palette: string;
|
|
||||||
typography: string;
|
|
||||||
personality: string;
|
|
||||||
heroImagery: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BuilderFormData {
|
|
||||||
siteId: number | null;
|
|
||||||
sectorId: number | null;
|
|
||||||
siteName: string;
|
|
||||||
businessType: string;
|
|
||||||
industry: string;
|
|
||||||
targetAudience: string;
|
|
||||||
hostingType: HostingType;
|
|
||||||
businessBrief: string;
|
|
||||||
objectives: string[];
|
|
||||||
style: StylePreferences;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SiteBlueprint {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
status: 'draft' | 'generating' | 'ready' | 'deployed';
|
|
||||||
hosting_type: HostingType;
|
|
||||||
config_json: Record<string, unknown>;
|
|
||||||
structure_json: SiteStructure | null;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PageBlueprint {
|
|
||||||
id: number;
|
|
||||||
site_blueprint: number;
|
|
||||||
slug: string;
|
|
||||||
title: string;
|
|
||||||
type: string;
|
|
||||||
status: string;
|
|
||||||
order: number;
|
|
||||||
blocks_json: PageBlock[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PageBlock {
|
|
||||||
type: string;
|
|
||||||
heading?: string;
|
|
||||||
subheading?: string;
|
|
||||||
layout?: string;
|
|
||||||
content?: string[] | Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SiteStructure {
|
|
||||||
site?: {
|
|
||||||
name?: string;
|
|
||||||
primary_navigation?: string[];
|
|
||||||
secondary_navigation?: string[];
|
|
||||||
hero_message?: string;
|
|
||||||
tone?: string;
|
|
||||||
};
|
|
||||||
pages: Array<{
|
|
||||||
slug: string;
|
|
||||||
title: string;
|
|
||||||
type: string;
|
|
||||||
status?: string;
|
|
||||||
objective?: string;
|
|
||||||
primary_cta?: string;
|
|
||||||
blocks?: PageBlock[];
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApiListResponse<T> {
|
|
||||||
count?: number;
|
|
||||||
next?: string | null;
|
|
||||||
previous?: string | null;
|
|
||||||
results?: T[];
|
|
||||||
data?: T[] | T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApiError {
|
|
||||||
message?: string;
|
|
||||||
error?: string;
|
|
||||||
detail?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
||||||
"target": "ES2022",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
||||||
"module": "ESNext",
|
|
||||||
"types": ["vite/client", "vitest/globals"],
|
|
||||||
"skipLibCheck": true,
|
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"@shared/*": ["../frontend/src/components/shared/*"]
|
|
||||||
},
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"erasableSyntaxOnly": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedSideEffectImports": true
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"files": [],
|
|
||||||
"references": [
|
|
||||||
{ "path": "./tsconfig.app.json" },
|
|
||||||
{ "path": "./tsconfig.node.json" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
||||||
"target": "ES2023",
|
|
||||||
"lib": ["ES2023"],
|
|
||||||
"module": "ESNext",
|
|
||||||
"types": ["node"],
|
|
||||||
"skipLibCheck": true,
|
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"noEmit": true,
|
|
||||||
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"erasableSyntaxOnly": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedSideEffectImports": true
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import fs from 'node:fs';
|
|
||||||
import path from 'node:path';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
import { defineConfig } from 'vite';
|
|
||||||
import react from '@vitejs/plugin-react';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
const sharedPathCandidates = [
|
|
||||||
path.resolve(__dirname, '../frontend/src/components/shared'),
|
|
||||||
path.resolve(__dirname, '../../frontend/src/components/shared'),
|
|
||||||
'/frontend/src/components/shared',
|
|
||||||
];
|
|
||||||
const sharedComponentsPath = sharedPathCandidates.find((candidate) => fs.existsSync(candidate)) ?? sharedPathCandidates[0];
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react()],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@shared': sharedComponentsPath,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
test: {
|
|
||||||
environment: 'jsdom',
|
|
||||||
setupFiles: './src/setupTests.ts',
|
|
||||||
globals: true,
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
host: '0.0.0.0',
|
|
||||||
port: 5175,
|
|
||||||
allowedHosts: ['builder.igny8.com'],
|
|
||||||
fs: {
|
|
||||||
allow: [path.resolve(__dirname, '..'), sharedComponentsPath],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user