diff --git a/servers/fastapi/chroma/chroma.sqlite3 b/servers/fastapi/chroma/chroma.sqlite3 index 1b3a19a3..60bfc98a 100644 Binary files a/servers/fastapi/chroma/chroma.sqlite3 and b/servers/fastapi/chroma/chroma.sqlite3 differ diff --git a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx index 09a563a0..6ec0e7fd 100644 --- a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx +++ b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx @@ -69,6 +69,7 @@ export interface LayoutContextType { isPreloading: boolean; cacheSize: number; refetch: () => Promise; + getCustomTemplateFonts: (presentationId: string) => string[] | null; } const LayoutContext = createContext(undefined); @@ -129,6 +130,7 @@ export const LayoutProvider: React.FC<{ const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [isPreloading, setIsPreloading] = useState(false); + const [customTemplateFonts, setCustomTemplateFonts] = useState>(new Map()); const dispatch = useDispatch(); const buildData = async (groupedLayoutsData: GroupedLayoutsResponse[]) => { @@ -336,7 +338,6 @@ export const LayoutProvider: React.FC<{ const LoadCustomLayouts = async () => { const layouts: LayoutInfo[] = []; - const layoutsById = new Map(); const layoutsByGroup = new Map>(); const groupSettingsMap = new Map(); @@ -348,9 +349,9 @@ export const LayoutProvider: React.FC<{ "/api/v1/ppt/template-management/summary" ); const customGroupData = await customGroupResponse.json(); - + + const customFonts = new Map(); const customGroup = customGroupData.presentations; - for (const group of customGroup) { const groupName = `custom-${group.presentation_id}`; fullDataByGroup.set(groupName, []); @@ -363,6 +364,9 @@ export const LayoutProvider: React.FC<{ ); const customLayoutsData = await customLayoutResponse.json(); const allLayout = customLayoutsData.layouts; + + + const settings = { description: `Custom presentation layouts`, @@ -402,6 +406,8 @@ export const LayoutProvider: React.FC<{ layoutCache.set(cacheKey, module.default); } + customFonts.set(presentationId, i.fonts); + const originalLayoutId = module.layoutId || i.layout_name.toLowerCase().replace(/layout$/, ""); @@ -447,6 +453,7 @@ export const LayoutProvider: React.FC<{ groupLayouts.push(layout); layouts.push(layout); } + setCustomTemplateFonts(customFonts); // Cache grouped layouts groupedLayouts.set(groupName, groupLayouts); fullDataByGroup.set(groupName, groupFullData); @@ -454,6 +461,7 @@ export const LayoutProvider: React.FC<{ } catch (err: any) { console.error("Compilation error:", err); } + return { layoutsById, @@ -548,6 +556,9 @@ export const LayoutProvider: React.FC<{ const getFullDataByGroup = (groupName: string): FullDataInfo[] => { return layoutData?.fullDataByGroup.get(groupName) || []; }; + const getCustomTemplateFonts = (presentationId: string): string[] | null => { + return customTemplateFonts.get(presentationId) || null; + }; // Load layouts on mount useEffect(() => { @@ -562,6 +573,7 @@ export const LayoutProvider: React.FC<{ getAllGroups, getAllLayouts, getFullDataByGroup, + getCustomTemplateFonts, loading, error, getLayout, diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/HtmlEditor.tsx b/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/HtmlEditor.tsx index 284e7b4a..39152d49 100644 --- a/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/HtmlEditor.tsx +++ b/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/HtmlEditor.tsx @@ -1,7 +1,6 @@ 'use client' import React, { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Save, X, Code } from "lucide-react"; import { ProcessedSlide } from "../../types"; import Editor from 'react-simple-code-editor'; @@ -10,6 +9,7 @@ import 'prismjs/components/prism-clike'; import 'prismjs/components/prism-javascript'; import 'prismjs/components/prism-markup'; import 'prismjs/components/prism-jsx'; +import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from "@/components/ui/sheet"; interface HtmlEditorProps { slide: ProcessedSlide; @@ -42,58 +42,56 @@ export const HtmlEditor: React.FC = ({ }; return ( - - - -
- - HTML Editor -
-
- - - -
-
-
- -
-

Edit the HTML code to customize the slide layout.

- {/* Render code editor */} -
+ { if (!open) handleCancel(); }}> + + + + + + HTML Editor + + + + - setHtmlContent(htmlContent)} - highlight={htmlContent => highlight(htmlContent, languages.jsx!,'jsx')} - padding={10} - id="html-editor" - name="html-editor" - - className="container__editor" - /> -
- - - - +
+
+ setHtmlContent(html)} + highlight={code => highlight(code, languages.jsx!, 'jsx')} + padding={10} + id="html-editor" + name="html-editor" + className="container__editor" + /> +
+
+ + +
+
+ + +
+
+
+ + ); }; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/GroupLayouts.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/GroupLayouts.tsx index 860304bf..eadcaf93 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/GroupLayouts.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/GroupLayouts.tsx @@ -13,8 +13,29 @@ const GroupLayouts: React.FC = ({ onSelectLayoutGroup, selectedLayoutGroup, }) => { - const { getFullDataByGroup } = useLayout(); + const { getFullDataByGroup,getCustomTemplateFonts } = useLayout(); const layoutGroup = getFullDataByGroup(group.id); + const fonts = getCustomTemplateFonts(group.id.split("custom-")[1]); + console.log("fonts here", fonts); +if(fonts){ + const injectFonts = (fontUrls: string[]) => { + fontUrls.forEach((fontUrl) => { + if (!fontUrl) return; + const existingStyle = document.querySelector(`style[data-font-url="${fontUrl}"]`); + if (existingStyle) return; + const fileName = fontUrl.split("/").pop() || "CustomFont"; + const baseName = fileName.replace(/\.[a-zA-Z0-9]+$/, ""); + const fontFamily = baseName.replace(/[^A-Za-z0-9_-]/g, "_"); + const ext = (fileName.split(".").pop() || "ttf").toLowerCase(); + const format = ext === "otf" ? "opentype" : ext === "woff" ? "woff" : ext === "woff2" ? "woff2" : "truetype"; + const style = document.createElement("style"); + style.setAttribute("data-font-url", fontUrl); + style.textContent = `@font-face { font-family: '${fontFamily}'; src: url('${fontUrl}') format('${format}'); font-display: swap; }`; + document.head.appendChild(style); + }); + }; + injectFonts(fonts); +} return (
onSelectLayoutGroup(group)} diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx index ca21df93..57c2a77b 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx @@ -90,6 +90,22 @@ const LayoutSelection: React.FC = ({ }); } }, [layoutGroups, selectedLayoutGroup, onSelectLayoutGroup]); + useEffect(() => { + if (loading) { + return; + } + const existingScript = document.querySelector( + 'script[src*="tailwindcss.com"]' + ); + if (!existingScript) { + const script = document.createElement("script"); + script.src = "https://cdn.tailwindcss.com"; + script.async = true; + document.head.appendChild(script); + } + + }, []); + if (loading) { return ( diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx index 487b2c42..d50a61b8 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx @@ -175,7 +175,11 @@ const SidePanel = ({ ? "bg-[#5141e5] hover:bg-[#4638c7]" : "bg-white hover:bg-white" }`} - onClick={() => setActive("grid")} + onClick={() => { + if (!isStreaming) { + setActive("grid") + } + }} > setActive("list")} + onClick={() =>{ + if(!isStreaming){ + setActive("list") + } + }} > { }, [renderSlideContent, slide, isStreaming]); useEffect(() => { - if (isStreaming || loading) { + if (loading) { return; } if (slide.layout_group.includes("custom")) { diff --git a/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/backup.tsx b/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/backup.tsx index 0d3b6ce7..db76f920 100644 --- a/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/backup.tsx +++ b/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/backup.tsx @@ -397,3 +397,4 @@ import { useDrawingCanvas } from "../../custom-template/hooks/useDrawingCanvas"; }; export default GroupLayoutPreview; + diff --git a/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx b/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx index 4a65b2c3..a5031fe4 100644 --- a/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx +++ b/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx @@ -1,22 +1,85 @@ "use client"; -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { useParams, useRouter } from "next/navigation"; // import { useGroupLayoutLoader } from '../hooks/useGroupLayoutLoader' import LoadingStates from "../components/LoadingStates"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { ArrowLeft, Home, Trash2 } from "lucide-react"; +import { ArrowLeft, Home, Trash2, Code, Save, X, Pencil } from "lucide-react"; import { useLayout } from "@/app/(presentation-generator)/context/LayoutContext"; +import Editor from "react-simple-code-editor"; +import { highlight, languages } from "prismjs"; +import "prismjs/components/prism-clike"; +import "prismjs/components/prism-javascript"; +import "prismjs/components/prism-markup"; +import "prismjs/components/prism-jsx"; +import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from "@/components/ui/sheet"; const GroupLayoutPreview = () => { const params = useParams(); const router = useRouter(); const slug = params.slug as string; - const { getFullDataByGroup, loading,refetch } = useLayout(); + const { getFullDataByGroup, loading, refetch } = useLayout(); const layoutGroup = getFullDataByGroup(slug); - const [templateMeta, setTemplateMeta] = React.useState<{ name?: string; description?: string } | null>(null); + const presentationId = slug.replace("custom-", ""); + const isCustom = slug.includes("custom-"); + + const [editorOpen, setEditorOpen] = useState(false); + const [currentCode, setCurrentCode] = useState(""); + const [currentLayoutName, setCurrentLayoutName] = useState(""); + const [currentLayoutId, setCurrentLayoutId] = useState(""); + const [currentFonts, setCurrentFonts] = useState(undefined); + const [isSaving, setIsSaving] = useState(false); + const [layoutsMap, setLayoutsMap] = useState>({}); + + const injectFonts = (fontUrls: string[]) => { + fontUrls.forEach((fontUrl) => { + if (!fontUrl) return; + const existingStyle = document.querySelector(`style[data-font-url="${fontUrl}"]`); + if (existingStyle) return; + const fileName = fontUrl.split("/").pop() || "CustomFont"; + const baseName = fileName.replace(/\.[a-zA-Z0-9]+$/, ""); + const fontFamily = baseName.replace(/[^A-Za-z0-9_-]/g, "_"); + const ext = (fileName.split(".").pop() || "ttf").toLowerCase(); + const format = ext === "otf" ? "opentype" : ext === "woff" ? "woff" : ext === "woff2" ? "woff2" : "truetype"; + const style = document.createElement("style"); + style.setAttribute("data-font-url", fontUrl); + style.textContent = `@font-face { font-family: '${fontFamily}'; src: url('${fontUrl}') format('${format}'); font-display: swap; }`; + document.head.appendChild(style); + }); + }; + + useEffect(() => { + const loadCustomLayouts = async () => { + if (!isCustom) return; + try { + const res = await fetch(`/api/v1/ppt/layout-management/get-layouts/${presentationId}`); + if (!res.ok) return; + const data = await res.json(); + const map: Record = {}; + for (const l of data.layouts || []) { + map[l.layout_name] = { + layout_id: l.layout_id, + layout_name: l.layout_name, + layout_code: l.layout_code, + fonts: l.fonts, + }; + } + setLayoutsMap(map); + // Inject all fonts used by this custom group's layouts + // const allFonts: string[] = []; + // Object.values(map).forEach((entry) => { + // (entry.fonts || []).forEach((f) => allFonts.push(f)); + // }); + injectFonts(map[0].fonts || []); + } catch (e) { + // noop + } + }; + loadCustomLayouts(); + }, [isCustom, presentationId]); useEffect(() => { const existingScript = document.querySelector( @@ -30,20 +93,15 @@ const GroupLayoutPreview = () => { } }, [slug]); + // Ensure fonts are injected if layoutsMap changes dynamically 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]); + if (!isCustom) return; + const allFonts: string[] = []; + Object.values(layoutsMap).forEach((entry) => { + (entry.fonts || []).forEach((f) => allFonts.push(f)); + }); + if (allFonts.length) injectFonts(allFonts); + }, [layoutsMap, isCustom]); // Handle loading state if (loading) { @@ -65,6 +123,63 @@ const GroupLayoutPreview = () => { router.push("/layout-preview"); } } + + const openEditor = (layoutName: string) => { + const entry = layoutsMap[layoutName]; + if (!entry) return; + setCurrentLayoutName(entry.layout_name); + setCurrentLayoutId(entry.layout_id); + setCurrentCode(entry.layout_code || ""); + setCurrentFonts(entry.fonts); + // Make sure fonts for this layout are loaded before editing + injectFonts(entry.fonts || []); + setEditorOpen(true); + }; + + const handleCancel = () => { + // reset to original code + const entry = layoutsMap[currentLayoutName]; + if (entry) setCurrentCode(entry.layout_code || ""); + setEditorOpen(false); + }; + + const handleSave = async () => { + try { + setIsSaving(true); + const payload = { + layouts: [ + { + presentation_id: presentationId, + layout_id: currentLayoutId, + layout_name: currentLayoutName, + layout_code: currentCode, + fonts: currentFonts, + }, + ], + }; + const res = await fetch(`/api/v1/ppt/layout-management/save-layouts`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + if (!res.ok) return; + // update cache map + setLayoutsMap((prev) => ({ + ...prev, + [currentLayoutName]: { + layout_id: currentLayoutId, + layout_name: currentLayoutName, + layout_code: currentCode, + fonts: currentFonts, + }, + })); + await refetch(); + setEditorOpen(false); + } finally { + setIsSaving(false); + } + }; + return (
{/* Header */} @@ -139,10 +254,22 @@ const GroupLayoutPreview = () => {
-
+
Layout #{index + 1}
+ {isCustom && ( + + )}
@@ -167,6 +294,61 @@ const GroupLayoutPreview = () => { + + {/* Right-side Sheet Editor */} + {isCustom && ( + { if (!open) handleCancel(); }}> + + + + + + HTML Editor + + + +
+
+ setCurrentCode(code)} + highlight={(code) => highlight(code, languages.jsx!, "jsx")} + padding={10} + id="layout-code-editor" + name="layout-code-editor" + className="container__editor" + /> +
+
+ + +
+
+ + +
+
+
+
+
+ )} ); }; diff --git a/servers/nextjs/app/(presentation-generator)/template-preview/hooks/useLayoutLoader.ts b/servers/nextjs/app/(presentation-generator)/template-preview/hooks/useLayoutLoader.ts deleted file mode 100644 index 014e8514..00000000 --- a/servers/nextjs/app/(presentation-generator)/template-preview/hooks/useLayoutLoader.ts +++ /dev/null @@ -1,157 +0,0 @@ -'use client' -import { useState, useEffect } from 'react' - -import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse, GroupSetting } from '../types' -import { toast } from 'sonner' - -interface UseLayoutLoaderReturn { - layoutGroups: LayoutGroup[] - layouts: LayoutInfo[] - loading: boolean - error: string | null - retry: () => void -} - -export const useLayoutLoader = (): UseLayoutLoaderReturn => { - const [layoutGroups, setLayoutGroups] = useState([]) - const [layouts, setLayouts] = useState([]) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - - const loadAllLayouts = async () => { - try { - setLoading(true) - setError(null) - - const response = await fetch('/api/templates') - if (!response.ok) { - toast.error('Error loading templates', { - description: response.statusText, - }) - return - } - const groupedLayoutsData: GroupedLayoutsResponse[] = await response.json() - const loadedGroups: LayoutGroup[] = [] - const allLayouts: LayoutInfo[] = [] - - for (const groupData of groupedLayoutsData) { - const groupLayouts: LayoutInfo[] = [] - - const groupSettings: GroupSetting = groupData.settings ? groupData.settings : { - description: `${groupData.groupName} presentation templates`, - ordered: false, - default: false - } - - for (const fileName of groupData.files) { - try { - const layoutName = fileName.replace('.tsx', '').replace('.ts', '') - const module = await import(`@/presentation-layouts/${groupData.groupName}/${layoutName}`) - - if (!module.default) { - toast.error(`${layoutName} has no default export`, { - description: 'Please ensure the template file exports a default component', - }) - console.warn(`${layoutName} has no default export`) - throw new Error(`${layoutName} has no default export`) - } - - if (!module.Schema) { - toast.error(`${layoutName} is missing required Schema export`, { - description: 'Please ensure the template file exports a Schema', - }) - console.error(`${layoutName} is missing required Schema export`) - throw new Error(`${layoutName} is missing required Schema export`) - } - - const sampleData = module.Schema.parse({}) - const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '') - - const layoutInfo: LayoutInfo = { - name: layoutName, - component: module.default, - schema: module.Schema, - sampleData, - fileName, - groupName: groupData.groupName, - layoutId - } - - groupLayouts.push(layoutInfo) - allLayouts.push(layoutInfo) - - } catch (importError) { - console.error(`Failed to import ${fileName} from ${groupData.groupName}:`, importError) - - try { - const layoutName = fileName.replace('.tsx', '').replace('.ts', '') - const module = await import(`@/presentation-layouts/${groupData.groupName}/${layoutName}`) - - if (module.default && module.Schema) { - const sampleData = module.Schema.parse({}) - const layoutId = module.layoutId || layoutName.toLowerCase().replace(/layout$/, '') - - const layoutInfo: LayoutInfo = { - name: layoutName, - component: module.default, - schema: module.Schema, - sampleData, - fileName, - groupName: groupData.groupName, - layoutId - } - groupLayouts.push(layoutInfo) - allLayouts.push(layoutInfo) - } else { - console.error(`${layoutName} is missing required exports (default component or Schema)`) - } - } catch (altError) { - console.error(`Alternative import also failed for ${fileName} from ${groupData.groupName}:`, altError) - } - } - } - - if (groupLayouts.length > 0) { - loadedGroups.push({ - groupName: groupData.groupName, - layouts: groupLayouts, - settings: groupSettings - }) - } - } - - if (allLayouts.length === 0) { - toast.error('No valid templates found', { - description: 'Make sure your template files export both a default component and a Schema.', - }) - setError('No valid templates found. Make sure your template files export both a default component and a Schema.') - } else { - setLayoutGroups(loadedGroups) - setLayouts(allLayouts) - setError(null) - } - - } catch (error) { - console.error('Error loading templates:', error) - setError(error instanceof Error ? error.message : 'Failed to load templates') - } finally { - setLoading(false) - } - } - - const retry = () => { - loadAllLayouts() - } - - useEffect(() => { - loadAllLayouts() - }, []) - - return { - layoutGroups, - layouts, - loading, - error, - retry - } -} \ No newline at end of file diff --git a/servers/nextjs/app/globals.css b/servers/nextjs/app/globals.css index 4f8e4ba1..5b890cba 100644 --- a/servers/nextjs/app/globals.css +++ b/servers/nextjs/app/globals.css @@ -443,7 +443,7 @@ thead { .container__content_area { tab-size: 4ch; - max-height: 400px; + /* max-height: 600px; */ overflow: auto; margin: 1.67em 0; }