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