Fix 5 post-Phase8 bugs: SSE crash, custom templates, ordering, persistence, JSON display
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
df99af91ac
commit
e360983249
6 changed files with 157 additions and 68 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="h-full mr-4">
|
||||
<div className="overflow-y-auto custom_scrollbar h-full">
|
||||
|
|
@ -208,7 +237,7 @@ const DocumentsPreviewPage: React.FC = () => {
|
|||
<Skeleton className="w-full h-full" />
|
||||
) : (
|
||||
<MarkdownRenderer
|
||||
content={textContents[selectedDocument] || ""}
|
||||
content={displayContent}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<string>(TABS.OUTLINE);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<TemplateLayoutsWithSettings | string | null>(null);
|
||||
|
||||
// Restore selectedTemplate from Redux (persists across navigation)
|
||||
// Custom templates are stored as string (templateId), built-ins as TemplateLayoutsWithSettings
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<TemplateLayoutsWithSettings | string | null>(() => {
|
||||
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 <EmptyStateView />;
|
||||
}
|
||||
|
|
@ -75,7 +102,7 @@ const OutlinePage: React.FC = () => {
|
|||
<div>
|
||||
<TemplateSelection
|
||||
selectedTemplate={selectedTemplate}
|
||||
onSelectTemplate={setSelectedTemplate}
|
||||
onSelectTemplate={handleSelectTemplate}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
|
|
|||
|
|
@ -41,6 +41,38 @@ const TemplateSelection: React.FC<TemplateSelectionProps> = ({
|
|||
|
||||
return (
|
||||
<div className="space-y-8 mb-4">
|
||||
{/* Custom AI Templates — shown first */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Custom AI Templates</h3>
|
||||
</div>
|
||||
{customLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
|
||||
<span className="ml-3 text-gray-600">Loading custom templates...</span>
|
||||
</div>
|
||||
) : customTemplates.length === 0 ? (
|
||||
<Card className="p-8 text-center">
|
||||
<p className="text-gray-500">No custom templates yet.</p>
|
||||
<p className="text-sm text-gray-400 mt-2">
|
||||
Custom templates you create will appear here.
|
||||
</p>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{customTemplates.map((template: CustomTemplates) => (
|
||||
|
||||
<CustomTemplateCard
|
||||
key={template.id}
|
||||
template={template}
|
||||
onSelectTemplate={onSelectTemplate}
|
||||
selectedTemplate={typeof selectedTemplate === 'string' ? selectedTemplate : null}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* In Built Templates */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-3">In Built Templates</h3>
|
||||
|
|
@ -97,38 +129,6 @@ const TemplateSelection: React.FC<TemplateSelectionProps> = ({
|
|||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom AI Templates */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Custom AI Templates</h3>
|
||||
</div>
|
||||
{customLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
|
||||
<span className="ml-3 text-gray-600">Loading custom templates...</span>
|
||||
</div>
|
||||
) : customTemplates.length === 0 ? (
|
||||
<Card className="p-8 text-center">
|
||||
<p className="text-gray-500">No custom templates yet.</p>
|
||||
<p className="text-sm text-gray-400 mt-2">
|
||||
Custom templates you create will appear here.
|
||||
</p>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{customTemplates.map((template: CustomTemplates) => (
|
||||
|
||||
<CustomTemplateCard
|
||||
key={template.id}
|
||||
template={template}
|
||||
onSelectTemplate={onSelectTemplate}
|
||||
selectedTemplate={typeof selectedTemplate === 'string' ? selectedTemplate : null}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<string | null>) => {
|
||||
state.selectedTemplateId = action.payload;
|
||||
},
|
||||
// Slides rendereimport { useEffect } from "react"d
|
||||
setSlidesRendered: (state, action: PayloadAction<boolean>) => {
|
||||
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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue