Phase 1 (Foundation): - Project restructure (presenton-main → backend/ + frontend/) - Database schema (8 new models, Alembic config, seed script) - Auth (Azure AD SSO + dev bypass, JWT sessions, AuthMiddleware) - RBAC (access_service, rbac_middleware, admin routers) - Audit logging (fire-and-forget, AuditMiddleware, admin router) - i18n (react-i18next with 5 namespace files) Phase 2 (Admin Panel & Client Management): - Admin panel shell (sidebar layout, role guard, 12 pages) - Redux admin slice with 18 async thunks - User management (role changes, deactivation) - Client management (CRUD, brand config, team management) - Brand config editor (colors, fonts, logos, voice rules) - Master deck upload & parser (PPTX → HTML → React pipeline) - Audit log viewer with filters and CSV/JSON export Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1449 lines
61 KiB
Markdown
1449 lines
61 KiB
Markdown
# OLIVER DeckForge — Implementation Plan
|
|
|
|
> Reference: [concept.md](concept.md) for full project concept and decisions.
|
|
> Base: [presenton-main/](presenton-main/) (fork + restructure)
|
|
|
|
---
|
|
|
|
## Phase 1: Foundation
|
|
|
|
### 1. Project Setup & Restructure
|
|
|
|
Fork Presenton, reorganize file structure, strip Electron shell, configure for web-only deployment.
|
|
|
|
**Backend restructure:**
|
|
- [ ] Copy `presenton-main/servers/fastapi/` → `backend/`
|
|
- [ ] Copy `presenton-main/servers/nextjs/` → `frontend/`
|
|
- [ ] Remove `presenton-main/electron/` (Electron shell — not needed)
|
|
- [ ] Remove Mixpanel tracking references (`MixpanelInitializer.tsx`, `mixpanel.ts`, `mixpanel-browser` dep)
|
|
- [ ] Update all internal import paths after restructure
|
|
- [ ] Create new root `docker-compose.yml` with services:
|
|
- `web` (Next.js frontend, port 3000)
|
|
- `api` (FastAPI backend, port 8000)
|
|
- `worker` (ARQ worker, same image as `api`, different entrypoint)
|
|
- `postgres` (PostgreSQL 16 with TDE-ready config)
|
|
- `redis` (Redis 7 for job queue)
|
|
- `nginx` (reverse proxy, port 80/443)
|
|
- [ ] Create new root `Dockerfile` (multi-stage: Python + Node)
|
|
- [ ] Create `Dockerfile.dev` for development with hot-reload
|
|
- [ ] Update `nginx.conf`: route `/api/*` → FastAPI, `/admin/*` and `/*` → Next.js
|
|
- [ ] Create `.env.example` with all required variables
|
|
- [ ] Create `Makefile` with common commands (`make dev`, `make build`, `make migrate`, `make test`)
|
|
- [ ] Verify `docker compose up` boots all services and health checks pass
|
|
|
|
**Verification:**
|
|
- `docker compose up` starts all 5 services
|
|
- `http://localhost` loads Next.js app
|
|
- `http://localhost/api/docs` loads FastAPI Swagger UI
|
|
|
|
---
|
|
|
|
### 2. Database Schema & Migrations
|
|
|
|
Set up PostgreSQL as primary database, add Alembic for migrations, create all new tables.
|
|
|
|
**Setup:**
|
|
- [ ] Add `alembic>=1.15` to `pyproject.toml`
|
|
- [ ] Run `alembic init migrations` in `backend/`
|
|
- [ ] Configure `alembic.ini` and `env.py` to use async PostgreSQL + SQLModel metadata
|
|
- [ ] Set `DATABASE_URL=postgresql+asyncpg://deckforge:deckforge@postgres:5432/deckforge` in `.env`
|
|
- [ ] Remove SQLite container DB logic (`container.db`, `get_container_session`)
|
|
|
|
**New models** (create in `backend/models/sql/`):
|
|
|
|
- [ ] `user.py` — `UserModel`
|
|
```
|
|
id: UUID (PK)
|
|
azure_oid: str (unique, from Azure AD)
|
|
email: str (unique)
|
|
display_name: str
|
|
role: Enum(super_admin, client_admin, user)
|
|
is_active: bool (default True)
|
|
last_login_at: datetime (nullable)
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
```
|
|
|
|
- [ ] `client.py` — `ClientModel`
|
|
```
|
|
id: UUID (PK)
|
|
name: str
|
|
slug: str (unique, URL-safe)
|
|
logo_path: str (nullable)
|
|
retention_days: int (nullable — null = unlimited)
|
|
review_policy: Enum(self_approve, require_reviewer) (default self_approve)
|
|
is_active: bool (default True)
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
```
|
|
|
|
- [ ] `team.py` — `TeamModel`
|
|
```
|
|
id: UUID (PK)
|
|
name: str
|
|
client_id: UUID (FK → ClientModel, nullable for Oliver Team)
|
|
is_default: bool (default False — True only for Oliver Team)
|
|
created_at: datetime
|
|
```
|
|
|
|
- [ ] `team_membership.py` — `TeamMembershipModel`
|
|
```
|
|
id: UUID (PK)
|
|
user_id: UUID (FK → UserModel)
|
|
team_id: UUID (FK → TeamModel)
|
|
assigned_by: UUID (FK → UserModel, nullable)
|
|
assigned_at: datetime
|
|
UNIQUE(user_id, team_id)
|
|
```
|
|
|
|
- [ ] `brand_config.py` — `BrandConfigModel`
|
|
```
|
|
id: UUID (PK)
|
|
client_id: UUID (FK → ClientModel, unique)
|
|
primary_colors: JSON (list of hex codes)
|
|
secondary_colors: JSON (list of hex codes)
|
|
fonts: JSON ({heading: str, body: str, accent: str})
|
|
logo_paths: JSON (list of file paths)
|
|
voice_rules: text (nullable — brand voice text rules)
|
|
voice_examples: JSON (nullable — [{good: str, bad: str}])
|
|
guideline_doc_path: str (nullable — uploaded brand guide PDF/DOCX)
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
```
|
|
|
|
- [ ] `master_deck.py` — `MasterDeckModel`
|
|
```
|
|
id: UUID (PK)
|
|
client_id: UUID (FK → ClientModel)
|
|
name: str
|
|
description: str (nullable)
|
|
original_file_path: str
|
|
thumbnail_path: str (nullable)
|
|
parsed_config: JSON (nullable — extracted themes, colors, fonts)
|
|
layouts: JSON (nullable — list of {id, name, type, description, json_schema, react_code})
|
|
parse_status: Enum(pending, processing, completed, failed)
|
|
is_active: bool (default True)
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
```
|
|
|
|
- [ ] `audit_log.py` — `AuditLogModel`
|
|
```
|
|
id: UUID (PK)
|
|
user_id: UUID (FK → UserModel, nullable — system actions)
|
|
action: str (e.g., "presentation.create", "master_deck.upload", "user.role_change")
|
|
resource_type: str (e.g., "presentation", "master_deck", "user")
|
|
resource_id: UUID (nullable)
|
|
client_id: UUID (nullable — for client-scoped actions)
|
|
details: JSON (nullable — action-specific metadata)
|
|
ip_address: str (nullable)
|
|
created_at: datetime (indexed)
|
|
```
|
|
|
|
- [ ] `job.py` — `JobModel`
|
|
```
|
|
id: UUID (PK)
|
|
user_id: UUID (FK → UserModel)
|
|
client_id: UUID (FK → ClientModel)
|
|
presentation_id: UUID (FK → PresentationModel, nullable — set after creation)
|
|
job_type: str (e.g., "generate_presentation", "parse_master_deck")
|
|
status: Enum(queued, processing, completed, failed)
|
|
progress: int (0-100)
|
|
progress_message: str (nullable)
|
|
error_message: str (nullable)
|
|
created_at: datetime
|
|
started_at: datetime (nullable)
|
|
completed_at: datetime (nullable)
|
|
```
|
|
|
|
**Modify existing models:**
|
|
|
|
- [ ] `presentation.py` — extend `PresentationModel`:
|
|
```
|
|
+ owner_id: UUID (FK → UserModel)
|
|
+ client_id: UUID (FK → ClientModel)
|
|
+ master_deck_id: UUID (FK → MasterDeckModel, nullable)
|
|
+ status: Enum(draft, in_review, approved) (default draft)
|
|
+ review_comment: text (nullable)
|
|
+ source_type: Enum(brief, url, manual)
|
|
+ is_saved: bool (default False — "Save to library" flag)
|
|
+ deleted_at: datetime (nullable — soft delete for GDPR)
|
|
```
|
|
|
|
- [ ] `slide.py` — extend `SlideModel`:
|
|
```
|
|
+ deleted_at: datetime (nullable — soft delete)
|
|
```
|
|
|
|
**Migration:**
|
|
- [ ] Generate initial Alembic migration: `alembic revision --autogenerate -m "initial_schema"`
|
|
- [ ] Create seed script: auto-create "Oliver Team" (is_default=True) on first run
|
|
- [ ] Test: `alembic upgrade head` creates all tables in PostgreSQL
|
|
|
|
**Verification:**
|
|
- `alembic upgrade head` runs without errors
|
|
- All tables visible in PostgreSQL
|
|
- Seed script creates Oliver Team
|
|
- Existing Presenton tables (PresentationModel, SlideModel) coexist with new tables
|
|
|
|
---
|
|
|
|
### 3. Auth Module (Azure AD SSO)
|
|
|
|
Implement Microsoft Azure AD OAuth 2.0 login, JWT session management, and auth middleware.
|
|
|
|
**Dependencies:** `pip install msal>=1.31 python-jose[cryptography]>=3.3 httpx>=0.28`
|
|
|
|
**Backend:**
|
|
|
|
- [ ] Create `backend/services/auth_service.py`:
|
|
- `AuthService` class with:
|
|
- `get_authorization_url()` → returns Azure AD OAuth URL with redirect_uri
|
|
- `exchange_code_for_token(code: str)` → exchanges auth code for access/id token via MSAL
|
|
- `validate_token(token: str)` → validates JWT, returns claims (oid, email, name)
|
|
- `get_or_create_user(claims: dict)` → find by `azure_oid` or create new `UserModel` (role=user by default)
|
|
- `create_session_jwt(user: UserModel)` → sign app-level JWT with user_id, role, expiry (24h)
|
|
- `refresh_session(token: str)` → refresh if near expiry
|
|
- Auto-add new users to Oliver Team (default team)
|
|
|
|
- [ ] Create `backend/api/v1/auth/router.py`:
|
|
- `GET /api/v1/auth/login` → redirect to Azure AD authorize URL
|
|
- `GET /api/v1/auth/callback` → exchange code, create/update user, set JWT cookie, redirect to dashboard
|
|
- `GET /api/v1/auth/me` → return current user (id, email, name, role, teams, clients)
|
|
- `POST /api/v1/auth/logout` → clear JWT cookie
|
|
- Register router in `api/v1/ppt/router.py` (or new `api/v1/router.py`)
|
|
|
|
- [ ] Create `backend/api/middlewares/auth_middleware.py`:
|
|
- `AuthMiddleware`: extract JWT from cookie/header → validate → attach `request.state.user` (UserModel)
|
|
- Skip auth for: `/api/v1/auth/*`, `/api/docs`, `/api/openapi.json`, `/api/health`
|
|
- Return 401 for invalid/missing token
|
|
|
|
- [ ] Create `backend/utils/auth_dependencies.py`:
|
|
- `get_current_user(request) → UserModel` (FastAPI Depends)
|
|
- `require_role(role: str)` → dependency that checks `request.state.user.role`
|
|
- `require_super_admin` → shortcut dependency
|
|
- `require_client_admin` → shortcut dependency
|
|
|
|
- [ ] Add env vars to `.env.example`:
|
|
```
|
|
AZURE_AD_TENANT_ID=
|
|
AZURE_AD_CLIENT_ID=
|
|
AZURE_AD_CLIENT_SECRET=
|
|
AZURE_AD_REDIRECT_URI=http://localhost/api/v1/auth/callback
|
|
JWT_SECRET_KEY=<random-256-bit-key>
|
|
```
|
|
|
|
**Frontend:**
|
|
|
|
- [ ] Create `frontend/store/slices/authSlice.ts`:
|
|
- State: `{user: User | null, isLoading: bool, isAuthenticated: bool}`
|
|
- Actions: `fetchCurrentUser()` (calls `/api/v1/auth/me`), `logout()`
|
|
- Types: `User {id, email, displayName, role, teams: Team[], clients: Client[]}`
|
|
|
|
- [ ] Create `frontend/components/AuthGuard.tsx`:
|
|
- Wraps app; redirects to `/api/v1/auth/login` if not authenticated
|
|
- Shows loading state while checking auth
|
|
|
|
- [ ] Create `frontend/app/login/page.tsx`:
|
|
- Simple login page with "Sign in with Microsoft" button
|
|
- Button redirects to `/api/v1/auth/login`
|
|
|
|
- [ ] Modify `frontend/app/layout.tsx`:
|
|
- Wrap app with `AuthGuard` and Redux Provider
|
|
- Call `fetchCurrentUser()` on mount
|
|
|
|
**Verification:**
|
|
- Click "Sign in with Microsoft" → Azure AD login → redirect back → user created in DB
|
|
- `/api/v1/auth/me` returns user with role and teams
|
|
- Unauthenticated requests to `/api/v1/ppt/*` return 401
|
|
- New user auto-added to Oliver Team
|
|
|
|
---
|
|
|
|
### 4. RBAC & Team Management
|
|
|
|
Role-based access control middleware and team/client access enforcement.
|
|
|
|
**Backend:**
|
|
|
|
- [ ] Create `backend/api/middlewares/rbac_middleware.py`:
|
|
- `check_client_access(user: UserModel, client_id: UUID)` → verify user belongs to a team for this client (or is super_admin)
|
|
- `check_team_admin(user: UserModel, client_id: UUID)` → verify user is client_admin for this client or super_admin
|
|
- Both raise `HTTPException(403)` on failure
|
|
|
|
- [ ] Create `backend/api/v1/admin/users_router.py` (Super Admin only):
|
|
- `GET /api/v1/admin/users` → list all users with their roles and team memberships
|
|
- `GET /api/v1/admin/users/{id}` → user detail
|
|
- `PUT /api/v1/admin/users/{id}/role` → change role (super_admin, client_admin, user)
|
|
- `DELETE /api/v1/admin/users/{id}` → deactivate user (soft: is_active=False)
|
|
- All endpoints protected with `require_super_admin` dependency
|
|
|
|
- [ ] Create `backend/api/v1/admin/teams_router.py`:
|
|
- `POST /api/v1/admin/teams` → create team for a client (Client Admin for that client, or Super Admin)
|
|
- `GET /api/v1/admin/teams` → list teams (filtered by user's accessible clients)
|
|
- `GET /api/v1/admin/teams/{id}` → team detail with members
|
|
- `POST /api/v1/admin/teams/{id}/members` → add user to team (body: `{user_id}`)
|
|
- `DELETE /api/v1/admin/teams/{id}/members/{user_id}` → remove user from team
|
|
- Enforce: transfer ownership of user's presentations in this client before removal
|
|
- `DELETE /api/v1/admin/teams/{id}` → delete team (Super Admin only, cannot delete Oliver Team)
|
|
|
|
- [ ] Create `backend/api/v1/admin/clients_router.py`:
|
|
- `POST /api/v1/admin/clients` → create client (Super Admin only)
|
|
- `GET /api/v1/admin/clients` → list clients (filtered: Super Admin sees all, others see assigned)
|
|
- `GET /api/v1/admin/clients/{id}` → client detail
|
|
- `PUT /api/v1/admin/clients/{id}` → update client (Client Admin or Super Admin)
|
|
- `DELETE /api/v1/admin/clients/{id}` → deactivate client (Super Admin only, soft delete)
|
|
- When creating client: auto-create a team with same name
|
|
|
|
- [ ] Create `backend/services/access_service.py`:
|
|
- `get_accessible_client_ids(user: UserModel) → list[UUID]` — returns client IDs user can access via team membership
|
|
- `get_accessible_clients(user: UserModel) → list[ClientModel]` — returns full client objects
|
|
- Used throughout the app for filtering queries
|
|
|
|
- [ ] Modify all existing presentation endpoints (`backend/api/v1/ppt/endpoints/presentation.py`):
|
|
- Add `user: UserModel = Depends(get_current_user)` to all handlers
|
|
- Filter `get_all_presentations` by user's accessible clients
|
|
- Check `check_client_access(user, client_id)` on create/generate
|
|
- Check ownership or client access on get/update/delete
|
|
|
|
**Verification:**
|
|
- Super Admin can see all clients, users, teams
|
|
- Client Admin can manage only assigned clients and their teams
|
|
- User can only see presentations for their assigned clients
|
|
- Removing user from team requires ownership transfer of their decks
|
|
- Oliver Team cannot be deleted
|
|
- Users auto-join Oliver Team on creation
|
|
|
|
---
|
|
|
|
### 5. Audit Logging
|
|
|
|
Full audit trail for GDPR compliance with CSV/JSON export.
|
|
|
|
**Backend:**
|
|
|
|
- [ ] Create `backend/services/audit_service.py`:
|
|
- `AuditService` class:
|
|
- `log(user_id, action, resource_type, resource_id, client_id, details, ip_address)` → insert `AuditLogModel`
|
|
- Use background task (not blocking request) via `asyncio.create_task()`
|
|
- `query(filters: AuditLogFilter) → list[AuditLogModel]` — paginated, filterable by date range, user, action, client
|
|
- `export_csv(filters) → StreamingResponse` — CSV export
|
|
- `export_json(filters) → StreamingResponse` — JSON export
|
|
|
|
- [ ] Create `backend/api/middlewares/audit_middleware.py`:
|
|
- Auto-log middleware for mutating requests (POST, PUT, DELETE)
|
|
- Captures: user_id (from auth), action (from endpoint name), IP (from request)
|
|
- Non-blocking: fires and forgets the log insert
|
|
|
|
- [ ] Create `backend/api/v1/admin/audit_router.py`:
|
|
- `GET /api/v1/admin/audit-log` → paginated audit log (Super Admin: all, Client Admin: own clients)
|
|
- `GET /api/v1/admin/audit-log/export?format=csv|json` → download filtered audit log
|
|
- Filters: `date_from`, `date_to`, `user_id`, `action`, `client_id`, `resource_type`
|
|
|
|
- [ ] Add audit calls to critical actions:
|
|
- Auth: login, logout, role change
|
|
- Clients: create, update, delete
|
|
- Teams: create, add/remove member
|
|
- Master decks: upload, parse, delete
|
|
- Presentations: create, generate, export, status change, delete
|
|
- Brand config: create, update
|
|
|
|
**Verification:**
|
|
- Generate a presentation → audit log shows entries for each step
|
|
- Export audit log as CSV → opens in Excel with correct columns
|
|
- Client Admin can only see logs for their clients
|
|
- Super Admin sees all logs
|
|
|
|
---
|
|
|
|
### 6. i18n Setup
|
|
|
|
Configure react-i18next for English MVP with structure ready for future languages.
|
|
|
|
**Frontend:**
|
|
|
|
- [ ] Install: `npm install react-i18next i18next i18next-http-backend i18next-browser-languagedetector`
|
|
- [ ] Create `frontend/i18n/` directory:
|
|
- `i18n.ts` — i18next initialization config
|
|
- `locales/en/common.json` — common strings (buttons, labels, errors)
|
|
- `locales/en/auth.json` — auth-related strings
|
|
- `locales/en/admin.json` — admin panel strings
|
|
- `locales/en/wizard.json` — wizard step strings
|
|
- `locales/en/editor.json` — slide editor strings
|
|
- [ ] Create `frontend/i18n/I18nProvider.tsx` — wraps app with I18nextProvider
|
|
- [ ] Update `frontend/app/layout.tsx` — add I18nProvider
|
|
- [ ] Replace hardcoded strings in existing Presenton components with `t()` calls (incremental — do this as we touch each component)
|
|
|
|
**Verification:**
|
|
- App loads in English
|
|
- All new UI strings use `t('key')` pattern
|
|
- Adding a new locale JSON file makes it available
|
|
|
|
---
|
|
|
|
## Phase 2: Admin Panel & Client Management
|
|
|
|
### 7. Admin Panel Frontend Shell
|
|
|
|
Create the admin section layout, navigation, and page structure.
|
|
|
|
**Frontend:**
|
|
|
|
- [ ] Create `frontend/app/admin/layout.tsx`:
|
|
- Sidebar navigation with sections:
|
|
- Users & Roles (Super Admin only)
|
|
- Clients (filtered by role)
|
|
- Teams (filtered by role)
|
|
- Master Decks (filtered by role)
|
|
- Brand Config (filtered by role)
|
|
- Audit Log (Super Admin: full, Client Admin: own clients)
|
|
- Analytics (Super Admin: full, Client Admin: own clients)
|
|
- System Settings (Super Admin only: LLM config, image provider config)
|
|
- Role-based sidebar item visibility
|
|
- Breadcrumb navigation
|
|
- Responsive layout (desktop only, 1280px+)
|
|
|
|
- [ ] Create admin pages (skeleton — data integration in subsequent steps):
|
|
- `frontend/app/admin/page.tsx` → dashboard/overview redirect
|
|
- `frontend/app/admin/users/page.tsx` → user list + role management
|
|
- `frontend/app/admin/users/[id]/page.tsx` → user detail + team assignments
|
|
- `frontend/app/admin/clients/page.tsx` → client list
|
|
- `frontend/app/admin/clients/[id]/page.tsx` → client detail (tabs: info, teams, master decks, brand)
|
|
- `frontend/app/admin/clients/[id]/teams/page.tsx` → team management for client
|
|
- `frontend/app/admin/clients/[id]/master-decks/page.tsx` → master deck list for client
|
|
- `frontend/app/admin/clients/[id]/brand/page.tsx` → brand config editor
|
|
- `frontend/app/admin/audit/page.tsx` → audit log viewer with filters + export
|
|
- `frontend/app/admin/analytics/page.tsx` → analytics dashboard
|
|
- `frontend/app/admin/settings/page.tsx` → system settings (LLM, image providers)
|
|
|
|
- [ ] Create `frontend/store/slices/adminSlice.ts`:
|
|
- State for users list, clients list, teams list
|
|
- CRUD async thunks calling admin API endpoints
|
|
|
|
- [ ] Create admin-specific components:
|
|
- `frontend/app/admin/components/AdminSidebar.tsx`
|
|
- `frontend/app/admin/components/UserTable.tsx`
|
|
- `frontend/app/admin/components/ClientCard.tsx`
|
|
- `frontend/app/admin/components/RoleBadge.tsx`
|
|
- `frontend/app/admin/components/AuditLogTable.tsx`
|
|
- `frontend/app/admin/components/DataExportButton.tsx` (CSV/JSON)
|
|
|
|
**Verification:**
|
|
- `/admin` accessible only to Super Admin and Client Admin roles
|
|
- User role sees no admin link in navigation
|
|
- Sidebar items filtered by role
|
|
- All pages render skeleton/empty states
|
|
|
|
---
|
|
|
|
### 8. Client & Brand Management
|
|
|
|
Full CRUD for clients, brand configuration, and frontend integration.
|
|
|
|
**Backend:**
|
|
- [ ] Implement `clients_router.py` endpoints with full validation (from step 4)
|
|
- [ ] Create `backend/api/v1/admin/brand_config_router.py`:
|
|
- `GET /api/v1/admin/clients/{client_id}/brand` → get brand config
|
|
- `PUT /api/v1/admin/clients/{client_id}/brand` → update brand config (colors, fonts, voice rules, voice examples)
|
|
- `POST /api/v1/admin/clients/{client_id}/brand/logo` → upload logo file
|
|
- `POST /api/v1/admin/clients/{client_id}/brand/guideline` → upload brand guide document (PDF/DOCX)
|
|
- `DELETE /api/v1/admin/clients/{client_id}/brand/logo/{index}` → remove logo
|
|
- All protected with `check_team_admin(user, client_id)`
|
|
|
|
**Frontend:**
|
|
- [ ] Wire `frontend/app/admin/clients/page.tsx`:
|
|
- Client list as cards/table with name, logo, team count, deck count, status
|
|
- "New Client" button (Super Admin only)
|
|
- Client create/edit modal with name, slug, retention policy, review policy
|
|
|
|
- [ ] Wire `frontend/app/admin/clients/[id]/page.tsx`:
|
|
- Tabs: Overview | Teams | Master Decks | Brand Config
|
|
- Overview: client info, quick stats
|
|
|
|
- [ ] Wire `frontend/app/admin/clients/[id]/brand/page.tsx`:
|
|
- Color pickers for primary/secondary palettes
|
|
- Font selectors (heading, body, accent)
|
|
- Logo upload area (drag & drop, multiple logos)
|
|
- Voice rules textarea
|
|
- Voice examples table: good example ↔ bad example pairs
|
|
- Brand guideline document upload (PDF/DOCX)
|
|
- Preview panel showing how brand config looks applied to a sample slide
|
|
|
|
**Verification:**
|
|
- Create client "Nike" → team auto-created → appears in client list
|
|
- Upload Nike logo → appears in brand config
|
|
- Set brand colors/fonts → saved to DB
|
|
- Upload brand guideline PDF → file stored, path in DB
|
|
- Client Admin for Nike can edit Nike brand, cannot see Adidas
|
|
|
|
---
|
|
|
|
### 9. Master Deck Upload & Parser
|
|
|
|
Upload client PPTX master decks, auto-detect layout types, admin review.
|
|
|
|
**Backend:**
|
|
|
|
- [ ] Create `backend/api/v1/admin/master_decks_router.py`:
|
|
- `POST /api/v1/admin/clients/{client_id}/master-decks` → upload PPTX file
|
|
- Save file to `data/clients/{client_id}/master_decks/{deck_id}/original.pptx`
|
|
- Create `MasterDeckModel` with `parse_status=pending`
|
|
- Enqueue parse job to Redis
|
|
- Return deck_id + status
|
|
- `GET /api/v1/admin/clients/{client_id}/master-decks` → list master decks
|
|
- `GET /api/v1/admin/master-decks/{id}` → deck detail with parsed layouts
|
|
- `PUT /api/v1/admin/master-decks/{id}` → update name, description, active status
|
|
- `PUT /api/v1/admin/master-decks/{id}/layouts/{layout_index}` → edit parsed layout (admin review: rename, recategorize type, edit react code)
|
|
- `DELETE /api/v1/admin/master-decks/{id}` → soft delete
|
|
|
|
- [ ] Create `backend/services/master_deck_parser_service.py`:
|
|
- `MasterDeckParserService` class:
|
|
- `parse(deck_id: UUID)` → main parse pipeline:
|
|
1. Open PPTX with `python-pptx`
|
|
2. Enumerate `prs.slide_masters[0].slide_layouts` → extract each layout
|
|
3. For each layout:
|
|
- Extract placeholder types and positions
|
|
- Render screenshot via Puppeteer/LibreOffice (reuse existing `pptx_slides` logic)
|
|
- Extract OXML
|
|
4. Extract theme: `a:clrScheme` → color palette, `a:fontScheme` → font config
|
|
5. Extract logos/images from master slide backgrounds
|
|
6. For each layout screenshot + OXML:
|
|
- Call existing `slide_to_html` pipeline (vision LLM converts screenshot + OXML → HTML)
|
|
- Call existing `html_to_react` pipeline (LLM converts HTML → React + Zod schema)
|
|
- Auto-classify layout type (title, content, chart, comparison, quote, table, metrics, timeline, image) based on placeholder analysis + LLM classification
|
|
7. Store results in `MasterDeckModel.parsed_config` and `layouts` JSON
|
|
8. Update `parse_status=completed`
|
|
- `get_layout_for_content_type(deck_id, content_type) → layout` — find best matching layout
|
|
|
|
- Reuse from Presenton:
|
|
- `backend/api/v1/ppt/endpoints/pptx_slides.py` — PPTX parsing logic
|
|
- `backend/api/v1/ppt/endpoints/slide_to_html.py` — vision LLM slide-to-HTML
|
|
- `backend/api/v1/ppt/endpoints/html_to_react.py` — HTML-to-React conversion
|
|
|
|
**Frontend:**
|
|
|
|
- [ ] Wire `frontend/app/admin/clients/[id]/master-decks/page.tsx`:
|
|
- Upload button (drag & drop PPTX)
|
|
- List of uploaded master decks with parse status badge (pending/processing/completed/failed)
|
|
- Click deck → expand to show parsed layouts as thumbnail grid
|
|
- Each layout card shows: thumbnail, auto-detected type, name
|
|
- Click layout → edit modal: rename, change type category, edit/regenerate React code
|
|
- "Reparse" button to re-run parser
|
|
|
|
**Verification:**
|
|
- Upload a client PPTX → parse job queued → status changes to processing → completed
|
|
- Parsed layouts visible as thumbnails with auto-detected types
|
|
- Admin can rename layouts and recategorize types
|
|
- Bad PPTX (no slide masters) → system still extracts patterns from content slides
|
|
- Layout React code renders correctly in preview
|
|
|
|
---
|
|
|
|
## Phase 3: Content Pipeline
|
|
|
|
### 10. File Upload & Document Parsing
|
|
|
|
Enhance document upload for briefs + attachments with parsing.
|
|
|
|
**Backend:**
|
|
|
|
- [ ] Modify `backend/api/v1/ppt/endpoints/files.py`:
|
|
- Extend upload endpoint to accept multiple files
|
|
- Classify uploads: brief (DOCX primary), attachments (Excel/CSV/images/PDF), URL reference
|
|
- Store files at `data/presentations/{presentation_id}/uploads/`
|
|
- Return parsed file info for each uploaded file
|
|
|
|
- [ ] Create `backend/services/attachment_parser_service.py`:
|
|
- `parse_excel(file_path) → list[TableData]` — extract sheets → structured table data (via `openpyxl`)
|
|
- `parse_csv(file_path) → TableData` — parse CSV into structured table data
|
|
- `extract_images(file_path) → list[ImageInfo]` — extract images from any doc format
|
|
- `parse_pdf_text(file_path) → str` — extract text from PDF (via `pdfplumber`, already a dep)
|
|
- Output model: `TableData {headers: list[str], rows: list[list[Any]], title: str | None}`
|
|
|
|
- [ ] Add deps: `pip install openpyxl>=3.1`
|
|
|
|
- [ ] Modify `backend/services/documents_loader.py`:
|
|
- Add `load_url(url: str) → str` (secondary input):
|
|
- Fetch URL via `aiohttp` (already a dep)
|
|
- Extract article via `trafilatura` or `readability-lxml`
|
|
- Return markdown
|
|
- Add deps: `pip install trafilatura>=2.0`
|
|
|
|
**Verification:**
|
|
- Upload DOCX brief → parsed to markdown, text extracted
|
|
- Upload Excel → sheets extracted as structured table data
|
|
- Upload CSV → rows/columns extracted
|
|
- Upload images (PNG/JPG) → stored, metadata extracted
|
|
- Upload PDF → text extracted
|
|
- Paste URL → article content extracted as markdown
|
|
|
|
---
|
|
|
|
### 11. Content Intelligence Service
|
|
|
|
NLP-powered classification and extraction of content blocks.
|
|
|
|
**Backend:**
|
|
|
|
- [ ] Create `backend/models/content_models.py`:
|
|
```python
|
|
class ContentBlockType(str, Enum):
|
|
narrative = "narrative"
|
|
quote = "quote"
|
|
metric = "metric"
|
|
table = "table"
|
|
timeline = "timeline"
|
|
comparison = "comparison"
|
|
list_items = "list_items"
|
|
image_reference = "image_reference"
|
|
call_to_action = "call_to_action"
|
|
|
|
class ContentBlock(BaseModel):
|
|
type: ContentBlockType
|
|
raw_text: str
|
|
extracted_data: dict | None # e.g., {value: "2M", label: "Impressions"} for metrics
|
|
source_section: str | None # heading from brief
|
|
priority: int # 1-10
|
|
|
|
class ClassifiedContent(BaseModel):
|
|
title: str | None
|
|
blocks: list[ContentBlock]
|
|
tables: list[TableData]
|
|
images: list[ImageInfo]
|
|
summary: str # brief overall summary
|
|
```
|
|
|
|
- [ ] Create `backend/services/content_intelligence_service.py`:
|
|
- `ContentIntelligenceService` class:
|
|
- `classify(markdown: str, attachments: list) → ClassifiedContent`:
|
|
1. Split markdown into chunks using `ScoreBasedChunker` (existing)
|
|
2. For each chunk, classify content type:
|
|
- Rule-based pass first:
|
|
- Regex for numbers/percentages/currency → `metric`
|
|
- Regex for quoted text with attribution → `quote`
|
|
- Markdown table patterns → `table`
|
|
- Year sequences or date patterns → `timeline`
|
|
- "vs", "compared to", "versus" → `comparison`
|
|
- Bullet lists → `list_items`
|
|
- Image references (URLs, "see figure") → `image_reference`
|
|
- LLM-based classification for ambiguous blocks (batch multiple chunks in one call)
|
|
3. Merge attachment data: Excel tables → `table` blocks, images → `image_reference` blocks
|
|
4. Extract numeric data from metric blocks: `{value, label, unit, context}`
|
|
5. Rank blocks by priority (title > metrics > quotes > narrative)
|
|
6. Return `ClassifiedContent`
|
|
|
|
- `ask_followup_questions(content: ClassifiedContent) → list[str] | None`:
|
|
- If total content is too short (< 200 words, < 3 blocks): generate clarifying questions
|
|
- LLM call: "Given this brief content, what additional information would be needed for a presentation?"
|
|
- Return list of questions or None if content is sufficient
|
|
|
|
**Verification:**
|
|
- Brief with "Revenue grew 45% to $2.3M" → classified as `metric` with `{value: "45%", label: "Revenue growth"}`
|
|
- Table in markdown → classified as `table` with extracted data
|
|
- Quoted text with speaker → classified as `quote`
|
|
- Short 2-sentence brief → returns follow-up questions
|
|
- Excel attachment → tables merged into content blocks
|
|
|
|
---
|
|
|
|
### 12. Slide Mapping Engine
|
|
|
|
Rules engine that maps classified content to master deck layout types.
|
|
|
|
**Backend:**
|
|
|
|
- [ ] Create `backend/services/slide_mapping_engine.py`:
|
|
- `SlideMappingEngine` class:
|
|
- `map(classified_content: ClassifiedContent, master_deck: MasterDeckModel, n_slides: int, instructions: str | None) → list[SlideMapping]`:
|
|
1. Determine required slide types based on content:
|
|
- Always: Title slide (1st)
|
|
- If >5 content blocks: Agenda/TOC slide (2nd)
|
|
- Per section heading in brief: Section divider slide
|
|
- Per content block: map type to layout:
|
|
- `metric` → metrics/KPI layout
|
|
- `quote` → quote layout
|
|
- `table` → table layout or chart layout (user will choose later)
|
|
- `comparison` → dual-column/comparison layout
|
|
- `timeline` → timeline layout
|
|
- `list_items` → bullet list layout
|
|
- `narrative` → content/description layout
|
|
- `image_reference` → image-focused layout
|
|
2. Match content types to available layouts in master deck (by layout `type` field)
|
|
3. If no matching layout: fall back to generic content layout
|
|
4. Respect `n_slides` constraint: merge/drop low-priority blocks if too many, split high-content blocks if too few
|
|
5. Use LLM for ambiguous mappings (existing `generate_presentation_structure` pattern)
|
|
6. Return ordered list of `SlideMapping`:
|
|
```python
|
|
class SlideMapping(BaseModel):
|
|
content_block_index: int | None
|
|
layout_id: str
|
|
layout_name: str
|
|
slide_type: str
|
|
content_summary: str
|
|
attachment_ids: list[str] # mapped attachments
|
|
```
|
|
|
|
**Verification:**
|
|
- Brief with 3 metrics, 1 quote, 2 narrative sections, 1 table → correct layout assignments
|
|
- Requesting 8 slides from 15 content blocks → low-priority blocks merged/dropped
|
|
- Missing layout type in master deck → falls back to generic layout
|
|
- Attachments (Excel) assigned to correct table/chart slides
|
|
|
|
---
|
|
|
|
### 13. Chart Data Extraction & Native PPTX Charts
|
|
|
|
Extract numeric data → chart-ready structures. Render as native editable PPTX charts.
|
|
|
|
**Backend:**
|
|
|
|
- [ ] Create `backend/services/chart_data_extractor.py`:
|
|
- `ChartDataExtractor` class:
|
|
- `extract(content_block: ContentBlock, table_data: TableData | None) → ChartData | None`:
|
|
1. If `table_data` provided: convert directly to `ChartData`
|
|
2. If `content_block.type == metric`: try to extract numeric series from text
|
|
3. Use LLM for complex extraction: "Convert this data to chart format"
|
|
4. Recommend chart type based on data shape:
|
|
- Single category with values → bar chart
|
|
- Time series → line chart
|
|
- Parts of whole (percentages summing to ~100%) → pie/donut chart
|
|
- Two series comparison → grouped bar
|
|
- Project phases with dates → Gantt (shape-based)
|
|
- Sequential increases/decreases → waterfall (shape-based)
|
|
5. Return `ChartData`:
|
|
```python
|
|
class ChartData(BaseModel):
|
|
chart_type: Enum(bar, column, line, pie, doughnut, area, scatter, bubble, gantt, waterfall)
|
|
title: str
|
|
categories: list[str] # X-axis labels
|
|
series: list[ChartSeries] # {name, values: list[float]}
|
|
unit: str | None # e.g., "$", "%", "units"
|
|
```
|
|
|
|
- [ ] Create `backend/services/native_chart_service.py`:
|
|
- `NativeChartService` class (extends `PptxPresentationCreator`):
|
|
- `add_native_chart(slide, chart_data: ChartData, position, size)`:
|
|
- For bar/column/line/pie/doughnut/area/scatter/bubble:
|
|
- Use `python-pptx` chart API directly: `slide.shapes.add_chart()`
|
|
- Set chart data from `ChartData.series`
|
|
- Apply brand colors from client config
|
|
- Set fonts from brand config
|
|
- For Gantt:
|
|
- Build from shapes: one `add_shape()` per task bar (rectangle)
|
|
- Add text labels, date markers, connectors
|
|
- Apply brand colors
|
|
- For Waterfall:
|
|
- Build from shapes: stacked rectangles with invisible bases
|
|
- Add value labels, category labels, connector lines
|
|
- Color-code: green for increases, red for decreases, blue for totals
|
|
|
|
- [ ] Modify `backend/services/pptx_presentation_creator.py`:
|
|
- Import and use `NativeChartService` for chart shapes
|
|
- Replace any image-based chart rendering with native chart calls
|
|
|
|
**Frontend:**
|
|
|
|
- [ ] Create `frontend/app/(presentation-generator)/components/ChartDataEditor.tsx`:
|
|
- Spreadsheet-like grid (using simple HTML table with contentEditable cells, or a lightweight lib)
|
|
- Columns: categories + series names
|
|
- Rows: data values
|
|
- Add/remove rows and series
|
|
- Chart type selector dropdown
|
|
- Live Recharts preview next to the data editor
|
|
- "Apply" button saves changes to Redux store and triggers slide re-render
|
|
|
|
**Verification:**
|
|
- Table with sales data → extracted as bar chart → native editable chart in PPTX
|
|
- Open PPTX in PowerPoint → right-click chart → "Edit Data" works
|
|
- Gantt chart renders as editable shapes with task bars
|
|
- Waterfall chart shows increases (green) and decreases (red) as shapes
|
|
- Chart data editor: change a value → preview updates live → export reflects change
|
|
- Brand colors applied to all chart elements
|
|
|
|
---
|
|
|
|
## Phase 4: Generation Pipeline
|
|
|
|
### 14. Brand Enforcement Service
|
|
|
|
Apply client brand rules to generated content and PPTX output.
|
|
|
|
**Backend:**
|
|
|
|
- [ ] Create `backend/services/brand_enforcement_service.py`:
|
|
- `BrandEnforcementService` class:
|
|
- `get_brand_context_for_llm(client_id: UUID) → str`:
|
|
- Load `BrandConfigModel` for client
|
|
- Build a prompt snippet with voice rules, examples, tone guidelines
|
|
- Used to inject into LLM system prompts during content generation
|
|
- `enforce_on_pptx_model(model: PptxPresentationModel, brand: BrandConfigModel) → PptxPresentationModel`:
|
|
- Walk all slides → all shapes → replace:
|
|
- Font families → brand fonts (heading font for titles, body font for content)
|
|
- Colors → brand palette (map template colors to brand primary/secondary)
|
|
- Add logo shape to designated position on each slide (configurable: top-left, bottom-right, etc.)
|
|
- Contrast check: ensure text color has sufficient contrast against background
|
|
- `enforce_on_native_charts(slide, brand: BrandConfigModel)`:
|
|
- Set chart colors to brand palette
|
|
- Set chart fonts to brand fonts
|
|
|
|
**Verification:**
|
|
- Generate deck for Nike → all fonts are Nike brand fonts
|
|
- All chart bars use Nike brand colors
|
|
- Nike logo appears on every slide in the configured position
|
|
- Text contrast passes WCAG AA check
|
|
|
|
---
|
|
|
|
### 15. Outline Generation (Modified)
|
|
|
|
Enhance existing outline generation with brand context and content intelligence.
|
|
|
|
**Backend:**
|
|
|
|
- [ ] Modify `backend/utils/llm_calls/generate_presentation_outlines.py`:
|
|
- Extend `get_system_prompt()` to include:
|
|
- Brand voice guidelines (from `BrandEnforcementService.get_brand_context_for_llm()`)
|
|
- Available layout types from master deck (names + descriptions)
|
|
- Content classification summary (types and counts of content blocks)
|
|
- **Critical instruction**: "You are restructuring and condensing existing content from a brief. Do NOT invent facts, statistics, or claims. Every data point must originate from the source material."
|
|
- Extend user prompt to include:
|
|
- User's custom instructions (free-text from Step 2)
|
|
- Classified content blocks with their types
|
|
- Attachment summaries (what Excel/CSV data is available)
|
|
|
|
- [ ] Modify `backend/utils/llm_calls/generate_slide_content.py`:
|
|
- Same brand context injection
|
|
- Same "strictly extract, don't invent" instruction
|
|
- For chart-type slides: include chart data schema so LLM populates data correctly
|
|
- For slides with mapped attachments: include attachment data as context
|
|
|
|
- [ ] Implement LLM auto-fallback in `backend/services/llm_client.py`:
|
|
- Wrap all LLM calls in try/except
|
|
- On provider failure (timeout, rate limit, API error):
|
|
- Log the failure
|
|
- Switch to fallback provider (Claude → OpenAI → Gemini)
|
|
- Retry the call
|
|
- Log which provider was used
|
|
|
|
**Verification:**
|
|
- Generate outline from brief → outline reflects source content accurately
|
|
- No hallucinated facts in outline
|
|
- Brand voice guidelines influence tone of generated text
|
|
- Disable primary LLM → auto-fallback to secondary provider → generation succeeds
|
|
- User instructions ("focus on ROI") reflected in outline emphasis
|
|
|
|
---
|
|
|
|
### 16. Job Queue & Async Generation
|
|
|
|
Redis-based job queue with ARQ for reliable async presentation generation.
|
|
|
|
**Backend:**
|
|
|
|
- [ ] Add dep: `pip install arq>=0.26`
|
|
|
|
- [ ] Create `backend/services/job_queue_service.py`:
|
|
- `JobQueueService` class:
|
|
- `enqueue(job_type: str, payload: dict) → UUID` — push job to ARQ Redis queue, create `JobModel` in DB
|
|
- `get_status(job_id: UUID) → JobModel` — poll job status
|
|
- `cancel(job_id: UUID)` — cancel queued job
|
|
|
|
- [ ] Create `backend/workers/main.py`:
|
|
- ARQ worker entry point
|
|
- Register task functions:
|
|
- `generate_presentation_task(ctx, job_id, ...)`
|
|
- `parse_master_deck_task(ctx, job_id, ...)`
|
|
- Configure: max_jobs=5, job_timeout=600s, retry=3 with exponential backoff
|
|
|
|
- [ ] Create `backend/workers/presentation_worker.py`:
|
|
- `generate_presentation_task(ctx, job_id: UUID)`:
|
|
1. Load job from DB, update status → `processing`
|
|
2. Load brief content + attachments
|
|
3. Run `ContentIntelligenceService.classify()`
|
|
4. Run `SlideMappingEngine.map()`
|
|
5. Run outline generation (LLM)
|
|
6. Run slide content generation (LLM, batched 10 at a time)
|
|
7. Run asset generation (images, icons — parallel)
|
|
8. Run chart data extraction + native chart prep
|
|
9. Run brand enforcement
|
|
10. Save slides to DB
|
|
11. Update job status → `completed`, progress → 100
|
|
- At each step: update `JobModel.progress` and `progress_message`
|
|
- On failure: update status → `failed`, save error_message
|
|
- SSE: push progress events via Redis pub/sub (existing SSE pattern)
|
|
|
|
- [ ] Create `backend/workers/master_deck_worker.py`:
|
|
- `parse_master_deck_task(ctx, job_id: UUID)`:
|
|
- Load master deck from DB
|
|
- Run `MasterDeckParserService.parse()`
|
|
- Update parse_status on completion/failure
|
|
|
|
- [ ] Modify `docker-compose.yml`:
|
|
- Add `worker` service: same image as `api`, entrypoint `python -m arq backend.workers.main.WorkerSettings`
|
|
|
|
- [ ] Create `backend/api/v1/ppt/endpoints/jobs.py`:
|
|
- `GET /api/v1/ppt/jobs/{id}` → job status + progress
|
|
- `GET /api/v1/ppt/jobs/{id}/stream` → SSE stream of job progress events
|
|
- `DELETE /api/v1/ppt/jobs/{id}` → cancel job
|
|
|
|
**Verification:**
|
|
- Start generation → job queued → worker picks up → progress updates visible
|
|
- Kill worker mid-job → job stays in `processing` → worker restart picks up retry
|
|
- 3 concurrent users generate → jobs processed via queue without conflicts
|
|
- Master deck parse runs as background job → status updates in admin UI
|
|
|
|
---
|
|
|
|
## Phase 5: Frontend Wizard & Editor
|
|
|
|
### 17. Wizard Flow — Steps 1-2 (Upload & Configure)
|
|
|
|
**Frontend:**
|
|
|
|
- [ ] Create `frontend/app/(presentation-generator)/generate/layout.tsx`:
|
|
- Wizard layout with step indicator bar (5 steps)
|
|
- Step labels: Upload → Configure → Outline → Generate → Edit
|
|
- Current step highlighted, completed steps checkmarked
|
|
- Each step is a nested route
|
|
|
|
- [ ] Create `frontend/app/(presentation-generator)/generate/upload/page.tsx` (Step 1):
|
|
- Large drag & drop zone for primary brief (DOCX)
|
|
- "Or browse files" button fallback
|
|
- Section for additional attachments (Excel, CSV, images, PDF)
|
|
- Each uploaded file shows: name, size, type badge, remove button
|
|
- Optional: URL input field with "Add reference URL" button
|
|
- Parsed content preview (shows extracted text/tables for each file)
|
|
- "Next" button → navigates to Step 2
|
|
- Auto-save: uploads saved to `JobModel` / session state
|
|
|
|
- [ ] Create `frontend/app/(presentation-generator)/generate/configure/page.tsx` (Step 2):
|
|
- Client selector dropdown (only assigned clients from `authSlice`)
|
|
- Master deck selector (filtered by selected client, shows thumbnails)
|
|
- Slide count slider (range: 5-40, default: smart suggestion based on content length)
|
|
- Free-text instructions textarea ("Focus on...", "Skip...", "Emphasize...")
|
|
- Tone selector (if not locked by brand config): professional / casual / educational
|
|
- "Generate Outline" button → triggers outline generation → navigates to Step 3
|
|
- "Back" button → returns to Step 1
|
|
|
|
- [ ] Create `frontend/store/slices/wizardSlice.ts`:
|
|
- State: `{currentStep, uploadedFiles, selectedClientId, selectedDeckId, slideCount, instructions, tone, outlines, jobId, presentationId}`
|
|
- Persist to localStorage for auto-save (user can close browser and return)
|
|
- Actions: setStep, setFiles, setClient, setDeck, etc.
|
|
|
|
**Verification:**
|
|
- Drag DOCX → file appears with metadata → proceed to Step 2
|
|
- Select client → master decks filter → select deck → slide count slider works
|
|
- Close browser on Step 2 → reopen → state restored
|
|
- Back button works between steps
|
|
|
|
---
|
|
|
|
### 18. Wizard Flow — Step 3 (Outline Review)
|
|
|
|
**Frontend:**
|
|
|
|
- [ ] Create `frontend/app/(presentation-generator)/generate/outline/page.tsx` (Step 3):
|
|
- **Split view (side-by-side)**:
|
|
- LEFT panel: parsed source content (brief markdown rendered as formatted text)
|
|
- Collapsible sections per heading
|
|
- Highlight which content block maps to which outline item (linked highlighting)
|
|
- RIGHT panel: generated outline
|
|
- Each outline item as a card: slide number, title, description, layout type badge
|
|
- Drag & drop reordering (using existing `@dnd-kit` dependency)
|
|
- Inline edit: click title/description to edit
|
|
- "Add Slide" button → insert new outline item
|
|
- "Delete" button per slide → remove with confirmation
|
|
- Layout type dropdown per slide (from master deck available layouts)
|
|
- **Attachment mapping panel** (bottom or collapsible):
|
|
- List of uploaded attachments (Excel tables, images, etc.)
|
|
- Drag attachment onto a specific outline item → maps it to that slide
|
|
- Show which attachments are assigned and which are unassigned
|
|
- **Mandatory approval**: "Approve Outline & Generate" button (disabled until user has reviewed)
|
|
- "Back" button → return to Step 2
|
|
|
|
**Verification:**
|
|
- Outline shows on right, source on left
|
|
- Drag slide 3 above slide 2 → order changes
|
|
- Edit title inline → saved
|
|
- Add new slide → appears at bottom
|
|
- Delete slide → removed from outline
|
|
- Map Excel attachment to slide 5 → indicator shows
|
|
- Click "Approve & Generate" → generation starts → navigate to Step 4
|
|
|
|
---
|
|
|
|
### 19. Wizard Flow — Step 4 (Generation Progress)
|
|
|
|
**Frontend:**
|
|
|
|
- [ ] Create `frontend/app/(presentation-generator)/generate/progress/page.tsx` (Step 4):
|
|
- Progress bar showing overall completion (0-100%)
|
|
- Status message text (e.g., "Generating slide 5 of 15...")
|
|
- Live slide preview grid:
|
|
- As each slide completes, its thumbnail appears in the grid
|
|
- Slides appear one by one with a fade-in animation
|
|
- Click thumbnail → shows larger preview in a modal
|
|
- SSE connection to `/api/v1/ppt/jobs/{id}/stream` for real-time updates
|
|
- On completion → auto-navigate to Step 5 (`/presentation/{id}`)
|
|
- On failure → show error message with "Retry" button
|
|
- "Cancel" button → cancels job, returns to Step 3
|
|
|
|
**Verification:**
|
|
- Generation starts → progress bar moves → slides appear one by one
|
|
- Close browser → reopen → progress page shows current state (polling fallback)
|
|
- Generation fails at slide 8 → error shown → "Retry" re-queues from slide 8
|
|
- Cancel → job cancelled → return to outline
|
|
|
|
---
|
|
|
|
### 20. Wizard Flow — Step 5 / Slide Editor
|
|
|
|
Enhance Presenton's existing slide editor with new capabilities.
|
|
|
|
**Frontend:**
|
|
|
|
- [ ] Modify `frontend/app/(presentation-generator)/presentation/page.tsx`:
|
|
- Integrate with wizard flow (accessible as Step 5 and as standalone for saved presentations)
|
|
- Add toolbar with:
|
|
- Review status indicator (Draft / In Review / Approved)
|
|
- "Save to Library" button
|
|
- Export dropdown: ".pptx" and "PDF"
|
|
- "Download" button
|
|
|
|
- [ ] Add per-slide regeneration:
|
|
- Right-click slide or "..." menu → "Regenerate this slide"
|
|
- Opens modal with:
|
|
- Current slide content preview
|
|
- Text input: "Instructions for regeneration" (e.g., "Make more concise", "Add a chart")
|
|
- "Regenerate" button → calls backend → replaces slide content
|
|
- "Cancel" button
|
|
|
|
- [ ] Add per-image choice:
|
|
- Click image on slide → popover shows:
|
|
- Current image (from source or AI-generated)
|
|
- "Source image" tab (if brief had an image for this section)
|
|
- "Stock images" tab (search Pexels/Pixabay)
|
|
- "AI generate" tab (enter prompt, generate new image)
|
|
- "Upload" tab (upload custom image)
|
|
- Select image → replaces in slide
|
|
|
|
- [ ] Add per-table/chart choice:
|
|
- Click table → context menu:
|
|
- "Keep as table" (render as formatted table)
|
|
- "Convert to chart" → opens chart type picker + chart data editor
|
|
- Click chart → context menu:
|
|
- "Edit data" → opens `ChartDataEditor`
|
|
- "Change chart type" → type picker dropdown
|
|
- "Convert to table" → renders data as table instead
|
|
|
|
- [ ] Integrate `ChartDataEditor.tsx` (from step 13):
|
|
- Opens as modal/panel when editing chart data
|
|
- Changes reflect live in slide preview
|
|
|
|
**Verification:**
|
|
- Regenerate slide 5 with "make it shorter" → new content generated, slide updates
|
|
- Click image → swap with stock photo from Pexels → slide updates
|
|
- Click table → "Convert to chart" → bar chart appears → edit data → chart updates
|
|
- Save to library → presentation persisted → appears on dashboard
|
|
|
|
---
|
|
|
|
### 21. Review Workflow
|
|
|
|
Status-based review: Draft → In Review → Approved.
|
|
|
|
**Backend:**
|
|
|
|
- [ ] Create `backend/api/v1/ppt/endpoints/review.py`:
|
|
- `PUT /api/v1/ppt/presentation/{id}/status` → change status
|
|
- Body: `{status: "in_review" | "approved" | "draft", comment: str | None}`
|
|
- Validation: only owner or team member can change status
|
|
- If client review_policy == "require_reviewer": cannot self-approve (owner != approver)
|
|
- Audit log entry on every status change
|
|
- `GET /api/v1/ppt/presentation/{id}/comments` → list comments/status changes
|
|
- `POST /api/v1/ppt/presentation/{id}/comment` → add comment (any team member)
|
|
|
|
**Frontend:**
|
|
|
|
- [ ] Create `frontend/app/(presentation-generator)/components/ReviewWorkflow.tsx`:
|
|
- Status badge (color-coded: yellow=Draft, blue=In Review, green=Approved)
|
|
- "Submit for Review" button (Draft → In Review)
|
|
- "Approve" button (In Review → Approved, only if reviewer is different user or self-approve allowed)
|
|
- "Request Changes" button (In Review → Draft, with required comment)
|
|
- Comments section: threaded comments with user avatar, timestamp
|
|
- "Add Comment" text area
|
|
|
|
- [ ] Add to presentation page header:
|
|
- Review status badge
|
|
- Action buttons based on current status and user role
|
|
|
|
**Verification:**
|
|
- User creates deck → status = Draft
|
|
- User clicks "Submit for Review" → status = In Review
|
|
- Different user opens same deck → can Approve or Request Changes
|
|
- Client with require_reviewer policy → owner cannot self-approve
|
|
- Comment added → visible to all team members
|
|
- Approved deck → export enabled
|
|
|
|
---
|
|
|
|
## Phase 6: Export & Polish
|
|
|
|
### 22. Export Pipeline (PPTX + PDF)
|
|
|
|
Enhance export with native charts, brand enforcement, and PDF support.
|
|
|
|
**Backend:**
|
|
|
|
- [ ] Modify `backend/utils/export_utils.py`:
|
|
- `export_presentation(presentation_id, format, user)`:
|
|
1. Load presentation + slides from DB
|
|
2. Load master deck config + brand config
|
|
3. Convert slide data → `PptxPresentationModel` (existing pipeline via Next.js API)
|
|
4. For chart slides: use `NativeChartService.add_native_chart()` instead of image
|
|
5. Run `BrandEnforcementService.enforce_on_pptx_model()` — final brand pass
|
|
6. Render PPTX via `PptxPresentationCreator`
|
|
7. If format == "pdf": also render via Puppeteer (existing pipeline)
|
|
8. Audit log: record export action
|
|
9. Return file path for download
|
|
|
|
- [ ] Create `backend/api/v1/ppt/endpoints/export.py`:
|
|
- `POST /api/v1/ppt/presentation/{id}/export` → body: `{format: "pptx" | "pdf"}`
|
|
- Returns: file download response
|
|
- Access check: user must have access to presentation's client
|
|
|
|
**Frontend:**
|
|
|
|
- [ ] Add export dropdown to slide editor:
|
|
- "Export as .pptx" → calls export endpoint → browser downloads file
|
|
- "Export as PDF" → calls export endpoint → browser downloads file
|
|
- Show loading spinner during export
|
|
- Toast on success/failure
|
|
|
|
**Verification:**
|
|
- Export PPTX → open in PowerPoint → all text editable, charts editable, brand correct
|
|
- Export PDF → opens in viewer, all slides rendered correctly
|
|
- Charts in PPTX: right-click → Edit Data → data table appears
|
|
- Gantt/Waterfall: shapes are individually selectable and editable
|
|
- Brand colors/fonts/logo present on every slide
|
|
|
|
---
|
|
|
|
### 23. Client Library & Dashboard
|
|
|
|
Two-level navigation: clients → templates/decks. Dashboard for saved presentations.
|
|
|
|
**Frontend:**
|
|
|
|
- [ ] Redesign `frontend/app/(presentation-generator)/dashboard/page.tsx`:
|
|
- **Two-level navigation:**
|
|
- First view: grid of client cards (only assigned clients + Oliver Team)
|
|
- Each card: client logo, name, deck count, "New Presentation" button
|
|
- Click client → second view: client's master decks + saved presentations
|
|
- **Client detail view:**
|
|
- Tabs: "Templates" (master decks) | "My Presentations" | "Team Presentations"
|
|
- Templates tab: master deck cards with thumbnails, "Use this template" button
|
|
- My Presentations: list of user's saved decks for this client (with status badges)
|
|
- Team Presentations: decks by other team members (read-only unless reviewer)
|
|
- **"New Presentation" button** → starts wizard (Step 1) with client pre-selected
|
|
- Search/filter bar for presentations
|
|
|
|
- [ ] Create `frontend/store/slices/clientSlice.ts`:
|
|
- State: `{clients: Client[], selectedClientId, masterDecks: MasterDeck[], presentations: Presentation[]}`
|
|
- Thunks: `fetchClients()`, `fetchMasterDecks(clientId)`, `fetchPresentations(clientId)`
|
|
|
|
**Verification:**
|
|
- User in Nike + Adidas teams → sees Nike, Adidas, Oliver Team cards
|
|
- Click Nike → sees Nike master decks + Nike presentations
|
|
- User not in Adidas team → Adidas card not visible
|
|
- Super Admin sees all clients
|
|
- "New Presentation" → wizard starts with Nike pre-selected
|
|
|
|
---
|
|
|
|
### 24. Data Retention Service
|
|
|
|
Auto-purge expired data per client retention policy.
|
|
|
|
**Backend:**
|
|
|
|
- [ ] Create `backend/services/retention_service.py`:
|
|
- `RetentionService` class:
|
|
- `run_cleanup()` — scheduled task (daily via ARQ cron):
|
|
1. Query all clients with `retention_days IS NOT NULL`
|
|
2. For each client: find presentations where `created_at < now - retention_days` and `deleted_at IS NULL`
|
|
3. Soft-delete expired presentations (set `deleted_at`)
|
|
4. Delete associated files from filesystem
|
|
5. Audit log each deletion
|
|
- `purge_soft_deleted(days_after_soft_delete=30)` — permanently delete records 30 days after soft delete
|
|
|
|
- [ ] Add ARQ cron job in `backend/workers/main.py`:
|
|
- Schedule `run_cleanup` daily at 2:00 AM
|
|
- Schedule `purge_soft_deleted` weekly
|
|
|
|
**Verification:**
|
|
- Set client retention to 1 day → create presentation → wait → presentation soft-deleted
|
|
- Soft-deleted presentations not visible in UI but still in DB
|
|
- After 30 days → permanently deleted from DB and filesystem
|
|
- Audit log shows retention cleanup entries
|
|
|
|
---
|
|
|
|
### 25. Brand-Adaptive UI Theme
|
|
|
|
UI colors adapt to the selected client's brand colors.
|
|
|
|
**Frontend:**
|
|
|
|
- [ ] Modify `frontend/app/globals.css`:
|
|
- Define CSS custom properties for brand-adaptive colors:
|
|
```css
|
|
:root {
|
|
--brand-primary: #...;
|
|
--brand-secondary: #...;
|
|
--brand-accent: #...;
|
|
}
|
|
```
|
|
|
|
- [ ] Create `frontend/hooks/useBrandTheme.ts`:
|
|
- Reads selected client's brand config from Redux store
|
|
- Sets CSS custom properties on `document.documentElement`
|
|
- Fallback: Oliver brand colors when no client selected
|
|
|
|
- [ ] Update key UI elements to use brand CSS variables:
|
|
- Wizard step indicator active color
|
|
- Primary buttons
|
|
- Header accent color
|
|
- Slide editor toolbar highlights
|
|
- Client card borders
|
|
|
|
**Verification:**
|
|
- Select Nike (red brand) → wizard step bar turns red, buttons turn red
|
|
- Select Adidas (blue brand) → UI shifts to blue accents
|
|
- No client selected → Oliver default theme
|
|
- Theme changes are instant (no page reload)
|
|
|
|
---
|
|
|
|
### 26. Analytics Dashboard
|
|
|
|
Admin dashboard with usage, quality, and performance metrics.
|
|
|
|
**Backend:**
|
|
|
|
- [ ] Create `backend/api/v1/admin/analytics_router.py`:
|
|
- `GET /api/v1/admin/analytics/overview` → aggregated stats:
|
|
- Total presentations generated (all time, this month, this week)
|
|
- Active users count
|
|
- Average generation time
|
|
- Most popular master decks
|
|
- `GET /api/v1/admin/analytics/usage` → usage metrics:
|
|
- Decks per user (top 10)
|
|
- Decks per client (top 10)
|
|
- Decks per day (time series, last 30 days)
|
|
- `GET /api/v1/admin/analytics/quality` → quality metrics:
|
|
- Average slides accepted without edit (% per deck)
|
|
- Average regenerations per deck
|
|
- Average comments per deck
|
|
- Approval rate
|
|
- `GET /api/v1/admin/analytics/performance` → performance metrics:
|
|
- Average generation time (by slide count)
|
|
- LLM provider usage breakdown
|
|
- Error/failure rates
|
|
- API cost estimates (based on token usage)
|
|
- Client Admin: filtered to their clients. Super Admin: all.
|
|
|
|
**Frontend:**
|
|
|
|
- [ ] Wire `frontend/app/admin/analytics/page.tsx`:
|
|
- Overview cards: total decks, active users, avg generation time
|
|
- Usage chart: line chart of decks per day (Recharts)
|
|
- Quality metrics: bar chart of acceptance rates
|
|
- Performance: generation time distribution
|
|
- Client filter dropdown (Super Admin sees all)
|
|
- Date range picker
|
|
|
|
**Verification:**
|
|
- Generate 5 decks → analytics show 5 decks, correct generation times
|
|
- Client Admin sees only their client's metrics
|
|
- Charts render with real data
|
|
|
|
---
|
|
|
|
## Phase 7: Testing & Verification
|
|
|
|
### 27. Testing Suite
|
|
|
|
**Backend tests:**
|
|
|
|
- [ ] Auth tests: login flow, JWT validation, role checks, token expiry
|
|
- [ ] RBAC tests: client access, team membership, cross-client isolation
|
|
- [ ] Audit tests: actions logged correctly, export format correct
|
|
- [ ] Content intelligence tests: classification accuracy for each content type
|
|
- [ ] Chart data extraction tests: table → chart data conversion accuracy
|
|
- [ ] Slide mapping tests: content type → layout mapping correctness
|
|
- [ ] Brand enforcement tests: color/font/logo replacement
|
|
- [ ] Native chart tests: each chart type renders correctly in PPTX
|
|
- [ ] Job queue tests: enqueue, process, retry, failure handling
|
|
- [ ] Retention tests: soft delete, purge timing
|
|
- [ ] Export tests: PPTX validity, PDF generation
|
|
- [ ] Integration test: full pipeline brief → classified content → outline → slides → export
|
|
|
|
**Frontend tests (Cypress E2E):**
|
|
|
|
- [ ] Login flow: Azure AD redirect → callback → dashboard
|
|
- [ ] Wizard flow: upload → configure → outline → generate → edit → export
|
|
- [ ] Admin: create client → upload master deck → configure brand → assign users
|
|
- [ ] RBAC: user cannot access unauthorized clients
|
|
- [ ] Review workflow: Draft → In Review → Approved
|
|
- [ ] Chart editor: edit data → preview updates → export correct
|
|
- [ ] Auto-save: close browser mid-wizard → return → state preserved
|
|
|
|
**Verification:**
|
|
- `make test` runs all backend tests
|
|
- `make test-e2e` runs Cypress suite
|
|
- All tests pass in CI (Docker-based)
|
|
|
|
---
|
|
|
|
## Dependency Graph (Build Order)
|
|
|
|
```
|
|
Phase 1: Foundation
|
|
1. Project Setup ──────────────────────────────────────┐
|
|
2. Database Schema ─── depends on 1 │
|
|
3. Auth Module ──────── depends on 2 │
|
|
4. RBAC & Teams ─────── depends on 3 │
|
|
5. Audit Logging ────── depends on 4 │
|
|
6. i18n Setup ───────── depends on 1 (parallel with 2) │
|
|
|
|
Phase 2: Admin & Clients │
|
|
7. Admin Panel Shell ── depends on 4, 6 │
|
|
8. Client & Brand ───── depends on 7 │
|
|
9. Master Deck Parser ─ depends on 8 │
|
|
|
|
Phase 3: Content Pipeline │
|
|
10. File Upload ─────── depends on 2, 4 │
|
|
11. Content Intelligence depends on 10 │
|
|
12. Slide Mapping ───── depends on 9, 11 │
|
|
13. Charts (extract + native PPTX) ── depends on 11 │
|
|
|
|
Phase 4: Generation │
|
|
14. Brand Enforcement ─ depends on 8 │
|
|
15. Outline Generation ─ depends on 11, 14 │
|
|
16. Job Queue ────────── depends on 15, 12, 13 │
|
|
|
|
Phase 5: Frontend Wizard & Editor │
|
|
17. Wizard Steps 1-2 ── depends on 7, 10 │
|
|
18. Wizard Step 3 ───── depends on 15, 17 │
|
|
19. Wizard Step 4 ───── depends on 16, 18 │
|
|
20. Wizard Step 5 / Editor ── depends on 13, 19 │
|
|
21. Review Workflow ──── depends on 4, 20 │
|
|
|
|
Phase 6: Export & Polish │
|
|
22. Export Pipeline ──── depends on 13, 14, 20 │
|
|
23. Client Library ───── depends on 8, 20 │
|
|
24. Data Retention ───── depends on 5, 8 │
|
|
25. Brand-Adaptive Theme depends on 8 │
|
|
26. Analytics Dashboard ─ depends on 5, 7 │
|
|
|
|
Phase 7: Testing │
|
|
27. Testing Suite ────── depends on everything ─┘
|
|
```
|
|
|
|
---
|
|
|
|
## Files Quick Reference
|
|
|
|
**New files to create (backend):**
|
|
```
|
|
backend/
|
|
├── api/
|
|
│ ├── middlewares/
|
|
│ │ ├── auth_middleware.py (Step 3)
|
|
│ │ ├── rbac_middleware.py (Step 4)
|
|
│ │ └── audit_middleware.py (Step 5)
|
|
│ └── v1/
|
|
│ ├── auth/
|
|
│ │ └── router.py (Step 3)
|
|
│ └── admin/
|
|
│ ├── users_router.py (Step 4)
|
|
│ ├── teams_router.py (Step 4)
|
|
│ ├── clients_router.py (Step 4)
|
|
│ ├── brand_config_router.py (Step 8)
|
|
│ ├── master_decks_router.py (Step 9)
|
|
│ ├── audit_router.py (Step 5)
|
|
│ └── analytics_router.py (Step 26)
|
|
├── models/
|
|
│ ├── sql/
|
|
│ │ ├── user.py (Step 2)
|
|
│ │ ├── client.py (Step 2)
|
|
│ │ ├── team.py (Step 2)
|
|
│ │ ├── team_membership.py (Step 2)
|
|
│ │ ├── brand_config.py (Step 2)
|
|
│ │ ├── master_deck.py (Step 2)
|
|
│ │ ├── audit_log.py (Step 2)
|
|
│ │ └── job.py (Step 2)
|
|
│ └── content_models.py (Step 11)
|
|
├── services/
|
|
│ ├── auth_service.py (Step 3)
|
|
│ ├── access_service.py (Step 4)
|
|
│ ├── audit_service.py (Step 5)
|
|
│ ├── attachment_parser_service.py (Step 10)
|
|
│ ├── content_intelligence_service.py (Step 11)
|
|
│ ├── slide_mapping_engine.py (Step 12)
|
|
│ ├── chart_data_extractor.py (Step 13)
|
|
│ ├── native_chart_service.py (Step 13)
|
|
│ ├── brand_enforcement_service.py (Step 14)
|
|
│ ├── master_deck_parser_service.py (Step 9)
|
|
│ ├── job_queue_service.py (Step 16)
|
|
│ └── retention_service.py (Step 24)
|
|
├── workers/
|
|
│ ├── main.py (Step 16)
|
|
│ ├── presentation_worker.py (Step 16)
|
|
│ └── master_deck_worker.py (Step 16)
|
|
├── utils/
|
|
│ └── auth_dependencies.py (Step 3)
|
|
└── migrations/ (Step 2, Alembic)
|
|
```
|
|
|
|
**New files to create (frontend):**
|
|
```
|
|
frontend/
|
|
├── app/
|
|
│ ├── login/
|
|
│ │ └── page.tsx (Step 3)
|
|
│ ├── admin/
|
|
│ │ ├── layout.tsx (Step 7)
|
|
│ │ ├── page.tsx (Step 7)
|
|
│ │ ├── users/ (Step 7)
|
|
│ │ ├── clients/ (Step 7, 8, 9)
|
|
│ │ ├── audit/ (Step 7)
|
|
│ │ ├── analytics/ (Step 26)
|
|
│ │ ├── settings/ (Step 7)
|
|
│ │ └── components/ (Step 7)
|
|
│ └── (presentation-generator)/
|
|
│ ├── generate/
|
|
│ │ ├── layout.tsx (Step 17)
|
|
│ │ ├── upload/page.tsx (Step 17)
|
|
│ │ ├── configure/page.tsx (Step 17)
|
|
│ │ ├── outline/page.tsx (Step 18)
|
|
│ │ └── progress/page.tsx (Step 19)
|
|
│ └── components/
|
|
│ ├── ChartDataEditor.tsx (Step 13)
|
|
│ ├── ReviewWorkflow.tsx (Step 21)
|
|
│ └── AttachmentMapper.tsx (Step 18)
|
|
├── components/
|
|
│ └── AuthGuard.tsx (Step 3)
|
|
├── store/slices/
|
|
│ ├── authSlice.ts (Step 3)
|
|
│ ├── wizardSlice.ts (Step 17)
|
|
│ ├── clientSlice.ts (Step 23)
|
|
│ └── adminSlice.ts (Step 7)
|
|
├── hooks/
|
|
│ └── useBrandTheme.ts (Step 25)
|
|
└── i18n/
|
|
├── i18n.ts (Step 6)
|
|
├── I18nProvider.tsx (Step 6)
|
|
└── locales/en/ (Step 6)
|
|
```
|