update: send template name and description and show them

This commit is contained in:
Suraj Jha 2025-08-10 00:24:13 +05:45
parent 9b9f6b88de
commit 2f653d78cf
No known key found for this signature in database
GPG key ID: 5AC6C16355CE2C14
10 changed files with 273 additions and 110 deletions

View file

@ -12,13 +12,14 @@ from utils.asset_directory_utils import get_images_directory
from services.database import get_async_session
from models.sql.presentation_layout_code import PresentationLayoutCodeModel
from .prompts import GENERATE_HTML_SYSTEM_PROMPT, HTML_TO_REACT_SYSTEM_PROMPT, HTML_EDIT_SYSTEM_PROMPT
from models.sql.template import TemplateModel
# Create separate routers for each functionality
SLIDE_TO_HTML_ROUTER = APIRouter(prefix="/slide-to-html", tags=["slide-to-html"])
HTML_TO_REACT_ROUTER = APIRouter(prefix="/html-to-react", tags=["html-to-react"])
HTML_EDIT_ROUTER = APIRouter(prefix="/html-edit", tags=["html-edit"])
LAYOUT_MANAGEMENT_ROUTER = APIRouter(prefix="/layout-management", tags=["layout-management"])
LAYOUT_MANAGEMENT_ROUTER = APIRouter(prefix="/template-management", tags=["template-management"])
# Request/Response models for slide-to-html endpoint
@ -74,12 +75,14 @@ class GetLayoutsResponse(BaseModel):
success: bool
layouts: list[LayoutData]
message: Optional[str] = None
template: Optional[dict] = None
class PresentationSummary(BaseModel):
presentation_id: str
layout_count: int
last_updated_at: Optional[datetime] = None
template: Optional[dict] = None
class GetPresentationSummaryResponse(BaseModel):
@ -96,6 +99,25 @@ class ErrorResponse(BaseModel):
error_code: Optional[str] = None
class TemplateCreateRequest(BaseModel):
id: str
name: str
description: Optional[str] = None
class TemplateCreateResponse(BaseModel):
success: bool
template: dict
message: Optional[str] = None
class TemplateInfo(BaseModel):
id: str
name: Optional[str] = None
description: Optional[str] = None
created_at: Optional[datetime] = None
async def generate_html_from_slide(base64_image: str, media_type: str, xml_content: str, api_key: str, fonts: Optional[List[str]] = None) -> str:
"""
Generate HTML content from slide image and XML using OpenAI GPT-5 Responses API.
@ -623,7 +645,7 @@ async def edit_html_with_images_endpoint(
# ENDPOINT 4: Save layouts for a presentation
@LAYOUT_MANAGEMENT_ROUTER.post(
"/save-layouts",
"/save-templates",
response_model=SaveLayoutsResponse,
responses={
400: {"model": ErrorResponse, "description": "Validation error"},
@ -739,7 +761,7 @@ async def save_layouts(
# ENDPOINT 5: Get layouts for a presentation
@LAYOUT_MANAGEMENT_ROUTER.get(
"/get-layouts/{presentation_id}",
"/get-templates/{presentation_id}",
response_model=GetLayoutsResponse,
responses={
400: {"model": ErrorResponse, "description": "Invalid presentation ID"},
@ -798,10 +820,22 @@ async def get_layouts(
for layout in layouts_db
]
# Fetch template meta
template_meta = await session.get(TemplateModel, presentation_id)
template = None
if template_meta:
template = {
"id": template_meta.id,
"name": template_meta.name,
"description": template_meta.description,
"created_at": template_meta.created_at,
}
return GetLayoutsResponse(
success=True,
layouts=layouts,
message=f"Retrieved {len(layouts)} layout(s) for presentation {presentation_id}"
message=f"Retrieved {len(layouts)} layout(s) for presentation {presentation_id}",
template=template,
)
except HTTPException:
@ -823,11 +857,11 @@ async def get_layouts(
description="Retrieve a summary of all presentations and the number of layouts in each",
responses={
200: {"model": GetPresentationSummaryResponse, "description": "Presentations summary retrieved successfully"},
500: {"model": ErrorResponse, "description": "Internal server error"}
}
500: {"model": ErrorResponse, "description": "Internal server error"},
},
)
async def get_presentations_summary(
session: AsyncSession = Depends(get_async_session)
session: AsyncSession = Depends(get_async_session),
):
"""
Get summary of all presentations with their layout counts.
@ -843,16 +877,27 @@ async def get_presentations_summary(
result = await session.execute(stmt)
presentation_data = result.all()
# Convert to response format
presentations = [
PresentationSummary(
presentation_id=row.presentation_id,
layout_count=row.layout_count,
last_updated_at=row.last_updated_at
# Convert to response format with template info if available
presentations = []
for row in presentation_data:
template_meta = await session.get(TemplateModel, row.presentation_id)
template = None
if template_meta:
template = {
"id": template_meta.id,
"name": template_meta.name,
"description": template_meta.description,
"created_at": template_meta.created_at,
}
presentations.append(
PresentationSummary(
presentation_id=row.presentation_id,
layout_count=row.layout_count,
last_updated_at=row.last_updated_at,
template=template,
)
)
for row in presentation_data
]
# Calculate totals
total_presentations = len(presentations)
total_layouts = sum(p.layout_count for p in presentations)
@ -862,7 +907,7 @@ async def get_presentations_summary(
presentations=presentations,
total_presentations=total_presentations,
total_layouts=total_layouts,
message=f"Retrieved {total_presentations} presentation(s) with {total_layouts} total layout(s)"
message=f"Retrieved {total_presentations} presentation(s) with {total_layouts} total layout(s)",
)
except Exception as e:
@ -870,4 +915,53 @@ async def get_presentations_summary(
raise HTTPException(
status_code=500,
detail=f"Internal server error while retrieving presentations summary: {str(e)}"
)
)
@LAYOUT_MANAGEMENT_ROUTER.post(
"/templates",
response_model=TemplateCreateResponse,
responses={
400: {"model": ErrorResponse, "description": "Validation error"},
500: {"model": ErrorResponse, "description": "Internal server error"},
},
)
async def create_template(
request: TemplateCreateRequest,
session: AsyncSession = Depends(get_async_session),
):
try:
if not request.id or not request.name:
raise HTTPException(status_code=400, detail="id and name are required")
# Upsert template by id
existing = await session.get(TemplateModel, request.id)
if existing:
existing.name = request.name
existing.description = request.description
else:
session.add(
TemplateModel(
id=request.id, name=request.name, description=request.description
)
)
await session.commit()
# Read back
template = await session.get(TemplateModel, request.id)
return TemplateCreateResponse(
success=True,
template={
"id": template.id,
"name": template.name,
"description": template.description,
"created_at": template.created_at,
},
message="Template saved",
)
except HTTPException:
await session.rollback()
raise
except Exception as e:
await session.rollback()
raise HTTPException(status_code=500, detail=f"Failed to save template: {str(e)}")

View file

@ -14,6 +14,7 @@ from models.sql.ollama_pull_status import OllamaPullStatus
from models.sql.presentation import PresentationModel
from models.sql.slide import SlideModel
from models.sql.presentation_layout_code import PresentationLayoutCodeModel
from models.sql.template import TemplateModel
from utils.get_env import get_app_data_directory_env, get_database_url_env
@ -70,10 +71,11 @@ async def create_db_and_tables():
SlideModel.__table__,
KeyValueSqlModel.__table__,
ImageAsset.__table__,
PresentationLayoutCodeModel.__table__,
],
)
)
PresentationLayoutCodeModel.__table__,
TemplateModel.__table__,
],
)
)
async with container_db_engine.begin() as conn:
await conn.run_sync(

View file

@ -345,7 +345,7 @@ export const LayoutProvider: React.FC<{
const fullDataByGroup = new Map<string, FullDataInfo[]>();
try {
const customGroupResponse = await fetch(
"/api/v1/ppt/layout-management/summary"
"/api/v1/ppt/template-management/summary"
);
const customGroupData = await customGroupResponse.json();
@ -359,7 +359,7 @@ export const LayoutProvider: React.FC<{
}
const presentationId = group.presentation_id;
const customLayoutResponse = await fetch(
`/api/v1/ppt/layout-management/get-layouts/${presentationId}`
`/api/v1/ppt/template-management/get-templates/${presentationId}`
);
const customLayoutsData = await customLayoutResponse.json();
const allLayout = customLayoutsData.layouts;

View file

@ -97,8 +97,7 @@ const FontManager: React.FC<FontManagerProps> = ({
Font Management
</CardTitle>
<p className="text-sm text-gray-600">
Manage fonts across all slides. Upload fonts once and they'll be
available for all slides.
We couldn't load these fonts automatically. Please upload them manually. Make sure naem of the font should be exactly as shown.
</p>
</CardHeader>
<CardContent className="space-y-6">

View file

@ -134,9 +134,16 @@ export const useLayoutSaving = (
}
console.log(reactComponents);
// First create/update the template metadata
await fetch("/api/v1/ppt/template-management/templates", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: presentationId, name: layoutName, description }),
});
// Save the layout components to the app_data/layouts folder
const saveResponse = await fetch(
"/api/v1/ppt/layout-management/save-layouts",
"/api/v1/ppt/template-management/save-templates",
{
method: "POST",
headers: {
@ -144,8 +151,6 @@ export const useLayoutSaving = (
},
body: JSON.stringify({
layouts: reactComponents,
layout_name: layoutName,
description: description,
}),
}
);

View file

@ -20,18 +20,22 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
loading
} = useLayout();
const [summaryMap, setSummaryMap] = React.useState<Record<string, number>>({});
const [summaryMap, setSummaryMap] = React.useState<Record<string, { lastUpdatedAt?: number; name?: string; description?: string }>>({});
useEffect(() => {
// Fetch custom templates summary to get last_updated_at for sorting
fetch("/api/v1/ppt/layout-management/summary")
// Fetch custom templates summary to get last_updated_at and template meta for sorting and display
fetch("/api/v1/ppt/template-management/summary")
.then(res => res.json())
.then(data => {
const map: Record<string, number> = {};
const map: Record<string, { lastUpdatedAt?: number; name?: string; description?: string }> = {};
if (data && Array.isArray(data.presentations)) {
for (const p of data.presentations) {
// groups are named custom-<presentation_id>
map[`custom-${p.presentation_id}`] = p.last_updated_at ? new Date(p.last_updated_at).getTime() : 0;
const slug = `custom-${p.presentation_id}`;
map[slug] = {
lastUpdatedAt: p.last_updated_at ? new Date(p.last_updated_at).getTime() : 0,
name: p.template?.name,
description: p.template?.description,
};
}
}
setSummaryMap(map);
@ -45,10 +49,12 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
const Groups: LayoutGroup[] = groups.map(groupName => {
const settings = getGroupSetting(groupName);
const customMeta = summaryMap[groupName];
const isCustom = groupName.toLowerCase().startsWith("custom-");
return {
id: groupName,
name: groupName,
description: settings?.description || `${groupName} presentation templates`,
name: isCustom && customMeta?.name ? customMeta.name : groupName,
description: (isCustom && customMeta?.description) ? customMeta.description : (settings?.description || `${groupName} presentation templates`),
ordered: settings?.ordered || false,
default: settings?.default || false,
};
@ -60,16 +66,16 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
if (!a.default && b.default) return 1;
return a.name.localeCompare(b.name);
});
}, [getAllGroups, getLayoutsByGroup, getGroupSetting]);
}, [getAllGroups, getLayoutsByGroup, getGroupSetting, summaryMap]);
const inBuiltGroups = React.useMemo(
() => layoutGroups.filter(g => !g.name.toLowerCase().startsWith("custom-")),
() => layoutGroups.filter(g => !g.id.toLowerCase().startsWith("custom-")),
[layoutGroups]
);
const customGroups = React.useMemo(() => {
const unsorted = layoutGroups.filter(g => g.name.toLowerCase().startsWith("custom-"));
// Sort by last_updated_at desc using summaryMap
return unsorted.sort((a, b) => (summaryMap[b.name] || 0) - (summaryMap[a.name] || 0));
const unsorted = layoutGroups.filter(g => g.id.toLowerCase().startsWith("custom-"));
// Sort by last_updated_at desc using summaryMap keyed by slug id
return unsorted.sort((a, b) => (summaryMap[b.id]?.lastUpdatedAt || 0) - (summaryMap[a.id]?.lastUpdatedAt || 0));
}, [layoutGroups, summaryMap]);
// Auto-select first group when groups are loaded

View file

@ -86,7 +86,7 @@ import { useDrawingCanvas } from "../../custom-template/hooks/useDrawingCanvas";
const presentationId = slug.replace('custom-','');
refetch();
router.back();
const response = await fetch(`/api/v1/ppt/layout-management/delete-layouts/${presentationId}`, {
const response = await fetch(`/api/v1/ppt/template-management/delete-templates/${presentationId}`, {
method: "DELETE",
});
if (response.ok) {

View file

@ -7,6 +7,7 @@ import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { ArrowLeft, Home, Trash2 } from "lucide-react";
import { useLayout } from "@/app/(presentation-generator)/context/LayoutContext";
const GroupLayoutPreview = () => {
const params = useParams();
const router = useRouter();
@ -15,6 +16,8 @@ const GroupLayoutPreview = () => {
const { getFullDataByGroup, loading,refetch } = useLayout();
const layoutGroup = getFullDataByGroup(slug);
const [templateMeta, setTemplateMeta] = React.useState<{ name?: string; description?: string } | null>(null);
useEffect(() => {
const existingScript = document.querySelector(
'script[src*="tailwindcss.com"]'
@ -27,6 +30,21 @@ const GroupLayoutPreview = () => {
}
}, [slug]);
useEffect(() => {
// Load template meta for custom groups
if (slug.startsWith("custom-")) {
const presentationId = slug.replace("custom-", "");
fetch(`/api/v1/ppt/template-management/get-templates/${presentationId}`)
.then(res => res.json())
.then(data => {
if (data?.template) {
setTemplateMeta({ name: data.template.name, description: data.template.description });
}
})
.catch(() => setTemplateMeta(null));
}
}, [slug]);
// Handle loading state
if (loading) {
return <LoadingStates type="loading" />;
@ -40,7 +58,7 @@ const GroupLayoutPreview = () => {
const presentationId = slug.replace('custom-','');
refetch();
router.back();
const response = await fetch(`/api/v1/ppt/layout-management/delete-layouts/${presentationId}`, {
const response = await fetch(`/api/v1/ppt/template-management/delete-templates/${presentationId}`, {
method: "DELETE",
});
if (response.ok) {
@ -79,11 +97,10 @@ const GroupLayoutPreview = () => {
<div className="text-center">
<h1 className="text-3xl font-bold text-gray-900 capitalize">
{layoutGroup[0].groupName} Layouts
{templateMeta?.name || layoutGroup[0].groupName} Layouts
</h1>
<p className="text-gray-600 mt-2">
{layoutGroup.length} layout{layoutGroup.length !== 1 ? "s" : ""} {" "}
{layoutGroup[0].groupName}
{layoutGroup.length} layout{layoutGroup.length !== 1 ? "s" : ""} {templateMeta?.description || layoutGroup[0].groupName}
</p>
</div>

View file

@ -75,7 +75,7 @@ export const useGroupLayoutLoader = (
const presentationId = groupSlug.replace("custom-", "");
const customLayoutResponse = await fetch(
`/api/v1/ppt/layout-management/get-layouts/${presentationId}`
`/api/v1/ppt/template-management/get-templates/${presentationId}`
);
if (!customLayoutResponse.ok) {
@ -86,10 +86,11 @@ export const useGroupLayoutLoader = (
const customLayoutsData = await customLayoutResponse.json();
const allLayouts = customLayoutsData.layouts;
const templateMeta = customLayoutsData.template;
const groupLayouts: LayoutInfo[] = [];
const settings: GroupSetting = {
description: `Custom presentation layouts`,
description: templateMeta?.description || `Custom presentation layouts`,
ordered: false,
default: false,
};

View file

@ -1,5 +1,5 @@
"use client";
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import LoadingStates from "./components/LoadingStates";
import { Card } from "@/components/ui/card";
@ -18,6 +18,8 @@ const LayoutPreview = () => {
} = useLayout();
const router = useRouter();
const [summaryMap, setSummaryMap] = useState<Record<string, { lastUpdatedAt?: number; name?: string; description?: string }>>({});
useEffect(() => {
const existingScript = document.querySelector(
'script[src*="tailwindcss.com"]'
@ -30,6 +32,27 @@ const LayoutPreview = () => {
}
}, []);
useEffect(() => {
// Fetch summary to map custom group slug to template meta and last updated time
fetch("/api/v1/ppt/template-management/summary")
.then((res) => res.json())
.then((data) => {
const map: Record<string, { lastUpdatedAt?: number; name?: string; description?: string }> = {};
if (data && Array.isArray(data.presentations)) {
for (const p of data.presentations) {
const slug = `custom-${p.presentation_id}`;
map[slug] = {
lastUpdatedAt: p.last_updated_at ? new Date(p.last_updated_at).getTime() : 0,
name: p.template?.name,
description: p.template?.description,
};
}
}
setSummaryMap(map);
})
.catch(() => setSummaryMap({}));
}, []);
// Transform context data to match expected format
const layoutGroups = getAllGroups().map((groupName) => ({
groupName,
@ -44,6 +67,11 @@ const LayoutPreview = () => {
g.groupName.toLowerCase().startsWith("custom-")
);
// Sort custom groups by last_updated_at desc using summaryMap
const customGroupsSorted = [...customGroups].sort(
(a, b) => (summaryMap[b.groupName]?.lastUpdatedAt || 0) - (summaryMap[a.groupName]?.lastUpdatedAt || 0)
);
// Handle loading state
if (loading) {
return <LoadingStates type="loading" />;
@ -77,41 +105,47 @@ const LayoutPreview = () => {
<div className="max-w-7xl mx-auto px-6 py-6 w-full">
<h2 className="text-xl font-semibold text-gray-900 mb-4">In Built Templates</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{inBuiltGroups.map((group) => (
<Card
key={group.groupName}
className="cursor-pointer hover:shadow-md transition-all duration-200 group"
onClick={() => router.push(`/template-preview/${group.groupName}`)}
>
<div className="p-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold text-gray-900 capitalize group-hover:text-blue-600 transition-colors">
{group.groupName}
</h3>
<div className="flex items-center gap-2">
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
{group.layouts.length}
{inBuiltGroups.map((group) => {
const isCustom = group.groupName.toLowerCase().startsWith("custom-");
const meta = summaryMap[group.groupName];
const displayName = isCustom && meta?.name ? meta.name : group.groupName;
const displayDescription = isCustom && meta?.description ? meta.description : group.settings.description;
return (
<Card
key={group.groupName}
className="cursor-pointer hover:shadow-md transition-all duration-200 group"
onClick={() => router.push(`/template-preview/${group.groupName}`)}
>
<div className="p-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold text-gray-900 capitalize group-hover:text-blue-600 transition-colors">
{displayName}
</h3>
<div className="flex items-center gap-2">
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
{group.layouts.length}
</span>
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
</div>
</div>
<p className="text-sm text-gray-600 mb-4">
{displayDescription}
</p>
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500">
{group.layouts.length} layout
{group.layouts.length !== 1 ? "s" : ""}
</span>
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
{group.settings.default && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
Default
</span>
)}
</div>
</div>
<p className="text-sm text-gray-600 mb-4">
{group.settings.description}
</p>
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500">
{group.layouts.length} layout
{group.layouts.length !== 1 ? "s" : ""}
</span>
{group.settings.default && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
Default
</span>
)}
</div>
</div>
</Card>
))}
</Card>
);
})}
</div>
</div>
</section>
@ -123,37 +157,42 @@ const LayoutPreview = () => {
<h2 className="text-xl font-semibold text-gray-900">Custom AI Templates</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{customGroups.length > 0 ? (
customGroups.map((group) => (
<Card
key={group.groupName}
className="cursor-pointer hover:shadow-md transition-all duration-200 group"
onClick={() => router.push(`/template-preview/${group.groupName}`)}
>
<div className="p-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold text-gray-900 capitalize group-hover:text-blue-600 transition-colors">
{group.groupName}
</h3>
<div className="flex items-center gap-2">
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
{group.layouts.length}
{customGroupsSorted.length > 0 ? (
customGroupsSorted.map((group) => {
const meta = summaryMap[group.groupName];
const displayName = meta?.name ? meta.name : group.groupName;
const displayDescription = meta?.description ? meta.description : group.settings.description;
return (
<Card
key={group.groupName}
className="cursor-pointer hover:shadow-md transition-all duration-200 group"
onClick={() => router.push(`/template-preview/${group.groupName}`)}
>
<div className="p-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold text-gray-900 capitalize group-hover:text-blue-600 transition-colors">
{displayName}
</h3>
<div className="flex items-center gap-2">
<span className="px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs font-medium">
{group.layouts.length}
</span>
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
</div>
</div>
<p className="text-sm text-gray-600 mb-4">
{displayDescription}
</p>
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500">
{group.layouts.length} layout
{group.layouts.length !== 1 ? "s" : ""}
</span>
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
</div>
</div>
<p className="text-sm text-gray-600 mb-4">
{group.settings.description}
</p>
<div className="flex items-center justify-between">
<span className="text-xs text-gray-500">
{group.layouts.length} layout
{group.layouts.length !== 1 ? "s" : ""}
</span>
</div>
</div>
</Card>
))
</Card>
);
})
) : (
<Card
className="cursor-pointer hover:shadow-md transition-all border-blue-500 duration-200 group"