From e36098324912cedecfc067194430fbb07ecdb886 Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Sun, 1 Mar 2026 20:25:28 +0000 Subject: [PATCH] Fix 5 post-Phase8 bugs: SSE crash, custom templates, ordering, persistence, JSON display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - outlines.py: Fix ECONNRESET/socket-hang-up — Depends session closes before StreamingResponse generator runs; capture presentation data upfront, use async_session_maker() inside inner() for the final DB commit (same pattern as Phase 4) - useCustomTemplates.ts: Filter null-template items in summary map (crashed on presentations without a TemplateModel); use item.layout_count instead of hardcoded 0 - TemplateSelection.tsx: Move custom AI templates section above built-in templates - presentationGeneration.ts + OutlinePage.tsx: Add selectedTemplateId to Redux so template selection persists when navigating away and back to /outline; clearOutlines also resets selectedTemplateId for new presentation flows - DocumentPreviewPage.tsx: Detect JSON file content (table decomposition output) and convert to markdown table or pretty-printed code block before passing to MarkdownRenderer Co-Authored-By: Claude Sonnet 4.6 --- backend/api/v1/ppt/endpoints/outlines.py | 66 ++++++++++++------- .../components/DocumentPreviewPage.tsx | 31 ++++++++- .../outline/components/OutlinePage.tsx | 37 +++++++++-- .../outline/components/TemplateSelection.tsx | 64 +++++++++--------- frontend/app/hooks/useCustomTemplates.ts | 18 ++--- .../store/slices/presentationGeneration.ts | 9 +++ 6 files changed, 157 insertions(+), 68 deletions(-) diff --git a/backend/api/v1/ppt/endpoints/outlines.py b/backend/api/v1/ppt/endpoints/outlines.py index c7114cb..be028cb 100644 --- a/backend/api/v1/ppt/endpoints/outlines.py +++ b/backend/api/v1/ppt/endpoints/outlines.py @@ -17,7 +17,7 @@ from models.sse_response import ( SSEStatusResponse, ) from services.temp_file_service import TEMP_FILE_SERVICE -from services.database import get_async_session +from services.database import get_async_session, async_session_maker from services.documents_loader import DocumentsLoader from utils.llm_calls.generate_presentation_outlines import generate_ppt_outline from utils.ppt_utils import get_presentation_title_from_outlines @@ -34,6 +34,20 @@ async def stream_outlines( if not presentation: raise HTTPException(status_code=404, detail="Presentation not found") + # Capture all data from the presentation before the endpoint returns + # (Depends session closes when endpoint function returns, before StreamingResponse runs) + presentation_id = presentation.id + presentation_content = presentation.content + presentation_file_paths = presentation.file_paths + presentation_n_slides = presentation.n_slides + presentation_language = presentation.language + presentation_tone = presentation.tone + presentation_verbosity = presentation.verbosity + presentation_instructions = presentation.instructions + presentation_include_title_slide = presentation.include_title_slide + presentation_include_table_of_contents = presentation.include_table_of_contents + presentation_web_search = presentation.web_search + temp_dir = TEMP_FILE_SERVICE.create_temp_dir() async def inner(): @@ -42,8 +56,8 @@ async def stream_outlines( ).to_string() additional_context = "" - if presentation.file_paths: - documents_loader = DocumentsLoader(file_paths=presentation.file_paths) + if presentation_file_paths: + documents_loader = DocumentsLoader(file_paths=presentation_file_paths) await documents_loader.load_documents(temp_dir) documents = documents_loader.documents if documents: @@ -51,23 +65,23 @@ async def stream_outlines( presentation_outlines_text = "" - n_slides_to_generate = presentation.n_slides - if presentation.include_table_of_contents: - needed_toc_count = math.ceil((presentation.n_slides - 1) / 10) + n_slides_to_generate = presentation_n_slides + if presentation_include_table_of_contents: + needed_toc_count = math.ceil((presentation_n_slides - 1) / 10) n_slides_to_generate -= math.ceil( - (presentation.n_slides - needed_toc_count) / 10 + (presentation_n_slides - needed_toc_count) / 10 ) async for chunk in generate_ppt_outline( - presentation.content, + presentation_content, n_slides_to_generate, - presentation.language, + presentation_language, additional_context, - presentation.tone, - presentation.verbosity, - presentation.instructions, - presentation.include_title_slide, - presentation.web_search, + presentation_tone, + presentation_verbosity, + presentation_instructions, + presentation_include_title_slide, + presentation_web_search, ): # Give control to the event loop await asyncio.sleep(0) @@ -106,14 +120,22 @@ async def stream_outlines( :n_slides_to_generate ] - presentation.outlines = presentation_outlines.model_dump() - presentation.title = get_presentation_title_from_outlines(presentation_outlines) + outlines_data = presentation_outlines.model_dump() + title = get_presentation_title_from_outlines(presentation_outlines) - sql_session.add(presentation) - await sql_session.commit() - - yield SSECompleteResponse( - key="presentation", value=presentation.model_dump(mode="json") - ).to_string() + # Use a fresh session for the DB write — Depends session is already closed + async with async_session_maker() as db: + pres = await db.get(PresentationModel, presentation_id) + if pres: + pres.outlines = outlines_data + pres.title = title + db.add(pres) + await db.commit() + await db.refresh(pres) + yield SSECompleteResponse( + key="presentation", value=pres.model_dump(mode="json") + ).to_string() + else: + yield SSEErrorResponse(detail="Presentation not found after outline generation").to_string() return StreamingResponse(inner(), media_type="text/event-stream") diff --git a/frontend/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx b/frontend/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx index f921bd3..4f1acb4 100644 --- a/frontend/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx +++ b/frontend/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx @@ -191,6 +191,31 @@ const DocumentsPreviewPage: React.FC = () => { } }, [documentKeys]); + // Try to render JSON content as a readable markdown table/list + const formatJsonContent = (raw: string): string => { + try { + const parsed = JSON.parse(raw); + // Array of objects → markdown table + if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === 'object') { + const keys = Object.keys(parsed[0]); + const header = `| ${keys.join(' | ')} |`; + const sep = `| ${keys.map(() => '---').join(' | ')} |`; + const rows = parsed.map((row: any) => + `| ${keys.map((k) => String(row[k] ?? '')).join(' | ')} |` + ); + return [header, sep, ...rows].join('\n'); + } + // Object with table_data array + if (parsed && Array.isArray(parsed.table_data) && parsed.table_data.length > 0) { + return formatJsonContent(JSON.stringify(parsed.table_data)); + } + // Fallback: pretty-print as code block + return '```json\n' + JSON.stringify(parsed, null, 2) + '\n```'; + } catch { + return raw; + } + }; + // Render helpers const renderDocumentContent = () => { if (!selectedDocument) return null; @@ -199,6 +224,10 @@ const DocumentsPreviewPage: React.FC = () => { if (!isDocument) return null; + const rawContent = textContents[selectedDocument] || ""; + const isJsonContent = rawContent.trimStart().startsWith('{') || rawContent.trimStart().startsWith('['); + const displayContent = isJsonContent ? formatJsonContent(rawContent) : rawContent; + return (
@@ -208,7 +237,7 @@ const DocumentsPreviewPage: React.FC = () => { ) : ( )}
diff --git a/frontend/app/(presentation-generator)/outline/components/OutlinePage.tsx b/frontend/app/(presentation-generator)/outline/components/OutlinePage.tsx index c4a70b3..db9428f 100644 --- a/frontend/app/(presentation-generator)/outline/components/OutlinePage.tsx +++ b/frontend/app/(presentation-generator)/outline/components/OutlinePage.tsx @@ -1,9 +1,9 @@ "use client"; -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { RootState } from "@/store/store"; -import { useSelector } from "react-redux"; +import { useSelector, useDispatch } from "react-redux"; import { OverlayLoader } from "@/components/ui/overlay-loader"; import Wrapper from "@/components/Wrapper"; import OutlineContent from "./OutlineContent"; @@ -16,14 +16,34 @@ import { useOutlineManagement } from "../hooks/useOutlineManagement"; import { usePresentationGeneration } from "../hooks/usePresentationGeneration"; import TemplateSelection from "./TemplateSelection"; import { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils"; +import { templates } from "@/app/presentation-templates"; +import { setSelectedTemplateId } from "@/store/slices/presentationGeneration"; const OutlinePage: React.FC = () => { - const { presentation_id, outlines } = useSelector( + const dispatch = useDispatch(); + const { presentation_id, outlines, selectedTemplateId } = useSelector( (state: RootState) => state.presentationGeneration ); const [activeTab, setActiveTab] = useState(TABS.OUTLINE); - const [selectedTemplate, setSelectedTemplate] = useState(null); + + // Restore selectedTemplate from Redux (persists across navigation) + // Custom templates are stored as string (templateId), built-ins as TemplateLayoutsWithSettings + const [selectedTemplate, setSelectedTemplate] = useState(() => { + if (!selectedTemplateId) return null; + // Check if it matches a built-in template + const builtin = templates.find((t) => t.id === selectedTemplateId); + if (builtin) return builtin; + // Otherwise it's a custom template id (string) + return selectedTemplateId; + }); + + const handleSelectTemplate = (template: TemplateLayoutsWithSettings | string) => { + setSelectedTemplate(template); + // Persist to Redux + const id = typeof template === 'string' ? template : template.id; + dispatch(setSelectedTemplateId(id)); + }; // Custom hooks const streamState = useOutlineStreaming(presentation_id); const { handleDragEnd, handleAddSlide } = useOutlineManagement(outlines); @@ -33,6 +53,13 @@ const OutlinePage: React.FC = () => { selectedTemplate, setActiveTab ); + + // Also sync if selectedTemplateId changes externally (e.g. after clearing) + useEffect(() => { + if (!selectedTemplateId) { + setSelectedTemplate(null); + } + }, [selectedTemplateId]); if (!presentation_id) { return ; } @@ -75,7 +102,7 @@ const OutlinePage: React.FC = () => {
diff --git a/frontend/app/(presentation-generator)/outline/components/TemplateSelection.tsx b/frontend/app/(presentation-generator)/outline/components/TemplateSelection.tsx index 2335900..aca56c8 100644 --- a/frontend/app/(presentation-generator)/outline/components/TemplateSelection.tsx +++ b/frontend/app/(presentation-generator)/outline/components/TemplateSelection.tsx @@ -41,6 +41,38 @@ const TemplateSelection: React.FC = ({ return (
+ {/* Custom AI Templates — shown first */} +
+
+

Custom AI Templates

+
+ {customLoading ? ( +
+ + Loading custom templates... +
+ ) : customTemplates.length === 0 ? ( + +

No custom templates yet.

+

+ Custom templates you create will appear here. +

+
+ ) : ( +
+ {customTemplates.map((template: CustomTemplates) => ( + + + ))} +
+ )} +
+ {/* In Built Templates */}

In Built Templates

@@ -97,38 +129,6 @@ const TemplateSelection: React.FC = ({ })}
- - {/* Custom AI Templates */} -
-
-

Custom AI Templates

-
- {customLoading ? ( -
- - Loading custom templates... -
- ) : customTemplates.length === 0 ? ( - -

No custom templates yet.

-

- Custom templates you create will appear here. -

-
- ) : ( -
- {customTemplates.map((template: CustomTemplates) => ( - - - ))} -
- )} -
); }; diff --git a/frontend/app/hooks/useCustomTemplates.ts b/frontend/app/hooks/useCustomTemplates.ts index 210cf6f..8a1fbc1 100644 --- a/frontend/app/hooks/useCustomTemplates.ts +++ b/frontend/app/hooks/useCustomTemplates.ts @@ -264,14 +264,16 @@ export function useCustomTemplateSummaries() { // } // }); - const mappedTemplates: CustomTemplates[] = data.presentations.map((item: any) => { - return { - id: item.template.id, - name: item.template.name || "Custom Template", - layoutCount: 0, - isCustom: true as const, - } - }); + const mappedTemplates: CustomTemplates[] = data.presentations + .filter((item: any) => item.template != null) + .map((item: any) => { + return { + id: item.template.id, + name: item.template.name || "Custom Template", + layoutCount: item.layout_count ?? 0, + isCustom: true as const, + } + }); setTemplates(mappedTemplates); diff --git a/frontend/store/slices/presentationGeneration.ts b/frontend/store/slices/presentationGeneration.ts index 9c10e3d..ee3ac10 100644 --- a/frontend/store/slices/presentationGeneration.ts +++ b/frontend/store/slices/presentationGeneration.ts @@ -23,6 +23,8 @@ interface PresentationGenerationState { presentationData: PresentationData | null; isSlidesRendered: boolean; isLayoutLoading: boolean; + /** ID of the selected template (string = custom templateId, or built-in template id) */ + selectedTemplateId: string | null; } const initialState: PresentationGenerationState = { @@ -34,6 +36,7 @@ const initialState: PresentationGenerationState = { isStreaming: null, error: null, presentationData: null, + selectedTemplateId: null, }; const presentationGenerationSlice = createSlice({ @@ -55,6 +58,10 @@ const presentationGenerationSlice = createSlice({ state.presentation_id = action.payload; state.error = null; }, + // Selected template + setSelectedTemplateId: (state, action: PayloadAction) => { + state.selectedTemplateId = action.payload; + }, // Slides rendereimport { useEffect } from "react"d setSlidesRendered: (state, action: PayloadAction) => { state.isSlidesRendered = action.payload; @@ -70,6 +77,7 @@ const presentationGenerationSlice = createSlice({ }, clearOutlines: (state) => { state.outlines = []; + state.selectedTemplateId = null; }, // Set outlines setOutlines: (state, action: PayloadAction<{ content: string }[]>) => { @@ -392,6 +400,7 @@ export const { deleteSlideOutline, setPresentationData, setOutlines, + setSelectedTemplateId, // slides operations addSlide, updateSlide,