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:
parent
17d1a0573c
commit
25b70af9fb
3 changed files with 52 additions and 10 deletions
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue