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,