Fix 422 errors: clientSlice bare fetch, prepare schemaJSON null, update SlideModel validation

- clientSlice: fetchMasterDecks + fetchClientPresentations use apiFetch (adds /ppt-tool basePath)
- useCustomTemplates: parsedLayoutToCompiled generates schemaJSON from elements instead of null
  (null schemaJSON caused 422 on /prepare because backend SlideLayoutModel.json_schema: dict is required)
- presentation.py: update_presentation uses SlideInput (plain Pydantic model with extra='ignore')
  instead of SlideModel (table=True SQLModel) to avoid strict validation causing 422 on /update

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-03-20 18:26:54 +00:00
parent 17d1a0573c
commit 25b70af9fb
3 changed files with 52 additions and 10 deletions

View file

@ -43,6 +43,7 @@ from utils.llm_calls.generate_presentation_outlines import generate_ppt_outline
from utils.llm_calls.summarize_brief import summarize_brief
from models.sql.slide import SlideModel
from models.sse_response import SSECompleteResponse, SSEErrorResponse, SSEResponse
from pydantic import BaseModel, ConfigDict
from services.database import get_async_session, async_session_maker
from services.temp_file_service import TEMP_FILE_SERVICE
@ -73,6 +74,23 @@ from utils.process_slides import (
import uuid
class SlideInput(BaseModel):
"""Plain Pydantic model for slide data in request body (avoids SQLModel table=True validation quirks)."""
model_config = ConfigDict(extra="ignore")
id: Optional[uuid.UUID] = None
presentation: uuid.UUID
layout_group: str
layout: str
index: int
content: dict
html_content: Optional[str] = None
speaker_note: Optional[str] = None
properties: Optional[dict] = None
deleted_at: Optional[datetime] = None
PRESENTATION_ROUTER = APIRouter(prefix="/presentation", tags=["Presentation"])
@ -425,7 +443,7 @@ async def update_presentation(
id: Annotated[uuid.UUID, Body()],
n_slides: Annotated[Optional[int], Body()] = None,
title: Annotated[Optional[str], Body()] = None,
slides: Annotated[Optional[List[SlideModel]], Body()] = None,
slides: Annotated[Optional[List[SlideInput]], Body()] = None,
_current_user: UserModel = Depends(get_current_user),
sql_session: AsyncSession = Depends(get_async_session),
):
@ -442,22 +460,32 @@ async def update_presentation(
if n_slides or title:
presentation.sqlmodel_update(presentation_update_dict)
slide_models: List[SlideModel] = []
if slides:
# Just to make sure id is UUID
for slide in slides:
slide.presentation = uuid.UUID(slide.presentation)
slide.id = uuid.UUID(slide.id)
for s in slides:
slide_models.append(SlideModel(
id=s.id or uuid.uuid4(),
presentation=s.presentation,
layout_group=s.layout_group,
layout=s.layout,
index=s.index,
content=s.content,
html_content=s.html_content,
speaker_note=s.speaker_note,
properties=s.properties,
deleted_at=s.deleted_at,
))
await sql_session.execute(
delete(SlideModel).where(SlideModel.presentation == presentation.id)
)
sql_session.add_all(slides)
sql_session.add_all(slide_models)
await sql_session.commit()
return PresentationWithSlides(
**presentation.model_dump(),
slides=slides or [],
slides=slide_models,
)

View file

@ -8,6 +8,20 @@ import TemplateService from "../(presentation-generator)/services/api/template";
import { isJsonLayoutCode, parseLayoutSchema, ParsedLayout, LayoutSchema } from "./parseLayoutSchema";
import { SlideRenderer } from "../(presentation-generator)/components/SlideRenderer";
// Build a JSON Schema from a LayoutSchema's elements so the backend LLM knows what content to generate.
function buildSchemaFromElements(layoutSchema: LayoutSchema): Record<string, any> {
const properties: Record<string, any> = {};
for (const el of layoutSchema.elements) {
if (!el.placeholder) continue;
properties[el.placeholder] = { type: "string" };
}
return {
type: "object",
title: layoutSchema.layoutName,
properties,
};
}
// Adapter: convert ParsedLayout (JSON schema) into a CompiledLayout-compatible object
// so existing code that uses CompiledLayout can work with both formats.
function parsedLayoutToCompiled(parsed: ParsedLayout): CompiledLayout {
@ -26,7 +40,7 @@ function parsedLayoutToCompiled(parsed: ParsedLayout): CompiledLayout {
layoutDescription: parsed.layoutDescription,
schema: null,
sampleData: parsed.sampleData,
schemaJSON: null,
schemaJSON: buildSchemaFromElements(schema),
};
}

View file

@ -67,7 +67,7 @@ export const fetchMasterDecks = createAsyncThunk(
"client/fetchMasterDecks",
async (clientId: string, { rejectWithValue }) => {
try {
const response = await fetch(
const response = await apiFetch(
`/api/v1/admin/master-decks?client_id=${clientId}`,
{ headers: getHeader() }
);
@ -84,7 +84,7 @@ export const fetchClientPresentations = createAsyncThunk(
"client/fetchClientPresentations",
async (clientId: string, { rejectWithValue }) => {
try {
const response = await fetch(
const response = await apiFetch(
`/api/v1/ppt/presentation/all?client_id=${clientId}`,
{ headers: getHeader() }
);