ppt-tool/backend/api/v1/ppt/endpoints/review.py
Vadym Samoilenko ad65f6fe2d Phase 5: Frontend Wizard & Editor — 5-step generation wizard, review workflow
- New 5-step wizard flow at /generate/ (Upload → Configure → Outline → Generate → Edit)
- wizardSlice with localStorage persistence for cross-session state
- Upload page: drag & drop files, brief text input, file type badges
- Configure page: client/deck selectors, slide count slider, tone, language, instructions
- Outline review page: split-view with source content + outline editor, template selection
- Progress page: SSE streaming + polling fallback for real-time job progress
- Review workflow: status badge (Draft/In Review/Approved) with popover transitions
- Backend review endpoints: PUT status, POST comment, GET review info with audit logging
- Wizard API helpers: clients, master decks, file upload/decompose, job status/cancel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 16:31:28 +00:00

131 lines
4 KiB
Python

"""Presentation review workflow endpoints."""
import uuid
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from models.sql.presentation import PresentationModel
from models.sql.user import UserModel
from services import audit_service
from services.database import get_async_session
from utils.auth_dependencies import get_current_user
REVIEW_ROUTER = APIRouter(prefix="/api/v1/ppt", tags=["Review"])
VALID_STATUSES = {"draft", "in_review", "approved"}
VALID_TRANSITIONS = {
"draft": {"in_review"},
"in_review": {"draft", "approved"},
"approved": {"draft"},
}
class StatusUpdateRequest(BaseModel):
status: str
comment: Optional[str] = None
class CommentRequest(BaseModel):
comment: str
@REVIEW_ROUTER.put("/presentation/{presentation_id}/status")
async def update_status(
presentation_id: uuid.UUID,
body: StatusUpdateRequest,
current_user: UserModel = Depends(get_current_user),
session: AsyncSession = Depends(get_async_session),
):
"""Change presentation review status (draft -> in_review -> approved)."""
if body.status not in VALID_STATUSES:
raise HTTPException(
status_code=400,
detail=f"Invalid status. Must be one of: {', '.join(VALID_STATUSES)}",
)
presentation = await session.get(PresentationModel, presentation_id)
if not presentation:
raise HTTPException(status_code=404, detail="Presentation not found")
current = presentation.status or "draft"
if body.status not in VALID_TRANSITIONS.get(current, set()):
raise HTTPException(
status_code=400,
detail=f"Cannot transition from '{current}' to '{body.status}'",
)
old_status = current
presentation.status = body.status
if body.comment:
presentation.review_comment = body.comment
await session.commit()
await session.refresh(presentation)
# Audit log (fire-and-forget)
audit_service.log(
user_id=current_user.id,
action="status_change",
resource_type="presentation",
resource_id=presentation.id,
client_id=presentation.client_id,
details={
"old_status": old_status,
"new_status": body.status,
"comment": body.comment,
},
)
return {
"id": str(presentation.id),
"status": presentation.status,
"review_comment": presentation.review_comment,
}
@REVIEW_ROUTER.post("/presentation/{presentation_id}/comment")
async def add_comment(
presentation_id: uuid.UUID,
body: CommentRequest,
current_user: UserModel = Depends(get_current_user),
session: AsyncSession = Depends(get_async_session),
):
"""Add a review comment to a presentation."""
presentation = await session.get(PresentationModel, presentation_id)
if not presentation:
raise HTTPException(status_code=404, detail="Presentation not found")
presentation.review_comment = body.comment
await session.commit()
audit_service.log(
user_id=current_user.id,
action="comment",
resource_type="presentation",
resource_id=presentation.id,
client_id=presentation.client_id,
details={"comment": body.comment},
)
return {"ok": True}
@REVIEW_ROUTER.get("/presentation/{presentation_id}/review")
async def get_review_info(
presentation_id: uuid.UUID,
_current_user: UserModel = Depends(get_current_user),
session: AsyncSession = Depends(get_async_session),
):
"""Get review status and comment for a presentation."""
presentation = await session.get(PresentationModel, presentation_id)
if not presentation:
raise HTTPException(status_code=404, detail="Presentation not found")
return {
"id": str(presentation.id),
"status": presentation.status or "draft",
"review_comment": presentation.review_comment,
"title": presentation.title,
}