feat: new templates & update template preview, template selection, custom template

This commit is contained in:
shiva raj badu 2026-04-15 21:52:55 +05:45
parent c7860127f2
commit 4af3534c6d
No known key found for this signature in database
44 changed files with 615484 additions and 1358 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,25 @@
{
"_name_or_path": "sentence-transformers/all-MiniLM-L6-v2",
"architectures": [
"BertModel"
],
"attention_probs_dropout_prob": 0.1,
"classifier_dropout": null,
"gradient_checkpointing": false,
"hidden_act": "gelu",
"hidden_dropout_prob": 0.1,
"hidden_size": 384,
"initializer_range": 0.02,
"intermediate_size": 1536,
"layer_norm_eps": 1e-12,
"max_position_embeddings": 512,
"model_type": "bert",
"num_attention_heads": 12,
"num_hidden_layers": 6,
"pad_token_id": 0,
"position_embedding_type": "absolute",
"transformers_version": "4.36.2",
"type_vocab_size": 2,
"use_cache": true,
"vocab_size": 30522
}

View file

@ -0,0 +1,64 @@
{
"added_tokens_decoder": {
"0": {
"content": "[PAD]",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": true
},
"100": {
"content": "[UNK]",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": true
},
"101": {
"content": "[CLS]",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": true
},
"102": {
"content": "[SEP]",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": true
},
"103": {
"content": "[MASK]",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false,
"special": true
}
},
"clean_up_tokenization_spaces": true,
"cls_token": "[CLS]",
"do_basic_tokenize": true,
"do_lower_case": true,
"mask_token": "[MASK]",
"max_length": 128,
"model_max_length": 512,
"never_split": null,
"pad_to_multiple_of": null,
"pad_token": "[PAD]",
"pad_token_type_id": 0,
"padding_side": "right",
"sep_token": "[SEP]",
"stride": 0,
"strip_accents": null,
"tokenize_chinese_chars": true,
"tokenizer_class": "BertTokenizer",
"truncation_side": "right",
"truncation_strategy": "longest_first",
"unk_token": "[UNK]"
}

View file

@ -0,0 +1,37 @@
{
"cls_token": {
"content": "[CLS]",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false
},
"mask_token": {
"content": "[MASK]",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false
},
"pad_token": {
"content": "[PAD]",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false
},
"sep_token": {
"content": "[SEP]",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false
},
"unk_token": {
"content": "[UNK]",
"lstrip": false,
"normalized": false,
"rstrip": false,
"single_word": false
}
}

View file

@ -0,0 +1 @@
{"snapshots/5f1b8cd78bc4fb444dd171e59b18f3a3af89a079/special_tokens_map.json": {"size": 695, "blob_id": "9bbecc17cabbcbd3112c14d6982b51403b264bfa"}, "snapshots/5f1b8cd78bc4fb444dd171e59b18f3a3af89a079/model.onnx": {"size": 90387630, "blob_id": "672677e74ea0ea5bc37e3698bd1c7f5f1550ac8a"}, "snapshots/5f1b8cd78bc4fb444dd171e59b18f3a3af89a079/config.json": {"size": 650, "blob_id": "56c8c186de9040d4fea8daac2ca110f9d412bf04"}, "snapshots/5f1b8cd78bc4fb444dd171e59b18f3a3af89a079/tokenizer.json": {"size": 711661, "blob_id": "c17ed520ed8438736732a54957a69306b8822215"}, "snapshots/5f1b8cd78bc4fb444dd171e59b18f3a3af89a079/tokenizer_config.json": {"size": 1433, "blob_id": "61e23f16c75ff9995b1d2f251d720c6146d21338"}}

View file

@ -0,0 +1 @@
5f1b8cd78bc4fb444dd171e59b18f3a3af89a079

View file

@ -0,0 +1 @@
../../blobs/56c8c186de9040d4fea8daac2ca110f9d412bf04

View file

@ -0,0 +1 @@
../../blobs/bbd7b466f6d58e646fdc2bd5fd67b2f5e93c0b687011bd4548c420f7bd46f0c5

View file

@ -0,0 +1 @@
../../blobs/c17ed520ed8438736732a54957a69306b8822215

View file

@ -0,0 +1 @@
../../blobs/61e23f16c75ff9995b1d2f251d720c6146d21338

View file

@ -1,15 +1,17 @@
import { Plus, Sparkles } from 'lucide-react'
import { useRouter } from 'next/navigation';
import React from 'react'
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
const CreateCustomTemplate = () => {
const router = useRouter();
return (
<div
onClick={() => {
trackEvent(MixpanelEvent.Templates_Build_Template_Clicked);
router.push('/custom-template')
}}
className='w-full rounded-xl border border-[#EDEEEF] cursor-pointer font-syne'>
className='w-full rounded-[22px] border border-[#EDEEEF] cursor-pointer font-syne'>
<div className='relative h-[215px] flex justify-center items-center '>
<img src="/card_bg.svg" alt="" className="absolute top-0 z-[1] left-0 w-full h-full object-cover" />
<div className='w-[36px] h-[36px] relative z-[4] rounded-full bg-[#7A5AF8] flex items-center justify-center'
@ -22,7 +24,7 @@ const CreateCustomTemplate = () => {
</div>
</div>
</div>
<div className='px-5 py-4 bg-white flex items-center gap-4 border-t border-[#EDEEEF]'>
<div className='px-5 py-4 bg-white flex items-center gap-4 overflow-hidden border-t border-[#EDEEEF]'>
<div className='bg-[#7A5AF8] w-[45px] h-[45px] rounded-lg p-2 flex items-center justify-center'>
<Sparkles className='w-6 h-6 text-white' />

View file

@ -2,95 +2,57 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { Card } from "@/components/ui/card";
import { ArrowUpRight, ChevronRight, ExternalLink, Loader2, Plus } from "lucide-react";
import { ArrowUpRight, ChevronRight, Loader2 } from "lucide-react";
import { templates } from "@/app/presentation-templates";
import { TemplateWithData, TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";
import { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";
import {
useCustomTemplateSummaries,
useCustomTemplatePreview,
CustomTemplates,
} from "@/app/hooks/useCustomTemplates";
import { CompiledLayout } from "@/app/hooks/compileLayout";
import CreateCustomTemplate from "./CreateCustomTemplate";
import Link from "next/link";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import {
TemplatePreviewStage,
LayoutsBadge,
InbuiltTemplatePreview,
CustomTemplatePreview,
} from "../../../components/TemplatePreviewComponents";
// Component for rendering custom template card with lazy-loaded previews
export const CustomTemplateCard = React.memo(function CustomTemplateCard({ template }: { template: CustomTemplates }) {
const router = useRouter();
const { previewLayouts, loading, totalLayouts } = useCustomTemplatePreview(`${template.id}`);
const { previewLayouts, loading } = useCustomTemplatePreview(`${template.id}`);
const handleOpen = useCallback(() => {
trackEvent(MixpanelEvent.Templates_Custom_Opened, { template_id: template.id, template_name: template.name });
if (template.id.startsWith('custom-')) {
router.push(`/template-preview/${template.id}`)
router.push(`/template-preview?slug=${template.id}`)
} else {
router.push(`/template-preview/custom-${template.id}`)
router.push(`/template-preview?slug=custom-${template.id}`)
}
}
, [router, template.id]);
}, [router, template.id, template.name]);
return (
<Card
className="cursor-pointer flex flex-col justify-between shadow-none sm:shadow-none relative hover:shadow-lg transition-all duration-200 group overflow-hidden"
className="cursor-pointer flex flex-col shadow-none sm:shadow-none relative hover:shadow-sm transition-all duration-200 group overflow-hidden rounded-[22px] border border-[#E8E9EC] bg-white"
onClick={handleOpen}
>
<img src="/card_bg.svg" alt="" className="absolute top-0 left-0 w-full h-full object-cover" />
<span className="text-xs font-syne absolute top-2 flex gap-1 capitalize items-center left-2 rounded-[100px] px-2.5 py-1 bg-[#3A3A3AF5] text-white font-semibold z-40">
Layouts- {totalLayouts}
</span>
<div className="p-5">
{/* Layout previews */}
<div className="grid grid-cols-2 gap-2">
{loading ? (
// Loading placeholders
[...Array(Math.min(4, template.layoutCount))].map((_, index) => (
<div
key={`${template.id}-loading-${index}`}
className="relative bg-gradient-to-br from-purple-50 to-blue-50 border border-gray-200 overflow-hidden aspect-video rounded flex items-center justify-center"
>
<Loader2 className="w-4 h-4 text-purple-300 animate-spin" />
</div>
))
) : previewLayouts.length > 0 && (
// Actual layout previews
previewLayouts.slice(0, 4).map((layout: CompiledLayout, index: number) => {
const LayoutComponent = layout.component;
return (
<div
key={`${template.id}-preview-${index}`}
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded"
>
<div className="absolute inset-0 bg-transparent z-10" />
<div
className="transform scale-[0.12] origin-top-left"
style={{ width: "833.33%", height: "833.33%" }}
>
<LayoutComponent data={layout.sampleData} />
</div>
</div>
);
})
)}
</div>
</div>
<div className="flex items-center justify-between p-5 bg-white border-t border-[#EDEEEF] relative z-40 ">
<h3 className="text-sm font-bold w-[191px] text-gray-900">
{template.name}
</h3>
<div className="flex items-center gap-2">
<ArrowUpRight className="w-4 h-4 text-gray-400 group-hover:text-purple-600 transition-colors" />
</div>
<TemplatePreviewStage>
<LayoutsBadge count={template.layoutCount} />
<CustomTemplatePreview
previewLayouts={previewLayouts}
loading={loading}
templateId={template.id}
/>
</TemplatePreviewStage>
<div className="relative z-40 flex items-center justify-between border-t border-[#EDEEEF] bg-white px-6 py-5">
<h3 className="max-w-[min(191px,65%)] text-base font-bold text-gray-900">{template.name}</h3>
<ArrowUpRight className="h-4 w-4 shrink-0 text-gray-400 transition-colors group-hover:text-purple-600" />
</div>
</Card>
);
}, (prev, next) => {
// Custom templates may be refetched, producing new object references; compare on fields we render/use.
return (
prev.template.id === next.template.id &&
prev.template.id === next.template.id &&
prev.template.name === next.template.name &&
prev.template.layoutCount === next.template.layoutCount
@ -104,54 +66,24 @@ const InbuiltTemplateCard = React.memo(function InbuiltTemplateCard({
template: TemplateLayoutsWithSettings;
onOpen: (id: string) => void;
}) {
const previewLayouts = useMemo(() => template.layouts.slice(0, 4), [template.layouts]);
const handleOpen = useCallback(() => onOpen(template.id), [onOpen, template.id]);
return (
<Card
key={template.id}
className="cursor-pointer relative sm:shadow-none shadow-none hover:shadow-lg transition-all duration-200 group overflow-hidden"
className="group relative cursor-pointer overflow-hidden rounded-[22px] border border-[#E8E9EC] bg-white shadow-none sm:shadow-none transition-all duration-200 hover:shadow-sm"
onClick={handleOpen}
>
<span className="text-xs font-syne absolute top-2 flex gap-1 capitalize items-center left-2 rounded-[100px] px-2.5 py-1 bg-[#3A3A3AF5] text-white font-semibold z-40">
Layouts- {template.layouts.length}
</span>
<img src="/card_bg.svg" alt="" className="absolute top-0 left-0 w-full h-full object-cover" />
<div className="p-5">
<div className="grid grid-cols-2 gap-2">
{previewLayouts.map((layout: TemplateWithData, index: number) => {
const LayoutComponent = layout.component;
return (
<div
key={`${template.id}-preview-${index}`}
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded"
>
<div className="absolute inset-0 bg-transparent z-10" />
<div
className="transform scale-[0.12] origin-top-left"
style={{ width: "833.33%", height: "833.33%" }}
>
<LayoutComponent data={layout.sampleData} />
</div>
</div>
);
})}
</div>
</div>
<div className="flex items-center justify-between p-5 bg-white border-t border-[#EDEEEF] relative z-40 ">
<div className="w-[191px]">
<h3 className="text-sm font-bold text-gray-900 capitalize">
{template.name}
</h3>
<p className="text-xs text-gray-600 mb-4 line-clamp-2">
{template.description}
</p>
</div>
<div className="flex items-center gap-2">
<ArrowUpRight className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
<TemplatePreviewStage>
<LayoutsBadge count={template.layouts.length} />
<InbuiltTemplatePreview layouts={template.layouts} templateId={template.id} />
</TemplatePreviewStage>
<div className="relative z-40 flex items-center justify-between gap-4 border-t border-[#EDEEEF] bg-white px-6 py-5">
<div className="min-w-0 flex-1">
<h3 className="text-base font-bold capitalize text-gray-900">{template.name}</h3>
<p className="mt-1 line-clamp-2 text-sm text-gray-500">{template.description}</p>
</div>
<ArrowUpRight className="h-4 w-4 shrink-0 text-gray-400 transition-colors group-hover:text-blue-600" />
</div>
</Card>
);
@ -163,6 +95,7 @@ const LayoutPreview = () => {
const { templates: customTemplates, loading: customLoading } = useCustomTemplateSummaries();
useEffect(() => {
trackEvent(MixpanelEvent.Templates_Page_Viewed);
const existingScript = document.querySelector('script[src*="tailwindcss.com"]');
if (!existingScript) {
const script = document.createElement("script");
@ -172,18 +105,20 @@ const LayoutPreview = () => {
}
}, []);
const handleOpenPreview = useCallback((id: string) => router.push(`/template-preview/${id}`), [router]);
const handleOpenPreview = useCallback((id: string) => {
trackEvent(MixpanelEvent.Templates_Inbuilt_Opened, { template_id: id });
router.push(`/template-preview?slug=${id}`);
}, [router]);
const inbuiltTemplateCards = useMemo(
() =>
templates.map((template: TemplateLayoutsWithSettings) => (
<InbuiltTemplateCard key={template.id} template={template} onOpen={handleOpenPreview} />
)),
[handleOpenPreview],
);
const { nonNeoInbuilt, neoInbuilt } = useMemo(() => {
const nonNeo: TemplateLayoutsWithSettings[] = [];
const neo: TemplateLayoutsWithSettings[] = [];
for (const t of templates) {
if (t.id.startsWith("neo")) neo.push(t);
else nonNeo.push(t);
}
return { nonNeoInbuilt: nonNeo, neoInbuilt: neo };
}, []);
const customTemplateCards = useMemo(
() => customTemplates.map((template: CustomTemplates) => <CustomTemplateCard key={template.id} template={template} />),
@ -193,7 +128,7 @@ const LayoutPreview = () => {
return (
<div className="min-h-screen relative font-syne">
<div
className='fixed z-0 bottom-[-16.5rem] left-0 w-full h-full'
className='fixed z-0 -bottom-[16.5rem] left-0 w-full h-full'
style={{
height: "341px",
borderRadius: '1440px',
@ -206,12 +141,9 @@ const LayoutPreview = () => {
Templates
</h3>
<div className="flex gap-2.5 max-sm:w-full max-md:justify-center max-sm:flex-wrap">
<Link
href="/custom-template"
onClick={() => trackEvent(MixpanelEvent.Templates_New_Template_Clicked)}
className="inline-flex items-center font-syne font-semibold gap-2 rounded-xl px-4 py-2.5 text-black text-sm shadow-sm hover:shadow-md"
aria-label="Create new template"
style={{
@ -231,7 +163,7 @@ const LayoutPreview = () => {
<div className="l mx-auto px-6 py-8">
<div className='p-1 rounded-[40px] bg-[#ffffff] w-fit border border-[#EDEEEF] flex items-center justify-center '>
<button className='px-5 py-2 text-xs font-medium text-[#3A3A3A] rounded-[70px]'
onClick={() => setTab('custom')}
onClick={() => { trackEvent(MixpanelEvent.Templates_Tab_Switched, { tab: 'custom' }); setTab('custom'); }}
style={{
background: tab === 'custom' ? '#F4F3FF' : 'transparent',
color: tab === 'custom' ? '#5146E5' : '#3A3A3A'
@ -241,7 +173,7 @@ const LayoutPreview = () => {
<path d="M1 0V16.5" stroke="#EDECEC" strokeWidth="2" />
</svg>
<button className='px-5 py-2 text-xs font-medium text-[#3A3A3A] rounded-[70px]'
onClick={() => setTab('default')}
onClick={() => { trackEvent(MixpanelEvent.Templates_Tab_Switched, { tab: 'default' }); setTab('default'); }}
style={{
background: tab === 'default' ? '#F4F3FF' : 'transparent',
color: tab === 'default' ? '#5146E5' : '#3A3A3A'
@ -249,12 +181,36 @@ const LayoutPreview = () => {
>Built-in</button>
</div>
{/* Inbuilt Templates Section */}
{tab === 'default' && <section className="my-12">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{inbuiltTemplateCards}
</div>
</section>}
{/* Inbuilt Templates Section: non-neo first, then Report (neo) */}
{tab === 'default' && (
<section className="my-12 space-y-12">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{nonNeoInbuilt.map((template) => (
<InbuiltTemplateCard
key={template.id}
template={template}
onOpen={handleOpenPreview}
/>
))}
</div>
{neoInbuilt.length > 0 && (
<div>
<h4 className="text-base font-semibold text-[#101828] mb-6 font-syne tracking-tight">
Report
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{neoInbuilt.map((template) => (
<InbuiltTemplateCard
key={template.id}
template={template}
onOpen={handleOpenPreview}
/>
))}
</div>
</div>
)}
</section>
)}
{tab === 'custom' && <section className="my-12">
@ -264,7 +220,7 @@ const LayoutPreview = () => {
<span className="ml-3 text-gray-600">Loading custom templates...</span>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="grid grid-cols-1 md:grid-cols-2 items-center lg:grid-cols-4 gap-6">
<CreateCustomTemplate />
{customTemplateCards}
</div>

View file

@ -1,58 +1,67 @@
import { Skeleton } from '@/components/ui/skeleton'
import { Card } from '@/components/ui/card'
const Loading = () => {
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto px-6 py-8">
{/* Header */}
<div className="text-center mb-8">
<Skeleton className="h-9 w-48 mx-auto mb-2" />
<Skeleton className="h-5 w-64 mx-auto" />
const TemplateCardSkeleton = () => (
<Card className="overflow-hidden shadow-none sm:shadow-none relative">
<Skeleton className="absolute top-2 left-2 h-6 w-20 rounded-full z-40" />
<img src="/card_bg.svg" alt="" className="absolute top-0 left-0 w-full h-full object-cover" />
<div className="p-5">
<div className="grid grid-cols-2 gap-2">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="aspect-video rounded" />
))}
</div>
</div>
<div className="flex items-center justify-between p-5 bg-white border-t border-[#EDEEEF] relative z-40">
<div className="w-[191px]">
<Skeleton className="h-4 w-28 mb-2" />
<Skeleton className="h-3 w-full mb-1" />
<Skeleton className="h-3 w-3/4" />
</div>
<Skeleton className="h-4 w-4" />
</div>
</Card>
)
{/* Inbuilt Templates Section */}
<section className="mb-12">
<Skeleton className="h-6 w-40 mb-6" />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{Array.from({ length: 4 }).map((_, idx) => (
<Card key={idx} className="overflow-hidden">
<div className="p-5">
<div className="flex items-center justify-between mb-2">
<Skeleton className="h-6 w-28" />
<div className="flex items-center gap-2">
<Skeleton className="h-6 w-8 rounded-full" />
<Skeleton className="h-4 w-4" />
const Loading = () => {
return (
<div className="min-h-screen relative font-syne">
<div
className="fixed z-0 -bottom-[16.5rem] left-0 w-full h-full"
style={{
height: '341px',
borderRadius: '1440px',
background: 'radial-gradient(5.92% 104.69% at 50% 100%, rgba(122, 90, 248, 0.00) 0%, rgba(255, 255, 255, 0.00) 100%), radial-gradient(50% 50% at 50% 50%, rgba(122, 90, 248, 0.80) 0%, rgba(122, 90, 248, 0.00) 100%)',
}}
/>
<div className="sticky top-0 right-0 z-50 py-[28px] px-6 backdrop-blur">
<div className="flex xl:flex-row flex-col gap-6 xl:gap-0 items-center justify-between">
<Skeleton className="h-[34px] w-[180px] rounded-lg" />
<div className="flex gap-2.5 max-sm:w-full max-md:justify-center max-sm:flex-wrap">
<Skeleton className="h-[42px] w-[160px] rounded-[48px]" />
</div>
</div>
<Skeleton className="h-4 w-full mb-1" />
<Skeleton className="h-4 w-3/4 mb-4" />
<div className="grid grid-cols-2 gap-2">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="aspect-video rounded" />
))}
</div>
</div>
</Card>
))}
</div>
</section>
</div>
{/* Custom Templates Section */}
<section>
<div className="flex items-center justify-between mb-6">
<Skeleton className="h-6 w-48" />
<Skeleton className="h-10 w-44 rounded-md" />
</div>
<div className="flex items-center justify-center py-12">
<Skeleton className="h-8 w-8 rounded-full" />
<Skeleton className="h-5 w-48 ml-3" />
</div>
</section>
</div>
</div>
)
<div className="mx-auto px-6 py-8">
<div className="p-1 rounded-[40px] bg-[#ffffff] w-fit border border-[#EDEEEF] flex items-center justify-center">
<Skeleton className="h-8 w-20 rounded-[70px]" />
<svg xmlns="http://www.w3.org/2000/svg" className="mx-1" width="2" height="17" viewBox="0 0 2 17" fill="none">
<path d="M1 0V16.5" stroke="#EDECEC" strokeWidth="2" />
</svg>
<Skeleton className="h-8 w-20 rounded-[70px]" />
</div>
<section className="my-12">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{Array.from({ length: 4 }).map((_, idx) => (
<TemplateCardSkeleton key={idx} />
))}
</div>
</section>
</div>
</div>
)
}
export default Loading

View file

@ -1,94 +1,50 @@
"use client";
import React, { memo } from "react";
import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { CustomTemplates, useCustomTemplatePreview } from "@/app/hooks/useCustomTemplates";
import { Loader2 } from "lucide-react";
import { CompiledLayout } from "@/app/hooks/compileLayout";
import {
TemplatePreviewStage,
LayoutsBadge,
CustomTemplatePreview,
} from "../../components/TemplatePreviewComponents";
// Memoized preview component to prevent re-renders during scroll
export const LayoutPreview = memo(({ layout, templateId, index }: { layout: CompiledLayout, templateId: string, index: number }) => {
const LayoutComponent = layout.component;
return (
<div
key={`${templateId}-preview-${index}`}
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded"
style={{ contain: 'layout style paint', willChange: 'auto' }}
>
<div className="absolute inset-0 bg-transparent z-10" />
<div
className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]"
style={{ transform: 'scale(0.2) translateZ(0)', backfaceVisibility: 'hidden' }}
>
<LayoutComponent data={layout.sampleData} />
</div>
</div>
);
});
LayoutPreview.displayName = 'LayoutPreview';
export const CustomTemplateCard = memo(({ template, onSelectTemplate, selectedTemplate }: { template: CustomTemplates, onSelectTemplate: (template: string) => void, selectedTemplate: string | null }) => {
const { previewLayouts, loading: customLoading, totalLayouts } = useCustomTemplatePreview(template.id);
export const CustomTemplateCard = memo(function CustomTemplateCard({
template,
onSelectTemplate,
selectedTemplate,
}: {
template: CustomTemplates;
onSelectTemplate: (template: string) => void;
selectedTemplate: string | null;
}) {
const { previewLayouts, loading } = useCustomTemplatePreview(template.id);
const isSelected = selectedTemplate === template.id;
return (
<Card
className={`${isSelected ? 'border-2 border-blue-500' : ''} font-syne cursor-pointer flex flex-col justify-between relative hover:shadow-lg transition-all duration-200 group overflow-hidden`}
className={cn(
"font-syne cursor-pointer flex flex-col justify-between relative hover:shadow-sm transition-all duration-200 group overflow-hidden rounded-[22px] bg-white border",
isSelected
? " border-blue-500 ring-2 ring-blue-500/25 shadow-sm"
: " border-[#E8E9EC]"
)}
onClick={() => onSelectTemplate(template.id)}
>
<img src="/card_bg.svg" alt="" className="absolute top-0 left-0 w-full h-full object-cover" />
<span className="text-xs font-syne absolute top-2 flex gap-1 capitalize items-center left-2 rounded-[100px] px-2.5 py-1 bg-[#3A3A3AF5] text-white font-semibold z-40">
Layouts- {totalLayouts}
</span>
<div className="p-5">
{/* Layout previews */}
<div className="grid grid-cols-2 gap-2">
{customLoading ? (
// Loading placeholders
[...Array(Math.min(4, template.layoutCount))].map((_, index) => (
<div
key={`${template.id}-loading-${index}`}
className="relative bg-gradient-to-br from-purple-50 to-blue-50 border border-gray-200 overflow-hidden aspect-video rounded flex items-center justify-center"
>
<Loader2 className="w-4 h-4 text-purple-300 animate-spin" />
</div>
))
) : previewLayouts.length > 0 && (
// Actual layout previews
previewLayouts.slice(0, 4).map((layout: CompiledLayout, index: number) => {
const LayoutComponent = layout.component;
return (
<div
key={`${template.id}-preview-${index}`}
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded"
>
<div className="absolute inset-0 bg-transparent z-10" />
<div
className="transform scale-[0.12] origin-top-left"
style={{ width: "833.33%", height: "833.33%" }}
>
<LayoutComponent data={layout.sampleData} />
</div>
</div>
);
})
)}
</div>
</div>
<div className="flex items-center justify-between p-5 bg-white border-t border-[#EDEEEF] relative z-40 ">
<h3 className="text-sm font-bold text-gray-900">
<TemplatePreviewStage>
<LayoutsBadge count={template.layoutCount} />
<CustomTemplatePreview
previewLayouts={previewLayouts}
loading={loading}
templateId={template.id}
isOutline={true}
/>
</TemplatePreviewStage>
<div className="flex items-center justify-between px-6 py-5 bg-white border-t border-[#EDEEEF] relative z-40">
<h3 className="text-sm font-bold text-gray-900 font-syne">
{template.name}
</h3>
</div>
</Card>
);
});
CustomTemplateCard.displayName = 'CustomTemplateCard';

View file

@ -57,18 +57,7 @@ const OutlineContent: React.FC<OutlineContentProps> = ({
</span>
</div>
)}
{/* <div className="flex items-center justify-between">
<h5 className="text-lg font-medium">
Presentation Outline
</h5>
{isStreaming && (
<div className="flex items-center text-sm text-blue-600">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600 mr-2"></div>
Generating outlines...
</div>
)}
</div> */}
{/* Skeleton loading state */}
{isLoading && (
<div className="space-y-4 bg-white">
{[...Array(6)].map((_, index) => (
@ -93,7 +82,7 @@ const OutlineContent: React.FC<OutlineContentProps> = ({
{/* Outlines content */}
{outlines && outlines.length > 0 && (
<div className="bg-[#F9F8F8] p-7 relative z-20 rounded-[20px]">
<div className="bg-[#F9F8F8] p-7 relative z-20 rounded-[20px] min-h-[calc(100vh-200px)]">
<DndContext
sensors={sensors}
collisionDetection={closestCenter}

View file

@ -146,7 +146,7 @@ export function OutlineItem({
<div id={`outline-item-${index}`} className="flex flex-col basis-full gap-2">
<p className="text-black w-fit text-[10px] font-medium bg-white border border-[#EDEEEF] rounded-[80px] px-2.5">slide {index}</p>
<p className="text-black w-fit text-[10px] font-medium bg-white border border-[#EDEEEF] rounded-[80px] px-2.5">Slide {index}</p>
{/* Editable Markdown Content */}
{isStreaming ? (
isActiveStreaming ? (

View file

@ -37,6 +37,13 @@ const OutlinePage: React.FC = () => {
if (!presentation_id) {
return <EmptyStateView />;
}
const handleTabChange = (tab: string) => {
if (streamState.isStreaming) {
return;
}
setActiveTab(tab);
};
return (
@ -51,9 +58,10 @@ const OutlinePage: React.FC = () => {
<Wrapper className="flex flex-col w-full relative px-5 sm:px-10 lg:px-20 ">
<div className="w-full mx-auto">
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex w-full flex-col">
<Tabs value={activeTab} onValueChange={handleTabChange} className="flex w-full flex-col">
{/* Reserves vertical space so content does not sit under the fixed tab bar */}
<div className="h-[4.75rem] shrink-0 sm:h-[5rem]" aria-hidden />
<div className="fixed top-26 left-0 right-0 z-40 bg-[#FAFAFA] pb-2">
<div className="fixed top-26 left-0 right-0 z-50 pb-2">
<div className="mx-auto w-full max-w-[1440px] px-5 sm:px-10 lg:px-20">
<TabsList className="my-4 h-auto w-fit rounded-full border border-[#EDEEEF] bg-white p-1.5">
<TabsTrigger

View file

@ -4,72 +4,49 @@ import React, { useEffect, useMemo, useCallback, memo } from "react";
import { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";
import { templates } from "@/app/presentation-templates";
import { Card } from "@/components/ui/card";
import { TemplateWithData } from "@/app/presentation-templates/utils";
import { cn } from "@/lib/utils";
import { CustomTemplates, useCustomTemplateSummaries } from "@/app/hooks/useCustomTemplates";
import { Loader2 } from "lucide-react";
import { CustomTemplateCard } from "./CustomTemplateCard";
import CreateCustomTemplate from "../../(dashboard)/templates/components/CreateCustomTemplate";
import { CustomTemplateCard } from "./CustomTemplateCard";
import {
TemplatePreviewStage,
LayoutsBadge,
InbuiltTemplatePreview,
} from "../../components/TemplatePreviewComponents";
// Memoized layout preview for built-in templates
const BuiltInLayoutPreview = memo(({ layout, templateId, index }: {
layout: TemplateWithData;
templateId: string;
index: number;
}) => {
const LayoutComponent = layout.component;
return (
<div
className="relative bg-gray-100 font-syne border border-gray-200 overflow-hidden aspect-video rounded"
style={{ contain: 'layout style paint' }}
>
<div className="absolute inset-0 bg-transparent z-10" />
<div
className="transform scale-[0.12] origin-top-left"
style={{ width: "833.33%", height: "833.33%" }}
>
<LayoutComponent data={layout.sampleData} />
</div>
</div>
);
});
BuiltInLayoutPreview.displayName = 'BuiltInLayoutPreview';
// Memoized built-in template card
const BuiltInTemplateCard = memo(({ template, isSelected, onSelect }: {
const BuiltInTemplateCard = memo(function BuiltInTemplateCard({
template,
isSelected,
onSelect,
}: {
template: TemplateLayoutsWithSettings;
isSelected: boolean;
onSelect: (template: TemplateLayoutsWithSettings) => void;
}) => {
const previewLayouts = useMemo(() => template.layouts.slice(0, 4), [template.layouts]);
}) {
const handleClick = useCallback(() => onSelect(template), [onSelect, template]);
return (
<Card
className={`${isSelected ? 'border-2 border-blue-500' : ''} cursor-pointer relative hover:shadow-lg transition-all duration-200 group overflow-hidden`}
className={cn(
"cursor-pointer relative hover:shadow-sm transition-all duration-200 group overflow-hidden rounded-[22px] bg-white border",
isSelected
? " border-blue-500 ring-2 ring-blue-500/25 shadow-sm"
: " border-[#E8E9EC]"
)}
onClick={handleClick}
>
<span className="text-xs font-syne absolute top-2 flex gap-1 capitalize items-center left-2 rounded-[100px] px-2.5 py-1 bg-[#3A3A3AF5] text-white font-semibold z-40">
Layouts- {template.layouts.length}
</span>
<img src="/card_bg.svg" alt="" className="absolute top-0 left-0 w-full h-full object-cover" />
<div className="p-5">
<div className="grid grid-cols-2 gap-2">
{previewLayouts.map((layout: TemplateWithData, index: number) => (
<BuiltInLayoutPreview
key={`${template.id}-preview-${index}`}
layout={layout}
templateId={template.id}
index={index}
/>
))}
</div>
</div>
<div className="flex items-center justify-between p-5 bg-white border-t border-[#EDEEEF] relative z-40">
<div>
<TemplatePreviewStage>
<LayoutsBadge count={template.layouts.length} />
<InbuiltTemplatePreview layouts={template.layouts} templateId={template.id} isOutline={true} />
</TemplatePreviewStage>
<div className="flex items-center justify-between px-6 py-5 bg-white border-t border-[#EDEEEF] relative z-40">
<div className="min-w-0 flex-1">
<h3 className="text-sm font-bold text-gray-900 capitalize font-syne">
{template.name}
</h3>
<p className="text-xs text-gray-600 line-clamp-2 font-syne">
<p className="text-xs text-gray-600 line-clamp-2 font-syne">
{template.description}
</p>
</div>
@ -77,17 +54,16 @@ const BuiltInTemplateCard = memo(({ template, isSelected, onSelect }: {
</Card>
);
});
BuiltInTemplateCard.displayName = 'BuiltInTemplateCard';
interface TemplateSelectionProps {
selectedTemplate: (TemplateLayoutsWithSettings | string) | null;
onSelectTemplate: (template: TemplateLayoutsWithSettings | string) => void;
}
const TemplateSelection: React.FC<TemplateSelectionProps> = memo(({
const TemplateSelection: React.FC<TemplateSelectionProps> = memo(function TemplateSelection({
selectedTemplate,
onSelectTemplate
}) => {
onSelectTemplate,
}) {
useEffect(() => {
const existingScript = document.querySelector(
'script[src*="tailwindcss.com"]'
@ -102,50 +78,44 @@ const TemplateSelection: React.FC<TemplateSelectionProps> = memo(({
const { templates: customTemplates, loading: customLoading } = useCustomTemplateSummaries();
// Stable callback for custom template selection
const handleCustomSelect = useCallback(
(template: TemplateLayoutsWithSettings | string) => onSelectTemplate(template),
[onSelectTemplate]
);
// Stable callback for built-in template selection
const handleBuiltInSelect = useCallback(
(template: TemplateLayoutsWithSettings) => onSelectTemplate(template),
[onSelectTemplate]
);
// Derive the selected custom template id only when selectedTemplate changes
const selectedCustomId = useMemo(
() => (typeof selectedTemplate === 'string' ? selectedTemplate : null),
() => (typeof selectedTemplate === "string" ? selectedTemplate : null),
[selectedTemplate]
);
// Derive the selected built-in template id only when selectedTemplate changes
const selectedBuiltInId = useMemo(
() => (typeof selectedTemplate !== 'string' ? selectedTemplate?.id ?? null : null),
() => (typeof selectedTemplate !== "string" ? selectedTemplate?.id ?? null : null),
[selectedTemplate]
);
// Memoize the custom templates section
const customTemplateCards = useMemo(() => {
if (customLoading) {
return (
<div className="flex items-center justify-center py-12 font-syne">
<Loader2 className="w-8 h-8 animate-spin text-blue-600 font-syne" />
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
<span className="ml-3 text-gray-600">Loading custom templates...</span>
</div>
);
}
if (customTemplates.length === 0) {
return (
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<CreateCustomTemplate />
</div>
);
}
return (
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{customTemplates.map((template: CustomTemplates) => (
<CustomTemplateCard
key={template.id}
@ -158,7 +128,6 @@ const TemplateSelection: React.FC<TemplateSelectionProps> = memo(({
);
}, [customLoading, customTemplates, handleCustomSelect, selectedCustomId]);
// Memoize the built-in templates list
const builtInTemplateCards = useMemo(
() =>
templates.map((template: TemplateLayoutsWithSettings) => (
@ -174,23 +143,20 @@ const TemplateSelection: React.FC<TemplateSelectionProps> = memo(({
return (
<div className="space-y-[30px] mb-4">
{/* Custom AI Templates */}
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-base font-semibold text-gray-900 font-syne">Custom</h3>
</div>
{customTemplateCards}
</div>
{/* In Built Templates */}
<div>
<h3 className="text-base font-semibold text-gray-900 mb-3 font-syne">In Built</h3>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{builtInTemplateCards}
</div>
</div>
</div>
);
});
TemplateSelection.displayName = 'TemplateSelection';
export default TemplateSelection;

View file

@ -4,7 +4,10 @@ import { toast } from "sonner";
import { setOutlines } from "@/store/slices/presentationGeneration";
import { jsonrepair } from "jsonrepair";
import { RootState } from "@/store/store";
import { getApiUrl } from "@/utils/api";
import { getFastAPIUrl } from "@/utils/api";
const MAX_STREAM_RETRIES = 3;
const STREAM_RETRY_DELAY_MS = 1_000;
@ -22,143 +25,190 @@ export const useOutlineStreaming = (presentationId: string | null) => {
useEffect(() => {
if (!presentationId || outlines.length > 0) return;
let eventSource: EventSource;
let eventSource: EventSource | null = null;
let accumulatedChunks = "";
let retryCount = 0;
let isClosed = false;
let retryTimer: ReturnType<typeof setTimeout> | null = null;
const initializeStream = async () => {
setIsStreaming(true)
setIsLoading(true)
try {
eventSource = new EventSource(
getApiUrl(`/api/v1/ppt/outlines/stream/${presentationId}`)
);
eventSource.addEventListener("response", (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case "chunk":
//
accumulatedChunks += data.chunk;
//
try {
const repairedJson = jsonrepair(accumulatedChunks);
const partialData = JSON.parse(repairedJson);
if (partialData.slides) {
const nextSlides: { content: string }[] = partialData.slides || [];
// Determine which slide index changed to minimize live parsing
try {
const prev = prevSlidesRef.current || [];
let changedIndex: number | null = null;
const maxLen = Math.max(prev.length, nextSlides.length);
for (let i = 0; i < maxLen; i++) {
const prevContent = prev[i]?.content;
const nextContent = nextSlides[i]?.content;
if (nextContent !== prevContent) {
changedIndex = i;
}
}
// Keep active index stable if no change detected; and ensure non-decreasing
const prevActive = activeIndexRef.current;
let nextActive = changedIndex ?? prevActive;
if (nextActive < prevActive) {
nextActive = prevActive;
}
activeIndexRef.current = nextActive;
setActiveSlideIndex(nextActive);
if (nextActive > highestIndexRef.current) {
highestIndexRef.current = nextActive;
setHighestActiveIndex(nextActive);
}
} catch { }
prevSlidesRef.current = nextSlides;
dispatch(setOutlines(nextSlides));
setIsLoading(false)
}
} catch (error) {
// JSON isn't complete yet, continue accumulating
}
break;
case "complete":
try {
const outlinesData: { content: string }[] = data.presentation.outlines.slides;
dispatch(setOutlines(outlinesData));
setIsStreaming(false)
setIsLoading(false)
setActiveSlideIndex(null)
setHighestActiveIndex(-1)
prevSlidesRef.current = outlinesData;
activeIndexRef.current = -1;
highestIndexRef.current = -1;
eventSource.close();
} catch (error) {
console.error("Error parsing accumulated chunks:", error);
toast.error("Failed to parse presentation data");
eventSource.close();
}
accumulatedChunks = "";
break;
case "closing":
setIsStreaming(false)
setIsLoading(false)
setActiveSlideIndex(null)
setHighestActiveIndex(-1)
activeIndexRef.current = -1;
highestIndexRef.current = -1;
eventSource.close();
break;
case "error":
setIsStreaming(false)
setIsLoading(false)
setActiveSlideIndex(null)
setHighestActiveIndex(-1)
activeIndexRef.current = -1;
highestIndexRef.current = -1;
eventSource.close();
toast.error('Error in outline streaming',
{
description: data.detail || 'Failed to connect to the server. Please try again.',
}
);
break;
}
});
eventSource.onerror = () => {
setIsStreaming(false)
setIsLoading(false)
setActiveSlideIndex(null)
setHighestActiveIndex(-1)
activeIndexRef.current = -1;
highestIndexRef.current = -1;
eventSource.close();
toast.error("Failed to connect to the server. Please try again.");
};
} catch (error) {
setIsStreaming(false)
setIsLoading(false)
setActiveSlideIndex(null)
setHighestActiveIndex(-1)
activeIndexRef.current = -1;
highestIndexRef.current = -1;
toast.error("Failed to initialize connection");
}
};
initializeStream();
return () => {
const closeEventSource = () => {
if (eventSource) {
eventSource.close();
eventSource = null;
}
};
const clearRetryTimer = () => {
if (retryTimer) {
clearTimeout(retryTimer);
retryTimer = null;
}
};
const resetStreamingState = () => {
setIsStreaming(false);
setIsLoading(false);
setActiveSlideIndex(null);
setHighestActiveIndex(-1);
activeIndexRef.current = -1;
highestIndexRef.current = -1;
};
const scheduleRetry = (reason: string): boolean => {
if (retryCount >= MAX_STREAM_RETRIES || isClosed) {
return false;
}
retryCount += 1;
const retryDelay = STREAM_RETRY_DELAY_MS * retryCount;
console.warn(
`Outline stream retry ${retryCount}/${MAX_STREAM_RETRIES}: ${reason}`
);
closeEventSource();
clearRetryTimer();
accumulatedChunks = "";
prevSlidesRef.current = [];
activeIndexRef.current = -1;
highestIndexRef.current = -1;
retryTimer = setTimeout(() => {
if (!isClosed) {
openStream();
}
}, retryDelay);
return true;
};
const openStream = () => {
closeEventSource();
eventSource = new EventSource(`${getFastAPIUrl()}/api/v1/ppt/outlines/stream/${presentationId}`);
eventSource.addEventListener("response", (event) => {
let data: any;
try {
data = JSON.parse(event.data);
} catch {
if (!scheduleRetry("invalid SSE payload")) {
resetStreamingState();
toast.error("Failed to parse outline stream response.");
}
return;
}
switch (data.type) {
case "chunk":
accumulatedChunks += data.chunk;
try {
const repairedJson = jsonrepair(accumulatedChunks);
const partialData = JSON.parse(repairedJson);
if (partialData.slides) {
const nextSlides: { content: string }[] = partialData.slides || [];
try {
const prev = prevSlidesRef.current || [];
let changedIndex: number | null = null;
const maxLen = Math.max(prev.length, nextSlides.length);
for (let i = 0; i < maxLen; i++) {
const prevContent = prev[i]?.content;
const nextContent = nextSlides[i]?.content;
if (nextContent !== prevContent) {
changedIndex = i;
}
}
const prevActive = activeIndexRef.current;
let nextActive = changedIndex ?? prevActive;
if (nextActive < prevActive) {
nextActive = prevActive;
}
activeIndexRef.current = nextActive;
setActiveSlideIndex(nextActive);
if (nextActive > highestIndexRef.current) {
highestIndexRef.current = nextActive;
setHighestActiveIndex(nextActive);
}
} catch {}
prevSlidesRef.current = nextSlides;
dispatch(setOutlines(nextSlides));
setIsLoading(false);
}
} catch (error) {
// JSON isn't complete yet, continue accumulating
}
break;
case "complete":
try {
const outlinesData: { content: string }[] =
data.presentation.outlines.slides;
dispatch(setOutlines(outlinesData));
setIsStreaming(false);
setIsLoading(false);
setActiveSlideIndex(null);
setHighestActiveIndex(-1);
prevSlidesRef.current = outlinesData;
activeIndexRef.current = -1;
highestIndexRef.current = -1;
isClosed = true;
closeEventSource();
clearRetryTimer();
retryCount = 0;
} catch (error) {
if (!scheduleRetry("failed to parse complete payload")) {
resetStreamingState();
toast.error("Failed to parse presentation data");
}
}
accumulatedChunks = "";
break;
case "closing":
setIsStreaming(false);
setIsLoading(false);
setActiveSlideIndex(null);
setHighestActiveIndex(-1);
activeIndexRef.current = -1;
highestIndexRef.current = -1;
isClosed = true;
closeEventSource();
clearRetryTimer();
retryCount = 0;
break;
case "error":
if (!scheduleRetry(data.detail || "server returned stream error")) {
resetStreamingState();
closeEventSource();
toast.error("Error in outline streaming", {
description:
data.detail ||
"Failed to connect to the server. Please try again.",
});
}
break;
}
});
eventSource.onerror = () => {
if (!scheduleRetry("connection lost")) {
resetStreamingState();
closeEventSource();
toast.error("Failed to connect to the server. Please try again.");
}
};
};
setIsStreaming(true);
setIsLoading(true);
openStream();
return () => {
isClosed = true;
closeEventSource();
clearRetryTimer();
};
}, [presentationId, dispatch]);
return { isStreaming, isLoading, activeSlideIndex, highestActiveIndex };

View file

@ -57,6 +57,9 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
if (data?.theme) {
applyTheme(data.theme);
}
if (data.fonts) {
useFontLoader(data.fonts);
}
} catch (error) {
setError(true);
toast.error("Failed to load presentation");

View file

@ -61,6 +61,9 @@ export const usePresentationData = (
if (data?.theme) {
applyTheme(data.theme);
}
if (data.fonts) {
useFontLoader(data.fonts);
}
} catch (error) {
setError(true);
toast.error("Failed to load presentation");

View file

@ -1,185 +0,0 @@
'use client';
import { useEffect, useRef, useCallback, useState } from 'react';
import { getHeader } from '@/app/(presentation-generator)/services/api/header';
import { ApiResponseHandler } from '@/app/(presentation-generator)/services/api/api-error-handler';
import { ProcessedSlide } from '@/app/(presentation-generator)/custom-template/types';
import { CustomTemplateLayout } from '@/app/hooks/useCustomTemplates';
import { getApiUrl } from '@/utils/api';
interface LayoutPayload {
layout_id: string;
layout_code: string;
layout_name: string;
}
/** Slide state for template preview: ProcessedSlide plus saved layout code and name */
export type TemplatePreviewSlideState = ProcessedSlide & {
react?: string;
layout_name?: string;
};
interface UseTemplateLayoutsAutoSaveOptions {
templateId: string | null;
layouts: CustomTemplateLayout[];
slideStates: TemplatePreviewSlideState[];
debounceMs?: number;
enabled?: boolean;
}
export type SaveStatus = 'idle' | 'saving' | 'saved' | 'error';
export const useTemplateLayoutsAutoSave = ({
templateId,
layouts,
slideStates,
debounceMs = 2000,
enabled = true,
}: UseTemplateLayoutsAutoSaveOptions) => {
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const lastSavedDataRef = useRef<string>('');
const [saveStatus, setSaveStatus] = useState<SaveStatus>('idle');
const isSavingRef = useRef<boolean>(false);
const [lastSavedAt, setLastSavedAt] = useState<Date | null>(null);
// Build the payload for saving
const buildPayload = useCallback((): LayoutPayload[] => {
const payload: LayoutPayload[] = [];
layouts.forEach((layout, index) => {
const slideState = slideStates[index];
if (slideState?.react && layout.rawLayoutId) {
payload.push({
layout_id: layout.rawLayoutId,
layout_code: slideState.react,
layout_name: slideState.layout_name || `Slide${index + 1}`
});
}
});
return payload;
}, [layouts, slideStates]);
// Save function
const saveLayouts = useCallback(async (payload: LayoutPayload[]) => {
if (!templateId || payload.length === 0 || isSavingRef.current) {
return false;
}
const currentDataString = JSON.stringify(payload);
// Skip if data hasn't changed since last save
if (currentDataString === lastSavedDataRef.current) {
return false;
}
try {
isSavingRef.current = true;
setSaveStatus('saving');
console.log('🔄 Auto-saving template layouts...');
const response = await fetch(getApiUrl('/api/v1/ppt/template/update'), {
method: 'PUT',
headers: getHeader(),
body: JSON.stringify({
id: templateId,
layouts: payload,
}),
});
await ApiResponseHandler.handleResponse(response, 'Failed to auto-save layouts');
// Update last saved data reference
lastSavedDataRef.current = currentDataString;
setLastSavedAt(new Date());
setSaveStatus('saved');
console.log('✅ Auto-save successful');
// Reset to idle after showing "saved" briefly
setTimeout(() => {
setSaveStatus('idle');
}, 2000);
return true;
} catch (error) {
console.error('❌ Auto-save failed:', error);
setSaveStatus('error');
// Reset to idle after showing error briefly
setTimeout(() => {
setSaveStatus('idle');
}, 3000);
return false;
} finally {
isSavingRef.current = false;
}
}, [templateId]);
// Debounced save trigger
const debouncedSave = useCallback(() => {
if (!enabled || !templateId) return;
// Clear existing timeout
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
}
// Set new timeout
saveTimeoutRef.current = setTimeout(() => {
const payload = buildPayload();
if (payload.length > 0) {
saveLayouts(payload);
}
}, debounceMs);
}, [enabled, templateId, buildPayload, saveLayouts, debounceMs]);
// Watch for changes in slideStates
useEffect(() => {
if (!enabled || !templateId || slideStates.length === 0) return;
// Check if any slide is still processing
const hasProcessingSlide = Array.from(slideStates.values()).some(
slide => slide.processing
);
if (hasProcessingSlide) return;
debouncedSave();
// Cleanup timeout on unmount or when dependencies change
return () => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
}
};
}, [slideStates, enabled, templateId, debouncedSave]);
// Manual save function
const saveNow = useCallback(async () => {
// Clear any pending debounced save
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
}
const payload = buildPayload();
return saveLayouts(payload);
}, [buildPayload, saveLayouts]);
// Cleanup on unmount - save any pending changes
useEffect(() => {
return () => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
}
};
}, []);
return {
saveStatus,
lastSavedAt,
saveNow,
};
};

View file

@ -1,307 +0,0 @@
"use client";
import React, { useCallback, useEffect, useState } from "react";
import { useParams, usePathname, useRouter } from "next/navigation";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { ArrowLeft, Home, Loader2, Trash2 } from "lucide-react";
import { MixpanelEvent, trackEvent } from "@/utils/mixpanel";
import TemplateService from "../../services/api/template";
import Header from "../../(dashboard)/dashboard/components/Header";
import { toast } from "sonner";
import { CustomTemplateLayout, useCustomTemplateDetails } from "@/app/hooks/useCustomTemplates";
import { templates as templateGroups, getTemplatesByTemplateName } from "@/app/presentation-templates";
const GroupLayoutPreview = () => {
const params = useParams();
const router = useRouter();
const pathname = usePathname();
const templateParams = params.slug as string;
// Check if this is a custom template
const isCustom = templateParams.startsWith("custom-");
const customTemplateId = isCustom ? templateParams.split("custom-")[1] : null;
// Fetch static templates if not custom
const staticTemplates = !isCustom ? getTemplatesByTemplateName(templateParams) : [];
const staticGroup = !isCustom ? templateGroups.find((g: { id: string }) => g.id === templateParams) : null;
// Fetch custom template details if custom
const {
template: customTemplate,
loading: customLoading,
error: customError,
fonts: customFonts,
} = useCustomTemplateDetails({ id: templateParams?.split("custom-")[1] || "", name: "", description: "" });
useEffect(() => {
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);
}
}, [templateParams]);
const handleDeleteCustomTemplate = async () => {
if (!customTemplateId) return;
const confirmed = window.confirm(
"Are you sure you want to delete this template? This action cannot be undone."
);
if (!confirmed) return;
const success = await TemplateService.deleteCustomTemplate(customTemplateId);
if (success.success) {
toast.success("Template deleted successfully");
router.push("/template-preview");
} else {
toast.error("Failed to delete template");
}
};
// Loading state for custom templates
if (isCustom && (customLoading)) {
return (
<div className="min-h-screen bg-gray-50">
<Header />
<div className="flex items-center justify-center py-24">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
<span className="ml-3 text-gray-600">Compiling templates...</span>
</div>
</div>
);
}
// Error state
if (isCustom && customError) {
return (
<div className="min-h-screen bg-gray-50">
<Header />
<div className="flex flex-col items-center justify-center py-24">
<h2 className="text-2xl font-bold text-red-600 mb-4">Error loading template</h2>
<p className="text-gray-600 mb-4">{customError}</p>
<Button onClick={() => router.push("/template-preview")}>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Templates
</Button>
</div>
</div>
);
}
// Empty state
if (
(!isCustom && (!staticGroup || staticTemplates.length === 0)) ||
(isCustom && (!customTemplate))
) {
return (
<div className="min-h-screen bg-gray-50">
<Header />
<div className="flex flex-col items-center justify-center py-24">
<h2 className="text-2xl font-bold text-gray-900 mb-4">
Template not found
</h2>
<Button onClick={() => router.push("/template-preview")}>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Templates
</Button>
</div>
</div>
);
}
// Determine what to render
const templateName = isCustom ? customTemplate?.template.name || "Custom Template" : staticGroup?.name || "";
const templateDescription = isCustom
? customTemplate?.template.description || ""
: staticGroup?.description || "";
const layoutCount = isCustom
? customTemplate?.layouts.length || 0
: staticTemplates.length;
console.log('compileLayout', customTemplate)
return (
<div className="min-h-screen bg-gray-50">
<Header />
{/* Header */}
<header className="bg-white shadow-sm border-b sticky top-0 z-30">
<div className=" mx-auto px-6 py-6">
<div className="flex items-center justify-between mb-4 max-w-[1440px] mx-auto">
<div className="flex items-center gap-4">
<Button
variant="outline"
size="sm"
onClick={() => {
trackEvent(MixpanelEvent.TemplatePreview_Back_Button_Clicked, { pathname });
router.back();
}}
className="flex items-center gap-2"
>
<ArrowLeft className="w-4 h-4" />
Back
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
trackEvent(MixpanelEvent.TemplatePreview_All_Groups_Button_Clicked, { pathname });
router.push("/template-preview");
}}
className="flex items-center gap-2"
>
<Home className="w-4 h-4" />
All Templates
</Button>
</div>
{isCustom && (
<div className="flex items-center gap-4">
<Button
variant="outline"
size="sm"
onClick={() => {
trackEvent(MixpanelEvent.TemplatePreview_Delete_Templates_Button_Clicked, { pathname });
trackEvent(MixpanelEvent.TemplatePreview_Delete_Templates_API_Call);
handleDeleteCustomTemplate();
}}
className="flex items-center gap-2 border-red-200 text-red-700 hover:bg-red-50"
>
<Trash2 className="w-4 h-4" />
Delete Template
</Button>
</div>
)}
</div>
<div className="text-center">
<div className="flex items-center justify-center gap-2 mb-2">
<h1 className="text-3xl font-bold text-gray-900">{templateName}</h1>
{isCustom && (
<span className="px-2 py-0.5 bg-purple-100 text-purple-700 rounded text-sm">
Custom
</span>
)}
</div>
<p className="text-gray-600">
{layoutCount} layout{layoutCount !== 1 ? "s" : ""} {" "}
{templateDescription}
</p>
</div>
</div>
</header>
{/* Layout Grid - Wrapped in SchemaHighlightProvider for custom templates */}
<main className="mx-auto px-2 py-8" id="presentation-page">
{/* Static Templates */}
{!isCustom && (
<div className="space-y-12 w-[1440px] h-[720px] aspect-video mx-auto">
{staticTemplates.map((template: any, index: number) => {
const LayoutComponent = template.component;
return (
<Card
key={`${templateParams}-${template.layoutId}-${index}`}
id={template.layoutId}
className="overflow-hidden shadow-md"
>
<div className="bg-white px-6 py-4 border-b">
<div className="flex items-center justify-between">
<div>
<h3 className="text-xl font-semibold text-gray-900">
{template.layoutName}
</h3>
<p className="text-sm text-gray-500 mt-1 max-w-2xl">
{template.layoutDescription}
</p>
</div>
<div className="flex items-center gap-3">
<span className="px-3 py-1 bg-gray-100 text-gray-600 rounded text-sm font-mono">
{template.layoutId}
</span>
<span className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium">
#{index + 1}
</span>
</div>
</div>
</div>
<div className="bg-gray-100 p-6 flex justify-center overflow-x-auto">
<div
className="flex-shrink-0"
style={{ width: "1280px", height: "720px" }}
>
<LayoutComponent data={template.sampleData} />
</div>
</div>
</Card>
);
})}
</div>
)}
{/* Custom Templates - with page-level schema editor */}
{isCustom && (
<div className="flex flex-col items-center justify-center w-full gap-10 aspect-video mx-auto">
{/* Slides List */}
{customTemplate && customTemplate.layouts.map((layout: CustomTemplateLayout, index: number) => {
const LayoutComponent = layout.component;
return (
<Card
key={`${templateParams}-${layout.layoutId}-${index}`}
id={layout.layoutId}
className="overflow-hidden shadow-md"
>
<div className="bg-white px-6 py-4 border-b">
<div className="flex items-center justify-between">
<div>
<h3 className="text-xl font-semibold text-gray-900">
{layout.rawLayoutName}
</h3>
<p className="text-sm text-gray-500 mt-1 max-w-2xl">
{layout.layoutDescription}
</p>
</div>
</div>
<div className="flex items-end justify-end ">
<span className="px-3 py-1 bg-gray-100 text-gray-600 rounded text-sm font-mono">
{templateParams}:{layout.layoutId}
</span>
</div>
</div>
<div className="bg-gray-100 p-6 flex justify-center overflow-x-auto">
<div
className="flex-shrink-0"
style={{ width: "1280px", height: "720px" }}
>
<LayoutComponent data={layout.sampleData} />
</div>
</div>
</Card>
);
})}
</div>
)}
</main>
</div>
);
};
export default GroupLayoutPreview;

View file

@ -1,222 +1,15 @@
"use client";
import React, { useEffect } from "react";
import { useRouter } from "next/navigation";
import { Card } from "@/components/ui/card";
import { ExternalLink, Loader2, Plus } from "lucide-react";
import { templates } from "@/app/presentation-templates";
import type { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";
import { TemplateWithData } from "@/app/presentation-templates/utils";
import {
useCustomTemplateSummaries,
useCustomTemplatePreview,
CustomTemplates,
} from "@/app/hooks/useCustomTemplates";
import { CompiledLayout } from "@/app/hooks/compileLayout";
import Header from "../(dashboard)/dashboard/components/Header";
// Component for rendering custom template card with lazy-loaded previews
const CustomTemplateCard = ({ template }: { template: CustomTemplates }) => {
const router = useRouter();
const { previewLayouts, loading, totalLayouts } = useCustomTemplatePreview(template.id);
const handleNavigate = () => {
if (template.id.startsWith('custom-')) {
router.push(`/template-preview/${template.id}`);
} else {
router.push(`/template-preview/custom-${template.id}`);
}
}
import React, { Suspense } from "react";
import { Loader2 } from "lucide-react";
import GroupLayoutPreview from "./components/TemplatePreviewClient";
import "../utils/prism-languages";
const TemplatePreviewPage = () => {
return (
<Card
className="cursor-pointer hover:shadow-lg transition-all duration-200 group overflow-hidden"
onClick={handleNavigate}
>
<div className="p-5">
<div className="flex items-center justify-between mb-2">
<h3 className="text-xl font-bold text-gray-900">
{template.name}
</h3>
<div className="flex items-center gap-2">
<span className="px-2.5 py-0.5 bg-purple-100 text-purple-800 rounded-full text-sm font-medium">
{totalLayouts}
</span>
<ExternalLink className="w-4 h-4 text-gray-400 group-hover:text-purple-600 transition-colors" />
</div>
</div>
{/* Layout previews */}
<div className="grid grid-cols-2 gap-2">
{loading ? (
// Loading placeholders
[...Array(Math.min(4, template.layoutCount))].map((_, index) => (
<div
key={`${template.id}-loading-${index}`}
className="relative bg-gradient-to-br from-purple-50 to-blue-50 border border-gray-200 overflow-hidden aspect-video rounded flex items-center justify-center"
>
<Loader2 className="w-4 h-4 text-purple-300 animate-spin" />
</div>
))
) : previewLayouts.length > 0 ? (
// Actual layout previews
previewLayouts.slice(0, 4).map((layout: CompiledLayout, index: number) => {
const LayoutComponent = layout.component;
return (
<div
key={`${template.id}-preview-${index}`}
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded"
>
<div className="absolute inset-0 bg-transparent z-10" />
<div
className="transform scale-[0.12] origin-top-left"
style={{ width: "833.33%", height: "833.33%" }}
>
<LayoutComponent data={layout.sampleData} />
</div>
</div>
);
})
) : (
// Empty state placeholders
[...Array(Math.min(4, template.layoutCount))].map((_, index) => (
<div
key={`${template.id}-empty-${index}`}
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded flex items-center justify-center"
>
<span className="text-xs text-gray-400">No preview</span>
</div>
))
)}
</div>
</div>
</Card>
<Suspense fallback={<div className="min-h-screen bg-gray-50 flex items-center justify-center"><Loader2 className="w-8 h-8 animate-spin text-blue-600" /></div>}>
<GroupLayoutPreview />
</Suspense>
);
};
const LayoutPreview = () => {
const router = useRouter();
const { templates: customTemplates, loading: customLoading } = useCustomTemplateSummaries();
useEffect(() => {
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);
}
}, []);
const totalStaticLayouts = templates.reduce((acc: number, g: TemplateLayoutsWithSettings) => acc + g.layouts.length, 0);
const totalCustomLayouts = customTemplates.reduce((acc: number, t: CustomTemplates) => acc + t.layoutCount, 0);
return (
<div className="min-h-screen bg-gray-50">
<Header />
<div className="max-w-7xl mx-auto px-6 py-8">
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900">All Templates</h1>
<p className="text-gray-600 mt-2">
{totalStaticLayouts + totalCustomLayouts} layouts across{" "}
{templates.length + customTemplates.length} templates
</p>
</div>
{/* Inbuilt Templates Section */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-800 mb-6">Inbuilt Templates</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{templates.map((template: TemplateLayoutsWithSettings) => {
const previewLayouts = template.layouts.slice(0, 4);
return (
<Card
key={template.id}
className="cursor-pointer hover:shadow-lg transition-all duration-200 group overflow-hidden"
onClick={() => router.push(`/template-preview/${template.id}`)}
>
<div className="p-5">
<div className="flex items-center justify-between mb-2">
<h3 className="text-xl font-bold text-gray-900 capitalize">
{template.name}
</h3>
<div className="flex items-center gap-2">
<span className="px-2.5 py-0.5 bg-blue-100 text-blue-800 rounded-full text-sm font-medium">
{template.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 line-clamp-2">
{template.description}
</p>
<div className="grid grid-cols-2 gap-2">
{previewLayouts.map((layout: TemplateWithData, index: number) => {
const LayoutComponent = layout.component;
return (
<div
key={`${template.id}-preview-${index}`}
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded"
>
<div className="absolute inset-0 bg-transparent z-10" />
<div
className="transform scale-[0.12] origin-top-left"
style={{ width: "833.33%", height: "833.33%" }}
>
<LayoutComponent data={layout.sampleData} />
</div>
</div>
);
})}
</div>
</div>
</Card>
);
})}
</div>
</section>
{/* Custom Templates Section */}
<section>
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold text-gray-800 ">
My Custom Templates
</h2>
<a href="/custom-template" className="text-sm flex font-bold font-inter items-center justify-center gap-2 bg-[#5146E5] text-white px-4 py-2 rounded-md">
<Plus className="w-4 h-4" /> Create new template
</a>
</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 lg:grid-cols-4 gap-6">
{customTemplates.map((template: CustomTemplates) => (
<CustomTemplateCard key={template.id} template={template} />
))}
</div>
)}
</section>
</div>
</div>
);
};
export default LayoutPreview;
export default TemplatePreviewPage;

View file

@ -6,8 +6,8 @@ import {
SelectValue,
} from "@/components/ui/select";
import { LanguageType, PresentationConfig, ToneType, VerbosityType } from "../type";
import { useState } from "react";
import { Check, ChevronsUpDown, GalleryVertical, Languages, SlidersHorizontal } from "lucide-react";
import { useEffect, useState } from "react";
import { Check, ChevronsUp, ChevronsUpDown, ChevronUp, GalleryVertical, Languages, SlidersHorizontal } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Command,
@ -46,15 +46,24 @@ const SLIDE_OPTIONS: SlideOption[] = ["5", "8", "9", "10", "11", "12", "13", "14
const SlideCountSelect: React.FC<{
value: string | null;
onValueChange: (value: string) => void;
}> = ({ value, onValueChange }) => {
open: boolean;
onOpenChange: (open: boolean) => void;
}> = ({ value, onValueChange, open, onOpenChange }) => {
const [customInput, setCustomInput] = useState(
value && !SLIDE_OPTIONS.includes(value as SlideOption) ? value : ""
);
useEffect(() => {
if (value && !SLIDE_OPTIONS.includes(value as SlideOption)) {
setCustomInput(value);
} else {
setCustomInput("");
}
}, [value]);
const sanitizeToPositiveInteger = (raw: string): string => {
const digitsOnly = raw.replace(/\D+/g, "");
if (!digitsOnly) return "";
// Remove leading zeros
const noLeadingZeros = digitsOnly.replace(/^0+/, "");
return noLeadingZeros;
};
@ -63,21 +72,35 @@ const SlideCountSelect: React.FC<{
const sanitized = sanitizeToPositiveInteger(customInput);
if (sanitized && Number(sanitized) > 0) {
onValueChange(sanitized);
onOpenChange(false);
}
};
const displayLabel = value ? `${value} slides` : "Auto slides";
return (
<Select value={value || ""} onValueChange={onValueChange} name="slides">
<SelectTrigger
className="w-[140px] font-instrument_sans font-medium bg-white text-slate-700 hover:bg-slate-50 focus-visible:ring-[#5146E5]/30 flex items-center gap-2 h-10 rounded-xl px-3 ring-1 ring-inset ring-slate-200 shadow-sm"
data-testid="slides-select"
>
<div className="flex items-center gap-2.5"><GalleryVertical className="w-4 h-4" /> <SelectValue placeholder="Select Slides" /></div>
</SelectTrigger>
<SelectContent className="font-instrument_sans">
{/* Sticky custom input at the top */}
<Popover open={open} onOpenChange={onOpenChange}>
<PopoverTrigger asChild>
<button
role="combobox"
name="slides"
data-testid="slides-select"
aria-expanded={open}
className=" overflow-hidden font-syne font-medium text-[#191919] focus-visible:ring-[#5146E5]/30 flex justify-between items-center gap-2 h-[34px] rounded-full px-3.5 ring-1 ring-inset ring-slate-200 shadow-sm"
>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" fill="none">
<path d="M4.0835 12.25H9.91683" stroke="black" strokeLinecap="round" strokeLinejoin="round" />
<path d="M11.6665 1.75H2.33317C1.68884 1.75 1.1665 2.27233 1.1665 2.91667V8.75C1.1665 9.39433 1.68884 9.91667 2.33317 9.91667H11.6665C12.3108 9.91667 12.8332 9.39433 12.8332 8.75V2.91667C12.8332 2.27233 12.3108 1.75 11.6665 1.75Z" stroke="black" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<span className="flex flex-1 items-center gap-2.5">
<span className="text-xs font-medium ">{displayLabel}</span>
</span>
<ChevronUp className="ml-2 h-4 w-4 shrink-0" />
</button>
</PopoverTrigger>
<PopoverContent className="w-[140px] p-0 font-syne" align="end">
<div
className="sticky top-0 z-10 bg-white p-2 border-b"
className="sticky top-0 z-10 bg-white p-2 border-b"
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
@ -107,26 +130,35 @@ const SlideCountSelect: React.FC<{
<span className="text-sm font-medium">slides</span>
</div>
</div>
{/* Hidden item to allow SelectValue to render custom selection */}
{value && !SLIDE_OPTIONS.includes(value as SlideOption) && (
<SelectItem value={value} className="hidden">
{value} slides
</SelectItem>
)}
{SLIDE_OPTIONS.map((option) => (
<SelectItem
key={option}
value={option}
className="font-instrument_sans text-sm font-medium"
role="option"
>
{option} slides
</SelectItem>
))}
</SelectContent>
</Select>
<Command>
<CommandList>
<CommandGroup>
{SLIDE_OPTIONS.map((option) => (
<CommandItem
key={option}
value={`${option} slides`}
role="option"
onSelect={() => {
onValueChange(option);
setCustomInput("");
onOpenChange(false);
}}
className="font-syne text-sm font-medium"
>
<Check
className={cn(
"mr-2 h-4 w-4",
value === option ? "opacity-100" : "opacity-0"
)}
/>
{option} slides
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
@ -141,24 +173,22 @@ const LanguageSelect: React.FC<{
}> = ({ value, onValueChange, open, onOpenChange }) => (
<Popover open={open} onOpenChange={onOpenChange}>
<PopoverTrigger asChild>
<Button
variant="outline"
<button
role="combobox"
name="language"
data-testid="language-select"
aria-expanded={open}
className="w-[180px] flex justify-between items-center gap-2 font-instrument_sans font-semibold overflow-hidden bg-white text-slate-700 h-10 rounded-xl px-3 ring-1 ring-inset ring-slate-200 shadow-sm"
className="w-[125px] flex items-center gap-2 overflow-hidden font-syne font-semibold text-[#191919] h-10 rounded-full px-3.5 ring-1 ring-inset ring-slate-200 shadow-sm"
>
<span className="flex justify-center items-center gap-2.5">
<span className="border border-slate-200 rounded-md p-1">
<Languages className="w-4 h-4" />
</span>
<span className="text-sm font-medium truncate">
<Languages className="w-3.5 h-3.5" />
<span className="w-[40px] text-left">
<span className="text-xs font-medium truncate block">
{value || "Select language"}
</span>
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
<ChevronUp className="ml-2 h-4 w-4 shrink-0" />
</button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0" align="end">
<Command>
@ -200,6 +230,7 @@ export function ConfigurationSelects({
config,
onConfigChange,
}: ConfigurationSelectsProps) {
const [openSlides, setOpenSlides] = useState(false);
const [openLanguage, setOpenLanguage] = useState(false);
const [openAdvanced, setOpenAdvanced] = useState(false);
@ -241,6 +272,8 @@ export function ConfigurationSelects({
<SlideCountSelect
value={config.slides}
onValueChange={(value) => onConfigChange("slides", value)}
open={openSlides}
onOpenChange={setOpenSlides}
/>
<LanguageSelect
value={config.language}
@ -255,7 +288,7 @@ export function ConfigurationSelects({
title="Advanced settings"
type="button"
onClick={() => handleOpenAdvancedChange(true)}
className="ml-auto flex items-center gap-2 text-sm bg-white text-slate-700 hover:bg-slate-50 focus-visible:ring-[#5146E5]/30 h-10 rounded-xl px-3 ring-1 ring-inset ring-slate-200 shadow-sm font-instrument_sans font-medium"
className="ml-auto flex items-center gap-2 text-sm text-slate-700 hover:bg-slate-50 focus-visible:ring-[#5146E5]/30 h-10 rounded-xl px-3 ring-1 ring-inset ring-slate-200 shadow-sm font-instrument_sans font-medium"
data-testid="advanced-settings-button"
>
<SlidersHorizontal className="h-4 w-4" aria-hidden="true" />
@ -263,7 +296,7 @@ export function ConfigurationSelects({
</ToolTip>
<Dialog open={openAdvanced} onOpenChange={handleOpenAdvancedChange}>
<DialogContent className="max-w-2xl font-instrument_sans">
<DialogContent className="max-w-2xl font-syne">
<DialogHeader>
<DialogTitle>Advanced settings</DialogTitle>
</DialogHeader>

View file

@ -1,4 +1,5 @@
import { Textarea } from "@/components/ui/textarea";
import { PencilIcon } from "lucide-react";
import { useState } from "react";
interface PromptInputProps {
@ -15,18 +16,27 @@ export function PromptInput({ value, onChange }: PromptInputProps) {
};
return (
<div className="space-y-2 font-syne">
<div className="relative">
<Textarea
value={value}
rows={5}
onChange={(e) => handleChange(e.target.value)}
placeholder="Tell us about your presentation"
data-testid="prompt-input"
className={`py-4 px-5 border-2 font-medium font-instrument_sans text-base min-h-[150px] max-h-[300px] border-[#5146E5] focus-visible:ring-offset-0 focus-visible:ring-[#5146E5] overflow-y-auto custom_scrollbar `}
/>
</div>
<div className="relative font-syne border border-[#DBDBDB99] rounded-[8px] px-[10px] py-3"
style={{
boxShadow: "0 4px 14px 0 rgba(0, 0, 0, 0.04)",
}}
>
<div className="flex items-center gap-2 mb-1">
<PencilIcon className="w-3.5 h-3.5" />
<p className="text-sm font-normal text-[#333333] font-syne ">Write prompt</p>
</div>
<Textarea
value={value}
autoFocus={true}
rows={4}
onChange={(e) => handleChange(e.target.value)}
placeholder="Start with your idea… well handle the slides"
data-testid="prompt-input"
className={`px-2 py-0 font-medium shadow-none font-syne indent-4 text-base min-h-[120px] max-h-[250px] focus-visible:ring-offset-0 focus-visible:ring-transparent focus-visible:ring-0 border-none overflow-y-auto custom_scrollbar `}
/>
</div>
);
}

View file

@ -1,7 +1,7 @@
'use client'
import React, { ChangeEvent, useEffect, useMemo, useState } from 'react'
import { File, Paperclip, X } from 'lucide-react'
import { File, Paperclip, Plus, X } from 'lucide-react'
import { toast } from 'sonner'
interface SupportingDocProps {
@ -11,39 +11,54 @@ interface SupportingDocProps {
multiple?: boolean
}
const MAX_SUPPORTED_FILES = 8
const PDF_TYPES = ['.pdf']
const TEXT_TYPES = ['.txt']
const POWERPOINT_TYPES = ['.pptx']
const WORD_TYPES = ['.docx']
const WORD_TYPES = ['.doc', '.docx', '.docm', '.odt', '.rtf']
const POWERPOINT_TYPES = ['.ppt', '.pptx', '.pptm', '.odp']
const SPREADSHEET_TYPES = ['.xls', '.xlsx', '.xlsm', '.ods', '.csv', '.tsv']
const IMAGE_TYPES = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp', '.svg']
const ACCEPT_DEFAULT = [
'application/pdf',
'text/plain',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
...PDF_TYPES,
...TEXT_TYPES,
...POWERPOINT_TYPES,
...WORD_TYPES,
].join(',')
const ALLOWED_MIME_PREFIXES: string[] = []
const ALLOWED_MIME_PREFIXES: string[] = ['image/']
const ALLOWED_MIME_TYPES = [
'application/pdf',
'application/x-pdf',
'application/acrobat',
'applications/pdf',
'text/pdf',
'application/vnd.pdf',
'text/plain',
'text/csv',
'application/csv',
'text/tab-separated-values',
'text/tsv',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-word.document.macroenabled.12',
'application/vnd.oasis.opendocument.text',
'application/rtf',
'text/rtf',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.ms-powerpoint.presentation.macroenabled.12',
'application/vnd.oasis.opendocument.presentation',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-excel.sheet.macroenabled.12',
'application/vnd.oasis.opendocument.spreadsheet',
'image/jpeg',
'image/png',
'image/gif',
'image/bmp',
'image/tiff',
'image/webp',
'image/svg+xml',
]
const ALLOWED_EXTENSIONS = [
...PDF_TYPES,
...TEXT_TYPES,
...POWERPOINT_TYPES,
...WORD_TYPES,
...POWERPOINT_TYPES,
...SPREADSHEET_TYPES,
...IMAGE_TYPES,
]
const ACCEPT_DEFAULT = [...ALLOWED_MIME_TYPES, ...ALLOWED_EXTENSIONS].join(',')
const SupportingDoc = ({
files,
@ -75,17 +90,29 @@ const SupportingDoc = ({
const disallowed = filesToReview.filter((file) => !isAllowedFile(file))
if (disallowed.length > 0) {
toast.error('Some files are not supported', {
description: 'Only PDF, TXT, PPTX, and DOCX files are allowed.',
description: 'Supported: Word, PowerPoint, spreadsheets, PDF/TXT, and image files.',
})
}
}
const applyFileLimit = (candidateFiles: File[]) => {
if (candidateFiles.length <= MAX_SUPPORTED_FILES) {
return candidateFiles
}
toast.warning('Maximum file limit reached', {
description: `You can upload up to ${MAX_SUPPORTED_FILES} documents only.`,
})
return candidateFiles.slice(0, MAX_SUPPORTED_FILES)
}
const handleFilesSelected = (e: ChangeEvent<HTMLInputElement>) => {
const selectedFiles = Array.from(e.target.files ?? [])
if (selectedFiles.length === 0) return
const nextFiles = multiple ? [...files, ...selectedFiles] : [selectedFiles[0]]
const allowedFiles = nextFiles.filter(isAllowedFile)
const allowedFiles = applyFileLimit(nextFiles.filter(isAllowedFile))
onFilesChange(allowedFiles)
handleValidate(nextFiles)
@ -105,7 +132,7 @@ const SupportingDoc = ({
if (droppedFiles.length === 0) return
const nextFiles = multiple ? [...files, ...droppedFiles] : [droppedFiles[0]]
const allowedFiles = nextFiles.filter(isAllowedFile)
const allowedFiles = applyFileLimit(nextFiles.filter(isAllowedFile))
onFilesChange(allowedFiles)
handleValidate(nextFiles)
@ -140,9 +167,9 @@ const SupportingDoc = ({
<div className="space-y-2" data-testid="attachments-uploader">
<div className="flex items-center justify-between">
<p className="text-sm text-gray-600 font-syne">
{hasFiles ? `${filteredFiles.length} attachment${filteredFiles.length > 1 ? 's' : ''}` : 'No attachments yet'}
{hasFiles ? `${filteredFiles.length} attachment${filteredFiles.length > 1 ? 's' : ''}` : ''}
</p>
<button
{hasFiles && <button
type="button"
onClick={handleClearFiles}
disabled={!hasFiles}
@ -151,7 +178,7 @@ const SupportingDoc = ({
aria-disabled={!hasFiles}
>
Clear all
</button>
</button>}
</div>
<label
@ -169,10 +196,12 @@ const SupportingDoc = ({
data-testid="file-upload-input"
/>
<div className="flex flex-col items-center gap-2">
<Paperclip className="h-6 w-6 text-[#5146E5]" />
<p className="text-sm font-medium text-gray-800 font-syne">
Drag and drop PDF, TXT, PPTX, DOCX, or <span className="text-[#5146E5]">click to browse</span>
</p>
<div className='w-[42px] h-[42px] flex justify-center items-center rounded-full bg-[#EBE9FE]' >
<div className='w-[22px] h-[22px] rounded-full bg-[#7A5AF8] flex items-center justify-center text-white'>
<Plus className='w-3 h-3' />
</div>
</div>
<p className='text-[#808080] text-sm font-normal'>(Office docs, spreadsheets, images, PDF/TXT)</p>
</div>
</label>
@ -214,7 +243,7 @@ const SupportingDoc = ({
</ul>
{filteredFiles.length !== files.length && (
<p className="mt-2 text-xs text-amber-600 font-syne">
Some files were skipped. Only PDF, TXT, PPTX, and DOCX files are supported.
Some files were skipped. Supported: Word, PowerPoint, spreadsheets, PDF/TXT, and image files.
</p>
)}
</div>

View file

@ -12,7 +12,7 @@
"use client";
import React, { useState } from "react";
import { useRouter, usePathname } from "next/navigation";
import { useDispatch } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { clearOutlines, setPresentationId } from "@/store/slices/presentationGeneration";
import { PromptInput } from "./PromptInput";
import { LanguageType, PresentationConfig, ToneType, VerbosityType } from "../type";
@ -26,6 +26,18 @@ import Wrapper from "@/components/Wrapper";
import { setPptGenUploadState } from "@/store/slices/presentationGenUpload";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { ConfigurationSelects } from "./ConfigurationSelects";
import { RootState } from "@/store/store";
import { ImagesApi } from "../../services/api/images";
import CurrentConfig from "./CurrentConfig";
import { LLMConfig } from "@/types/llm_config";
const STOCK_IMAGE_PROVIDERS = new Set(["pexels", "pixabay"]);
const FILE_TYPE_WORD = new Set([".doc", ".docx", ".docm", ".odt", ".rtf"]);
const FILE_TYPE_PRESENTATION = new Set([".ppt", ".pptx", ".pptm", ".odp"]);
const FILE_TYPE_SPREADSHEET = new Set([".xls", ".xlsx", ".xlsm", ".ods", ".csv", ".tsv"]);
const FILE_TYPE_IMAGE = new Set([".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".webp", ".svg"]);
const FILE_TYPE_PDF = new Set([".pdf"]);
const FILE_TYPE_TEXT = new Set([".txt"]);
// Types for loading state
interface LoadingState {
@ -36,16 +48,60 @@ interface LoadingState {
extra_info?: string;
}
const getFileExtension = (fileName: string): string => {
const index = fileName.lastIndexOf(".");
if (index < 0) return "";
return fileName.slice(index).toLowerCase();
};
const getFileCategory = (file: File): string => {
const extension = getFileExtension(file.name || "");
if (FILE_TYPE_WORD.has(extension)) return "word";
if (FILE_TYPE_PRESENTATION.has(extension)) return "presentation";
if (FILE_TYPE_SPREADSHEET.has(extension)) return "spreadsheet";
if (FILE_TYPE_IMAGE.has(extension) || (file.type || "").startsWith("image/")) return "image";
if (FILE_TYPE_PDF.has(extension) || file.type === "application/pdf") return "pdf";
if (FILE_TYPE_TEXT.has(extension) || file.type === "text/plain") return "text";
return "other";
};
const getSelectedTextModel = (config?: LLMConfig): string => {
if (!config) return "";
switch (config.LLM) {
case "openai":
return config.OPENAI_MODEL || "";
case "google":
return config.GOOGLE_MODEL || "";
case "anthropic":
return config.ANTHROPIC_MODEL || "";
case "ollama":
return config.OLLAMA_MODEL || "";
case "custom":
return config.CUSTOM_MODEL || "";
case "codex":
return config.CODEX_MODEL || "";
default:
return "";
}
};
const getSelectedImageQuality = (config?: LLMConfig): string => {
if (!config) return "";
if (config.IMAGE_PROVIDER === "dall-e-3") return config.DALL_E_3_QUALITY || "";
if (config.IMAGE_PROVIDER === "gpt-image-1.5") return config.GPT_IMAGE_1_5_QUALITY || "";
return "";
};
const UploadPage = () => {
const router = useRouter();
const pathname = usePathname();
const dispatch = useDispatch();
const llmConfig = useSelector((state: RootState) => state.userConfig.llm_config);
// State management
const [files, setFiles] = useState<File[]>([]);
const [config, setConfig] = useState<PresentationConfig>({
slides: "5",
language: LanguageType.English,
slides: null,
language: LanguageType.Auto,
prompt: "",
tone: ToneType.Default,
verbosity: VerbosityType.Standard,
@ -63,13 +119,73 @@ const UploadPage = () => {
extra_info: "",
});
/**
* Updates the presentation configuration
* @param key - Configuration key to update
* @param value - New value for the configuration
*/
const handleConfigChange = (key: keyof PresentationConfig, value: string) => {
setConfig((prev) => ({ ...prev, [key]: value }));
const getUploadSnapshotProps = () => {
const trimmedPrompt = config.prompt.trim();
const trimmedInstructions = (config.instructions || "").trim();
const attachmentCategories = Array.from(new Set(files.map(getFileCategory))).sort();
const imageGenerationEnabled = !llmConfig?.DISABLE_IMAGE_GENERATION;
const parsedSlides =
config.slides && /^\d+$/.test(config.slides) ? Number(config.slides) : null;
return {
pathname,
generation_path: files.length > 0 ? "documents" : "prompt_only",
slides_selected: parsedSlides,
slides_mode: config.slides ? "selected" : "auto",
language: config.language || "",
tone: config.tone,
verbosity: config.verbosity,
include_table_of_contents: !!config.includeTableOfContents,
include_title_slide: !!config.includeTitleSlide,
web_search: !!config.webSearch,
has_prompt: Boolean(trimmedPrompt),
prompt_char_count: trimmedPrompt.length,
prompt_word_count: trimmedPrompt ? trimmedPrompt.split(/\s+/).filter(Boolean).length : 0,
has_instructions: Boolean(trimmedInstructions),
instructions_char_count: trimmedInstructions.length,
has_attachments: files.length > 0,
attachments_count: files.length,
attachment_categories: attachmentCategories.join(","),
text_provider: llmConfig?.LLM || "",
text_model: getSelectedTextModel(llmConfig),
image_generation_enabled: imageGenerationEnabled,
image_provider: imageGenerationEnabled ? (llmConfig?.IMAGE_PROVIDER || "") : "disabled",
image_quality: imageGenerationEnabled ? getSelectedImageQuality(llmConfig) : "",
};
};
const handleConfigChange = (key: keyof PresentationConfig, value: unknown) => {
setConfig((prev) => ({ ...prev, [key]: value } as PresentationConfig));
};
const ensureStockImageProviderReady = async (): Promise<boolean> => {
if (llmConfig?.DISABLE_IMAGE_GENERATION) {
return true;
}
const selectedProvider = (llmConfig?.IMAGE_PROVIDER || "").toLowerCase();
if (!STOCK_IMAGE_PROVIDERS.has(selectedProvider)) {
return true;
}
try {
const providerApiKey =
selectedProvider === "pexels"
? llmConfig?.PEXELS_API_KEY
: llmConfig?.PIXABAY_API_KEY;
await ImagesApi.searchStockImages("business", 1, {
provider: selectedProvider,
apiKey: providerApiKey,
strictApiKey: true,
});
return true;
} catch (error: any) {
toast.error(
error?.message ||
`Unable to reach ${selectedProvider} right now. Please check your API key/settings and try again.`
);
return false;
}
};
/**
@ -77,12 +193,29 @@ const UploadPage = () => {
* @returns boolean indicating if the configuration is valid
*/
const validateConfiguration = (): boolean => {
if (!config.language || !config.slides) {
toast.error("Please select number of Slides & Language");
if (!config.language) {
trackEvent(MixpanelEvent.Upload_Validation_Failed, {
...getUploadSnapshotProps(),
reason: "language_missing",
});
toast.error("Please select language");
return false;
}
if (files.length > 0 && config.language === LanguageType.Auto) {
trackEvent(MixpanelEvent.Upload_Validation_Failed, {
...getUploadSnapshotProps(),
reason: "language_auto_with_documents",
});
toast.error("Please choose a language before processing uploaded documents");
return false;
}
if (!config.prompt.trim() && files.length === 0) {
trackEvent(MixpanelEvent.Upload_Validation_Failed, {
...getUploadSnapshotProps(),
reason: "prompt_or_document_missing",
});
toast.error("No Prompt or Document Provided");
return false;
}
@ -95,6 +228,17 @@ const UploadPage = () => {
const handleGeneratePresentation = async () => {
if (!validateConfiguration()) return;
trackEvent(MixpanelEvent.Upload_GetStarted_Button_Clicked, getUploadSnapshotProps());
const isStockProviderReady = await ensureStockImageProviderReady();
if (!isStockProviderReady) {
trackEvent(MixpanelEvent.Upload_Validation_Failed, {
...getUploadSnapshotProps(),
reason: "stock_image_provider_unreachable",
});
return;
}
try {
const hasUploadedAssets = files.length > 0;
@ -128,11 +272,18 @@ const UploadPage = () => {
documents = uploadResponse;
}
const selectedLanguage = config?.language ?? "";
const promises: Promise<any>[] = [];
if (documents.length > 0) {
trackEvent(MixpanelEvent.Upload_Decompose_Documents_API_Call);
promises.push(PresentationGenerationApi.decomposeDocuments(documents));
promises.push(
PresentationGenerationApi.decomposeDocuments(
documents,
selectedLanguage
)
);
}
const responses = await Promise.all(promises);
dispatch(setPptGenUploadState({
@ -155,13 +306,15 @@ const UploadPage = () => {
duration: 30,
});
const selectedLanguage = config?.language ?? "";
// Use the first available layout group for direct generation
trackEvent(MixpanelEvent.Upload_Create_Presentation_API_Call);
const createResponse = await PresentationGenerationApi.createPresentation({
content: config?.prompt ?? "",
n_slides: config?.slides ? parseInt(config.slides) : null,
n_slides: config?.slides ? parseInt(config.slides, 10) : null,
file_paths: [],
language: config?.language ?? "",
language: selectedLanguage,
tone: config?.tone,
verbosity: config?.verbosity,
instructions: config?.instructions || null,
@ -202,58 +355,48 @@ const UploadPage = () => {
duration={loadingState.duration}
extra_info={loadingState.extra_info}
/>
<div className="rounded-2xl border border-slate-200/70 bg-white/80 shadow-sm backdrop-blur supports-[backdrop-filter]:bg-white/60" >
<div className="flex flex-col gap-4 md:items-center md:flex-row justify-between p-4">
<div >
<h2 className="text-lg font-unbounded tracking-tight text-slate-900 ">Configuration</h2>
<p className="text-sm text-slate-500 font-syne">Choose slides, tone, and language preferences.</p>
</div>
<div className="rounded-2xl " >
<div className="flex flex-col gap-4 md:items-center md:flex-row justify-between px-4 ">
<CurrentConfig />
<ConfigurationSelects
config={config}
onConfigChange={handleConfigChange}
/>
</div>
<div className="border-t border-slate-200/70" />
<div className="p-4 md:p-6">
<h3 className="text-base font-normal font-unbounded text-slate-900 mb-2">Content</h3>
<div className="p-4 ">
<div className="relative">
<PromptInput
value={config.prompt}
onChange={(value) => handleConfigChange("prompt", value)}
data-testid="prompt-input"
/>
</div>
</div>
<div className="border-t border-slate-200/70" />
<div className="p-4 md:p-6">
<h3 className="text-base font-normal font-unbounded text-slate-900 mb-2">Attachments (optional)</h3>
<div className="p-4 ">
<h3 className="text-sm font-normal font-unbounded text-[#333333] mb-2">Attachments (optional)</h3>
<SupportingDoc
files={[...files]}
onFilesChange={setFiles}
data-testid="file-upload-input"
/>
</div>
<div className="border-t border-slate-200/70" />
<div className="p-4 md:p-6">
<div className="p-4">
<Button
onClick={handleGeneratePresentation}
className="w-full rounded-[28px] flex items-center justify-center py-5 bg-[#5141e5] text-white font-syne font-semibold text-lg hover:bg-[#5141e5]/85 focus-visible:ring-2 focus-visible:ring-[#5141e5]/40"
data-testid="next-button"
style={{
background: "linear-gradient(270deg, #D5CAFC 2.4%, #E3D2EB 27.88%, #F4DCD3 69.23%, #FDE4C2 100%)"
}}
className="w-fit mr-0 ml-auto rounded-[28px] flex items-center justify-center py-5 px-4 text-[#101323] font-syne font-semibold text-xs "
>
<span>Generate Presentation</span>
<ChevronRight className="!w-5 !h-5 ml-1.5" />
<span>Get Started</span>
<ChevronRight className="!w-5 !h-5 " />
</Button>
</div>
</div>
</Wrapper>
);
};
export default UploadPage;
export default UploadPage;

View file

@ -46,11 +46,24 @@ const page = () => {
<div className="relative">
<Header />
<div className="flex flex-col items-center justify-center mb-8">
<h1 className="text-[64px] font-normal font-unbounded text-[#101323] ">
AI Presentation
<h1 className="text-[64px] relative leading-[112%] font-semibold font-syne text-[#101323] ">
Generate
<svg className="absolute top-[-4rem] left-[-5rem]" xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 13 13" fill="none">
<path d="M9.73497 5.85272C8.05237 5.69492 6.72098 4.39958 6.55904 2.76316L6.28582 0L6.0126 2.76316C5.85066 4.39985 4.51927 5.6952 2.83667 5.85272L0 6.11849L2.83667 6.38426C4.51927 6.54206 5.85066 7.8374 6.0126 9.47382L6.28582 12.237L6.55904 9.47382C6.72098 7.83713 8.05237 6.54178 9.73497 6.38426L12.5716 6.11849L9.73497 5.85272Z" fill="#09CCFE" />
</svg>
<svg className="absolute top-[-2rem] left-[-1rem]" xmlns="http://www.w3.org/2000/svg" width="26" height="25" viewBox="0 0 26 25" fill="none">
<path d="M19.4699 11.7054C16.1047 11.3898 13.442 8.79915 13.1181 5.52632L12.5716 0L12.0252 5.52632C11.7013 8.79971 9.03854 11.3904 5.67335 11.7054L0 12.237L5.67335 12.7685C9.03854 13.0841 11.7013 15.6748 12.0252 18.9476L12.5716 24.474L13.1181 18.9476C13.442 15.6743 16.1047 13.0836 19.4699 12.7685L25.1433 12.237L19.4699 11.7054Z" fill="#09CCFE" />
</svg>
<svg className="absolute bottom-0 -right-10" xmlns="http://www.w3.org/2000/svg" width="41" height="41" viewBox="0 0 41 41" fill="none">
<path d="M31.6166 19.8734C26.275 19.3587 22.0484 15.134 21.5343 9.797L20.6669 0.785156L19.7995 9.797C19.2854 15.1349 15.0588 19.3596 9.71723 19.8734L0.711914 20.7401L9.71723 21.6069C15.0588 22.1216 19.2854 26.3462 19.7995 31.6833L20.6669 40.6951L21.5343 31.6833C22.0484 26.3453 26.275 22.1207 31.6166 21.6069L40.6219 20.7401L31.6166 19.8734Z" fill="#DF92FC" />
</svg>
</h1>
<p className="text-xl font-syne text-[#101323CC]">Choose a design, set preferences, and generate polished slides.</p>
<p className="text-xl font-syne text-[#101323CC]">Turn prompts or documents into presentations with AI</p>
</div>
{/* stars */}
<UploadPage />
</div>

View file

@ -15,7 +15,7 @@ export enum ThemeType {
export enum LanguageType {
// Major World Languages
// Auto = "Auto",
Auto = "Auto",
English = "English",
Spanish = "Spanish (Español)",
French = "French (Français)",

View file

@ -3,6 +3,63 @@ import { TemplateWithData, TemplateGroupSettings, createTemplateEntry, TemplateL
// TODO: Step 1: Import All templates Layouts Here (like the ones below)
// Code templates
import CodeSlide01RoadmapCover, { Schema as CodeRoadmapCoverSchema, slideLayoutId as CodeRoadmapCoverId, slideLayoutName as CodeRoadmapCoverName, slideLayoutDescription as CodeRoadmapCoverDesc } from "./Code/CoverSlide";
import CodeSlide02CodeExplanationSplit, { Schema as CodeExplanationSplitSchema, slideLayoutId as CodeExplanationSplitId, slideLayoutName as CodeExplanationSplitName, slideLayoutDescription as CodeExplanationSplitDesc } from "./Code/CodeExplanationSplitSlide";
import CodeSlide03ApiRequestResponse, { Schema as CodeApiRequestResponseSchema, slideLayoutId as CodeApiRequestResponseId, slideLayoutName as CodeApiRequestResponseName, slideLayoutDescription as CodeApiRequestResponseDesc } from "./Code/APIRequestResponseSlide";
import CodeSlide04FeatureGrid, { Schema as CodeFeatureGridSchema, slideLayoutId as CodeFeatureGridId, slideLayoutName as CodeFeatureGridName, slideLayoutDescription as CodeFeatureGridDesc } from "./Code/CardsGridSlide";
import CodeSlide05ComparisonTable, { Schema as CodeComparisonTableSchema, slideLayoutId as CodeComparisonTableId, slideLayoutName as CodeComparisonTableName, slideLayoutDescription as CodeComparisonTableDesc } from "./Code/TableSlide";
import CodeSlide06Workflow, { Schema as CodeWorkflowSchema, slideLayoutId as CodeWorkflowId, slideLayoutName as CodeWorkflowName, slideLayoutDescription as CodeWorkflowDesc } from "./Code/WorkflowSlide";
import CodeSlide07UseCaseList, { Schema as CodeUseCaseListSchema, slideLayoutId as CodeUseCaseListId, slideLayoutName as CodeUseCaseListName, slideLayoutDescription as CodeUseCaseListDesc } from "./Code/TwoColumnBulletListSlide";
import CodeSlide08CodeExplanationText, { Schema as CodeExplanationTextSchema, slideLayoutId as CodeExplanationTextId, slideLayoutName as CodeExplanationTextName, slideLayoutDescription as CodeExplanationTextDesc } from "./Code/DescriptionTextSlide";
import CodeSlide09TableOfContent, { Schema as CodeTableOfContentSchema, slideLayoutId as CodeTableOfContentId, slideLayoutName as CodeTableOfContentName, slideLayoutDescription as CodeTableOfContentDesc } from "./Code/TableOfContentSlide";
import CodeSlide10MetricsSplit, { Schema as CodeMetricsSplitSchema, slideLayoutId as CodeMetricsSplitId, slideLayoutName as CodeMetricsSplitName, slideLayoutDescription as CodeMetricsSplitDesc } from "./Code/DescriptionAndMetricsSlide";
import CodeSlide11MetricsGrid, { Schema as CodeMetricsGridSchema, slideLayoutId as CodeMetricsGridId, slideLayoutName as CodeMetricsGridName, slideLayoutDescription as CodeMetricsGridDesc } from "./Code/MetricsGridSlide";
// Education templates
import EducationCoverSlide, { Schema as EduCoverSchema, slideLayoutId as EduCoverId, slideLayoutName as EduCoverName, slideLayoutDescription as EduCoverDesc } from "./Education/EducationCoverSlide";
import EducationTableOfContentsSlide, { Schema as EduTocSchema, slideLayoutId as EduTocId, slideLayoutName as EduTocName, slideLayoutDescription as EduTocDesc } from "./Education/EducationTableOfContentsSlide";
import EducationAboutSlide, { Schema as EduAboutSchema, slideLayoutId as EduAboutId, slideLayoutName as EduAboutName, slideLayoutDescription as EduAboutDesc } from "./Education/EducationAboutSlide";
import EducationContentSplitSlide, { Schema as EduContentSplitSchema, slideLayoutId as EduContentSplitId, slideLayoutName as EduContentSplitName, slideLayoutDescription as EduContentSplitDesc } from "./Education/EducationContentSplitSlide";
import EducationImageGallerySlide, { Schema as EduImageGallerySchema, slideLayoutId as EduImageGalleryId, slideLayoutName as EduImageGalleryName, slideLayoutDescription as EduImageGalleryDesc } from "./Education/EducationImageGallerySlide";
import EducationReportDonutSlide, { Schema as EduReportDonutSchema, slideLayoutId as EduReportDonutId, slideLayoutName as EduReportDonutName, slideLayoutDescription as EduReportDonutDesc } from "./Education/EducationReportChartSlide";
import EducationServicesSplitSlide, { Schema as EduServicesSplitSchema, slideLayoutId as EduServicesSplitId, slideLayoutName as EduServicesSplitName, slideLayoutDescription as EduServicesSplitDesc } from "./Education/EducationServicesSplitSlide";
import EducationStatisticsGridSlide, { Schema as EduStatisticsGridSchema, slideLayoutId as EduStatisticsGridId, slideLayoutName as EduStatisticsGridName, slideLayoutDescription as EduStatisticsGridDesc } from "./Education/EducationStatisticsGridSlide";
import EducationTimelineSlide, { Schema as EduTimelineSchema, slideLayoutId as EduTimelineId, slideLayoutName as EduTimelineName, slideLayoutDescription as EduTimelineDesc } from "./Education/EducationTimelineSlide";
// Product Overview templates
import BusinessChallengesCardsSlide, { Schema as PoBizChallengesCardsSchema, slideLayoutId as PoBizChallengesCardsId, slideLayoutName as PoBizChallengesCardsName, slideLayoutDescription as PoBizChallengesCardsDesc } from "./ProductOverview/BusinessChallengesCardsSlide";
import BusinessChallengesGridSlide, { Schema as PoBizChallengesGridSchema, slideLayoutId as PoBizChallengesGridId, slideLayoutName as PoBizChallengesGridName, slideLayoutDescription as PoBizChallengesGridDesc } from "./ProductOverview/BusinessChallengesGridSlide";
import ComparisonChartSlide, { Schema as PoComparisonChartSchema, slideLayoutId as PoComparisonChartId, slideLayoutName as PoComparisonChartName, slideLayoutDescription as PoComparisonChartDesc } from "./ProductOverview/ComparisonChartSlide";
import ComparisonTableWithTextSlide, { Schema as PoComparisonTableSchema, slideLayoutId as PoComparisonTableId, slideLayoutName as PoComparisonTableName, slideLayoutDescription as PoComparisonTableDesc } from "./ProductOverview/ComparisonTableWithTextSlide";
import CoverSlide, { Schema as PoCoverSchema, slideLayoutId as PoCoverId, slideLayoutName as PoCoverName, slideLayoutDescription as PoCoverDesc } from "./ProductOverview/CoverSlide";
import ImageGallerySlide, { Schema as PoImageGallerySchema, slideLayoutId as PoImageGalleryId, slideLayoutName as PoImageGalleryName, slideLayoutDescription as PoImageGalleryDesc } from "./ProductOverview/ImageGallerySlide";
import IntroductionSlide, { Schema as PoIntroductionSchema, slideLayoutId as PoIntroductionId, slideLayoutName as PoIntroductionName, slideLayoutDescription as PoIntroductionDesc } from "./ProductOverview/IntroductionSlide";
import KpiCardsSlide, { Schema as PoKpiCardsSchema, slideLayoutId as PoKpiCardsId, slideLayoutName as PoKpiCardsName, slideLayoutDescription as PoKpiCardsDesc } from "./ProductOverview/KpiCardsSlide";
// import MarketOpportunitySlide, { Schema as PoMarketOpportunitySchema, slideLayoutId as PoMarketOpportunityId, slideLayoutName as PoMarketOpportunityName, slideLayoutDescription as PoMarketOpportunityDesc } from "./ProductOverview/MarketOpportunitySlide";
import MeetTeamSlide, { Schema as PoMeetTeamSchema, slideLayoutId as PoMeetTeamId, slideLayoutName as PoMeetTeamName, slideLayoutDescription as PoMeetTeamDesc } from "./ProductOverview/MeetTeamSlide";
import MissionVisionSlide, { Schema as PoMissionVisionSchema, slideLayoutId as PoMissionVisionId, slideLayoutName as PoMissionVisionName, slideLayoutDescription as PoMissionVisionDesc } from "./ProductOverview/MissionVisionSlide";
import OurServicesSlide, { Schema as PoOurServicesSchema, slideLayoutId as PoOurServicesId, slideLayoutName as PoOurServicesName, slideLayoutDescription as PoOurServicesDesc } from "./ProductOverview/OurServicesSlide";
import PricingPlanSlide, { Schema as PoPricingPlanSchema, slideLayoutId as PoPricingPlanId, slideLayoutName as PoPricingPlanName, slideLayoutDescription as PoPricingPlanDesc } from "./ProductOverview/PricingPlanSlide";
import ProcessSlide, { Schema as PoProcessSchema, slideLayoutId as PoProcessId, slideLayoutName as PoProcessName, slideLayoutDescription as PoProcessDesc } from "./ProductOverview/ProcessSlide";
import ReportSnapshotSlide, { Schema as PoReportSnapshotSchema, slideLayoutId as PoReportSnapshotId, slideLayoutName as PoReportSnapshotName, slideLayoutDescription as PoReportSnapshotDesc } from "./ProductOverview/ReportSnapshotSlide";
import TableOfContentSlide, { Schema as PoTableOfContentSchema, slideLayoutId as PoTableOfContentId, slideLayoutName as PoTableOfContentName, slideLayoutDescription as PoTableOfContentDesc } from "./ProductOverview/TableOfContentSlide";
// Report templates
import ReportIntroSlide, { Schema as RepIntroSchema, slideLayoutId as RepIntroId, slideLayoutName as RepIntroName, slideLayoutDescription as RepIntroDesc } from "./Report/IntroCoverSlide";
import TitleDescriptionImageSlide, { Schema as RepIntroductionImageSchema, slideLayoutId as RepIntroductionImageId, slideLayoutName as RepIntroductionImageName, slideLayoutDescription as RepIntroductionImageDesc } from "./Report/TitleDescriptionImageSlide";
import IntroductionStatsSlide, { Schema as RepIntroductionStatsSchema, slideLayoutId as RepIntroductionStatsId, slideLayoutName as RepIntroductionStatsName, slideLayoutDescription as RepIntroductionStatsDesc } from "./Report/MetricsSlide";
import SolutionSlide, { Schema as RepSolutionSchema, slideLayoutId as RepSolutionId, slideLayoutName as RepSolutionName, slideLayoutDescription as RepSolutionDesc } from "./Report/TitleImageBulletCardsSlide";
import MilestoneSlide, { Schema as RepMilestoneSchema, slideLayoutId as RepMilestoneId, slideLayoutName as RepMilestoneName, slideLayoutDescription as RepMilestoneDesc } from "./Report/MilestoneSlide";
import DataAnalysisListSlide, { Schema as RepDataAnalysisListSchema, slideLayoutId as RepDataAnalysisListId, slideLayoutName as RepDataAnalysisListName, slideLayoutDescription as RepDataAnalysisListDesc } from "./Report/BulletListWithIconTitleDescriptionSlide";
import DataAnalysisBarSlide, { Schema as RepDataAnalysisBarSchema, slideLayoutId as RepDataAnalysisBarId, slideLayoutName as RepDataAnalysisBarName, slideLayoutDescription as RepDataAnalysisBarDesc } from "./Report/BarChartWithBulletListWithTitleDescriptionIconSlide";
import DataAnalysisInsightBarSlide, { Schema as RepDataAnalysisInsightBarSchema, slideLayoutId as RepDataAnalysisInsightBarId, slideLayoutName as RepDataAnalysisInsightBarName, slideLayoutDescription as RepDataAnalysisInsightBarDesc } from "./Report/TitleDescriptionChartSlide";
import DataAnalysisLineStatsSlide, { Schema as RepDataAnalysisLineStatsSchema, slideLayoutId as RepDataAnalysisLineStatsId, slideLayoutName as RepDataAnalysisLineStatsName, slideLayoutDescription as RepDataAnalysisLineStatsDesc } from "./Report/TitleChartWithMetricsCardsSlide";
import DataAnalysisDashboardSlide, { Schema as RepDataAnalysisDashboardSchema, slideLayoutId as RepDataAnalysisDashboardId, slideLayoutName as RepDataAnalysisDashboardName, slideLayoutDescription as RepDataAnalysisDashboardDesc } from "./Report/DataAnalysisDashboardSlide";
import PerformanceSnapshotSlide, { Schema as RepPerformanceSnapshotSchema, slideLayoutId as RepPerformanceSnapshotId, slideLayoutName as RepPerformanceSnapshotName, slideLayoutDescription as RepPerformanceSnapshotDesc } from "./Report/TitleMetricsSlide";
import ReportServicesSlide, { Schema as RepServicesSchema, slideLayoutId as RepServicesId, slideLayoutName as RepServicesName, slideLayoutDescription as RepServicesDesc } from "./Report/TitleWorkflowWithTitleDescriptionSlide";
import ReportTeamSlide, { Schema as RepTeamSchema, slideLayoutId as RepTeamId, slideLayoutName as RepTeamName, slideLayoutDescription as RepTeamDesc } from "./Report/HorizontalHeightSpanningImagesWithTitleSlide";
// General templates
import GeneralIntroSlideLayout, { Schema as GeneralIntroSchema, layoutId as GeneralIntroId, layoutName as GeneralIntroName, layoutDescription as GeneralIntroDesc } from "./general/IntroSlideLayout";
import BasicInfoSlideLayout, { Schema as BasicInfoSchema, layoutId as BasicInfoId, layoutName as BasicInfoName, layoutDescription as BasicInfoDesc } from "./general/BasicInfoSlideLayout";
@ -175,6 +232,10 @@ import neoGeneralSettings from "./neo-general/settings.json";
import neoStandardSettings from "./neo-standard/settings.json";
import neoModernSettings from "./neo-modern/settings.json";
import neoSwiftSettings from "./neo-swift/settings.json";
import codeSettings from "./Code/settings.json";
import educationSettings from "./Education/settings.json";
import productOverviewSettings from "./ProductOverview/settings.json";
import reportSettings from "./Report/settings.json";
// Helper to create template entry
@ -182,6 +243,67 @@ import neoSwiftSettings from "./neo-swift/settings.json";
// TODO: Step 3: Create template entries for each template (like the ones below)
export const codeTemplates: TemplateWithData[] = [
createTemplateEntry(CodeSlide01RoadmapCover, CodeRoadmapCoverSchema, CodeRoadmapCoverId, CodeRoadmapCoverName, CodeRoadmapCoverDesc, "code", "CoverSlide"),
createTemplateEntry(CodeSlide02CodeExplanationSplit, CodeExplanationSplitSchema, CodeExplanationSplitId, CodeExplanationSplitName, CodeExplanationSplitDesc, "code", "CodeExplanationSplitSlide"),
createTemplateEntry(CodeSlide03ApiRequestResponse, CodeApiRequestResponseSchema, CodeApiRequestResponseId, CodeApiRequestResponseName, CodeApiRequestResponseDesc, "code", "APIRequestResponseSlide"),
createTemplateEntry(CodeSlide04FeatureGrid, CodeFeatureGridSchema, CodeFeatureGridId, CodeFeatureGridName, CodeFeatureGridDesc, "code", "CardsGridSlide"),
createTemplateEntry(CodeSlide05ComparisonTable, CodeComparisonTableSchema, CodeComparisonTableId, CodeComparisonTableName, CodeComparisonTableDesc, "code", "TableSlide"),
createTemplateEntry(CodeSlide06Workflow, CodeWorkflowSchema, CodeWorkflowId, CodeWorkflowName, CodeWorkflowDesc, "code", "WorkflowSlide"),
createTemplateEntry(CodeSlide07UseCaseList, CodeUseCaseListSchema, CodeUseCaseListId, CodeUseCaseListName, CodeUseCaseListDesc, "code", "TwoColumnBulletListSlide"),
createTemplateEntry(CodeSlide08CodeExplanationText, CodeExplanationTextSchema, CodeExplanationTextId, CodeExplanationTextName, CodeExplanationTextDesc, "code", "DescriptionTextSlide"),
createTemplateEntry(CodeSlide09TableOfContent, CodeTableOfContentSchema, CodeTableOfContentId, CodeTableOfContentName, CodeTableOfContentDesc, "code", "TableOfContentSlide"),
createTemplateEntry(CodeSlide10MetricsSplit, CodeMetricsSplitSchema, CodeMetricsSplitId, CodeMetricsSplitName, CodeMetricsSplitDesc, "code", "DescriptionAndMetricsSlide"),
createTemplateEntry(CodeSlide11MetricsGrid, CodeMetricsGridSchema, CodeMetricsGridId, CodeMetricsGridName, CodeMetricsGridDesc, "code", "MetricsGridSlide"),
];
export const educationTemplates: TemplateWithData[] = [
createTemplateEntry(EducationCoverSlide, EduCoverSchema, EduCoverId, EduCoverName, EduCoverDesc, "education", "EducationCoverSlide"),
createTemplateEntry(EducationTableOfContentsSlide, EduTocSchema, EduTocId, EduTocName, EduTocDesc, "education", "EducationTableOfContentsSlide"),
createTemplateEntry(EducationAboutSlide, EduAboutSchema, EduAboutId, EduAboutName, EduAboutDesc, "education", "EducationAboutSlide"),
createTemplateEntry(EducationContentSplitSlide, EduContentSplitSchema, EduContentSplitId, EduContentSplitName, EduContentSplitDesc, "education", "EducationContentSplitSlide"),
createTemplateEntry(EducationImageGallerySlide, EduImageGallerySchema, EduImageGalleryId, EduImageGalleryName, EduImageGalleryDesc, "education", "EducationImageGallerySlide"),
createTemplateEntry(EducationReportDonutSlide, EduReportDonutSchema, EduReportDonutId, EduReportDonutName, EduReportDonutDesc, "education", "EducationReportDonutSlide"),
createTemplateEntry(EducationServicesSplitSlide, EduServicesSplitSchema, EduServicesSplitId, EduServicesSplitName, EduServicesSplitDesc, "education", "EducationServicesSplitSlide"),
createTemplateEntry(EducationStatisticsGridSlide, EduStatisticsGridSchema, EduStatisticsGridId, EduStatisticsGridName, EduStatisticsGridDesc, "education", "EducationStatisticsGridSlide"),
createTemplateEntry(EducationTimelineSlide, EduTimelineSchema, EduTimelineId, EduTimelineName, EduTimelineDesc, "education", "EducationTimelineSlide"),
];
export const productOverviewTemplates: TemplateWithData[] = [
createTemplateEntry(CoverSlide, PoCoverSchema, PoCoverId, PoCoverName, PoCoverDesc, "product-overview", "CoverSlide"),
createTemplateEntry(TableOfContentSlide, PoTableOfContentSchema, PoTableOfContentId, PoTableOfContentName, PoTableOfContentDesc, "product-overview", "TableOfContentSlide"),
createTemplateEntry(IntroductionSlide, PoIntroductionSchema, PoIntroductionId, PoIntroductionName, PoIntroductionDesc, "product-overview", "IntroductionSlide"),
createTemplateEntry(MissionVisionSlide, PoMissionVisionSchema, PoMissionVisionId, PoMissionVisionName, PoMissionVisionDesc, "product-overview", "MissionVisionSlide"),
// createTemplateEntry(MarketOpportunitySlide, PoMarketOpportunitySchema, PoMarketOpportunityId, PoMarketOpportunityName, PoMarketOpportunityDesc, "product-overview", "MarketOpportunitySlide"),
createTemplateEntry(BusinessChallengesGridSlide, PoBizChallengesGridSchema, PoBizChallengesGridId, PoBizChallengesGridName, PoBizChallengesGridDesc, "product-overview", "BusinessChallengesGridSlide"),
createTemplateEntry(BusinessChallengesCardsSlide, PoBizChallengesCardsSchema, PoBizChallengesCardsId, PoBizChallengesCardsName, PoBizChallengesCardsDesc, "product-overview", "BusinessChallengesCardsSlide"),
createTemplateEntry(OurServicesSlide, PoOurServicesSchema, PoOurServicesId, PoOurServicesName, PoOurServicesDesc, "product-overview", "OurServicesSlide"),
createTemplateEntry(ProcessSlide, PoProcessSchema, PoProcessId, PoProcessName, PoProcessDesc, "product-overview", "ProcessSlide"),
createTemplateEntry(ComparisonChartSlide, PoComparisonChartSchema, PoComparisonChartId, PoComparisonChartName, PoComparisonChartDesc, "product-overview", "ComparisonChartSlide"),
createTemplateEntry(ComparisonTableWithTextSlide, PoComparisonTableSchema, PoComparisonTableId, PoComparisonTableName, PoComparisonTableDesc, "product-overview", "ComparisonTableWithTextSlide"),
createTemplateEntry(KpiCardsSlide, PoKpiCardsSchema, PoKpiCardsId, PoKpiCardsName, PoKpiCardsDesc, "product-overview", "KpiCardsSlide"),
createTemplateEntry(ReportSnapshotSlide, PoReportSnapshotSchema, PoReportSnapshotId, PoReportSnapshotName, PoReportSnapshotDesc, "product-overview", "ReportSnapshotSlide"),
createTemplateEntry(PricingPlanSlide, PoPricingPlanSchema, PoPricingPlanId, PoPricingPlanName, PoPricingPlanDesc, "product-overview", "PricingPlanSlide"),
createTemplateEntry(MeetTeamSlide, PoMeetTeamSchema, PoMeetTeamId, PoMeetTeamName, PoMeetTeamDesc, "product-overview", "MeetTeamSlide"),
createTemplateEntry(ImageGallerySlide, PoImageGallerySchema, PoImageGalleryId, PoImageGalleryName, PoImageGalleryDesc, "product-overview", "ImageGallerySlide"),
];
export const reportTemplates: TemplateWithData[] = [
createTemplateEntry(ReportIntroSlide, RepIntroSchema, RepIntroId, RepIntroName, RepIntroDesc, "report", "IntroCoverSlide"),
createTemplateEntry(TitleDescriptionImageSlide, RepIntroductionImageSchema, RepIntroductionImageId, RepIntroductionImageName, RepIntroductionImageDesc, "report", "TitleDescriptionImageSlide"),
createTemplateEntry(IntroductionStatsSlide, RepIntroductionStatsSchema, RepIntroductionStatsId, RepIntroductionStatsName, RepIntroductionStatsDesc, "report", "MetricsSlide"),
createTemplateEntry(SolutionSlide, RepSolutionSchema, RepSolutionId, RepSolutionName, RepSolutionDesc, "report", "TitleImageBulletCardsSlide"),
createTemplateEntry(MilestoneSlide, RepMilestoneSchema, RepMilestoneId, RepMilestoneName, RepMilestoneDesc, "report", "MilestoneSlide"),
createTemplateEntry(DataAnalysisListSlide, RepDataAnalysisListSchema, RepDataAnalysisListId, RepDataAnalysisListName, RepDataAnalysisListDesc, "report", "BulletListWithIconTitleDescriptionSlide"),
createTemplateEntry(DataAnalysisBarSlide, RepDataAnalysisBarSchema, RepDataAnalysisBarId, RepDataAnalysisBarName, RepDataAnalysisBarDesc, "report", "BarChartWithBulletListWithTitleDescriptionIconSlide"),
createTemplateEntry(DataAnalysisInsightBarSlide, RepDataAnalysisInsightBarSchema, RepDataAnalysisInsightBarId, RepDataAnalysisInsightBarName, RepDataAnalysisInsightBarDesc, "report", "TitleDescriptionChartSlide"),
createTemplateEntry(DataAnalysisLineStatsSlide, RepDataAnalysisLineStatsSchema, RepDataAnalysisLineStatsId, RepDataAnalysisLineStatsName, RepDataAnalysisLineStatsDesc, "report", "TitleChartWithMetricsCardsSlide"),
createTemplateEntry(DataAnalysisDashboardSlide, RepDataAnalysisDashboardSchema, RepDataAnalysisDashboardId, RepDataAnalysisDashboardName, RepDataAnalysisDashboardDesc, "report", "DataAnalysisDashboardSlide"),
createTemplateEntry(PerformanceSnapshotSlide, RepPerformanceSnapshotSchema, RepPerformanceSnapshotId, RepPerformanceSnapshotName, RepPerformanceSnapshotDesc, "report", "TitleMetricsSlide"),
createTemplateEntry(ReportServicesSlide, RepServicesSchema, RepServicesId, RepServicesName, RepServicesDesc, "report", "TitleWorkflowWithTitleDescriptionSlide"),
createTemplateEntry(ReportTeamSlide, RepTeamSchema, RepTeamId, RepTeamName, RepTeamDesc, "report", "HorizontalHeightSpanningImagesWithTitleSlide"),
];
export const neoGeneralTemplates: TemplateWithData[] = [
createTemplateEntry(TextSplitWithEmphasisBlockLayout, TextSplitWithEmphasisBlockSchema, TextSplitWithEmphasisBlockId, TextSplitWithEmphasisBlockName, TextSplitWithEmphasisBlockDesc, 'neo-general', 'TextSplitWithEmphasisBlock'),
@ -352,42 +474,16 @@ export const allLayouts: TemplateWithData[] = [
...modernTemplates,
...standardTemplates,
...swiftTemplates,
...codeTemplates,
...educationTemplates,
...productOverviewTemplates,
...reportTemplates,
];
// TODO: Step 5: Combine all templates into a single array For UseCases (like the ones below)
// For UseCases we need to combine all templates into a single array with settings
export const templates: TemplateLayoutsWithSettings[] = [
{
id: "neo-general",
name: "Neo General",
description: neoGeneralSettings.description,
settings: neoGeneralSettings as TemplateGroupSettings,
layouts: neoGeneralTemplates,
},
{
id: "neo-standard",
name: "Neo Standard",
description: neoStandardSettings.description,
settings: neoStandardSettings as TemplateGroupSettings,
layouts: neoStandardTemplates,
},
{
id: "neo-modern",
name: "Neo Modern",
description: neoModernSettings.description,
settings: neoModernSettings as TemplateGroupSettings,
layouts: neoModernTemplates,
},
{
id: "neo-swift",
name: "Neo Swift",
description: neoSwiftSettings.description,
settings: neoSwiftSettings as TemplateGroupSettings,
layouts: neoSwiftTemplates,
},
{
id: "general",
name: "General",
@ -416,6 +512,63 @@ export const templates: TemplateLayoutsWithSettings[] = [
settings: swiftSettings as TemplateGroupSettings,
layouts: swiftTemplates,
},
{
id: "code",
name: "Code",
description: codeSettings.description,
settings: codeSettings as TemplateGroupSettings,
layouts: codeTemplates,
},
{
id: "education",
name: "Education",
description: educationSettings.description,
settings: educationSettings as TemplateGroupSettings,
layouts: educationTemplates,
},
{
id: "product-overview",
name: "Product Overview",
description: productOverviewSettings.description,
settings: productOverviewSettings as TemplateGroupSettings,
layouts: productOverviewTemplates,
},
{
id: "report",
name: "Report",
description: reportSettings.description,
settings: reportSettings as TemplateGroupSettings,
layouts: reportTemplates,
},
{
id: "neo-general",
name: "Neo General",
description: neoGeneralSettings.description,
settings: neoGeneralSettings as TemplateGroupSettings,
layouts: neoGeneralTemplates,
},
{
id: "neo-standard",
name: "Neo Standard",
description: neoStandardSettings.description,
settings: neoStandardSettings as TemplateGroupSettings,
layouts: neoStandardTemplates,
},
{
id: "neo-modern",
name: "Neo Modern",
description: neoModernSettings.description,
settings: neoModernSettings as TemplateGroupSettings,
layouts: neoModernTemplates,
},
{
id: "neo-swift",
name: "Neo Swift",
description: neoSwiftSettings.description,
settings: neoSwiftSettings as TemplateGroupSettings,
layouts: neoSwiftTemplates,
},
];

View file

@ -28,7 +28,7 @@ const introPitchDeckSchema = z.object({
.meta({ description: "Optional intro card shown below description" }),
image: ImageSchema.default({
__image_url__:
"",
"https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?q=80&w=1600&auto=format&fit=crop",
__image_prompt__: "Abstract business background",
}),

View file

@ -1,5 +1,6 @@
import React from "react"
import * as z from "zod"
import { resolveBackendAssetUrl } from "@/utils/api"
const layoutId = "Timeline"
const layoutName = "Timeline"
@ -148,7 +149,7 @@ const Timeline: React.FC<SlideLayoutProps> = ({ data: slideData }) => {
style={{ backgroundColor: 'var(--card-color, #FFFFFF)' }}
>
<div className="mx-auto w-12 h-12 rounded-full flex items-center justify-center mb-4" style={{ backgroundColor: 'var(--primary-color, #BFF4FF)' }}>
<img src={it.icon.__icon_url__} alt={it.icon.__icon_query__} className="w-6 h-6 object-contain" />
<img src={resolveBackendAssetUrl(it.icon.__icon_url__)} alt={it.icon.__icon_query__} className="w-6 h-6 object-contain" />
</div>
<div className="text-[18px] font-semibold" style={{ color: 'var(--background-text, #111827)' }}>{it.heading}</div>
<p className="mt-3 text-[14px]" style={{ color: 'var(--background-text, #6B7280)' }}>{it.body}</p>

View file

@ -1,5 +1,4 @@
import { cn } from "@/lib/utils"
import { Loader } from "./loader"
import { ProgressBar } from "./progress-bar"
import { useEffect, useState } from "react"
@ -46,14 +45,18 @@ export const OverlayLoader = ({
>
<div
className={cn(
"flex flex-col items-center justify-center px-6 pt-0 pb-8 rounded-xl bg-[#030303] shadow-2xl",
"min-w-[280px] sm:min-w-[330px] border border-white/10 transition-all duration-400 ease-out",
"flex flex-col items-center justify-center px-6 pt-6 pb-10 rounded-xl bg-white shadow-2xl relative min-h-[347px]",
"min-w-[280px] sm:min-w-[447px] border border-white/10 transition-all duration-400 ease-out",
isVisible ? "opacity-100 scale-100" : "opacity-0 scale-90",
className
)}
>
<img loading="eager" src={'/loading.gif'} alt="loading" width={250} height={250} />
<div
className="overlay-loader-dots shrink-0"
role="status"
aria-label="Loading"
/>
{showProgress ? (
<div className="w-full space-y-6 pt-4">
<ProgressBar
@ -62,23 +65,68 @@ export const OverlayLoader = ({
/>
{text && (
<div className="space-y-1">
<p className="text-white text-base text-center font-semibold font-inter">
<p className="text-[#191919] text-base text-center font-medium font-inter">
{text}
</p>
{extra_info && <p className="text-white/80 text-xs text-center font-semibold font-inter">{extra_info}</p>}
{extra_info && <p className="text-[#191919]/80 text-xs text-center font-medium font-inter">{extra_info}</p>}
</div>
)}
</div>
) : (
<>
<p className="text-white text-base text-center font-semibold font-inter">
<p className="text-[#191919] text-base text-center font-medium font-inter">
{text}
</p>
{extra_info && <p className="text-white/80 text-xs text-center font-semibold font-inter">{extra_info}</p>}
{extra_info && <p className="text-[#191919]/80 text-xs text-center font-medium font-inter">{extra_info}</p>}
</>
)}
<svg className="absolute left-0 bottom-0" xmlns="http://www.w3.org/2000/svg" width="447" height="277" viewBox="0 0 447 277" fill="none">
<g filter="url(#filter0_d_4852_6112)">
<path d="M674.5 748.5C668.101 804.091 669 808.5 657.5 832L639 887.5C627 972.5 668.5 1143.5 785 1158.5C984.755 1184.22 877.602 926.811 837.653 808.716C843.652 768.181 841.852 633.973 786.657 421.42C717.663 155.729 278.698 139.89 18.7199 302.37C-241.259 464.851 -399.894 486.766 -478.239 422.953C-544.734 368.793 -537.234 154.707 -464.24 75L-757.716 82.1532C-760.716 183.831 -739.218 390.764 -726.719 430.617C-715.665 465.864 -652.725 581.857 -516.736 619.156C-390.988 653.646 -209.56 584.814 -169.765 572.66C-136.5 562.5 97.7134 443.561 210.704 380.545C699.164 216.532 682.499 679.012 674.5 748.5Z" fill="url(#paint0_radial_4852_6112)" shape-rendering="crispEdges" />
</g>
<defs>
<filter id="filter0_d_4852_6112" x="-833" y="0" width="1810.32" height="1235.29" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feOffset />
<feGaussianBlur stdDeviation="37.5" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix type="matrix" values="0 0 0 0 0.85098 0 0 0 0 0.839216 0 0 0 0 0.996078 0 0 0 1 0" />
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_4852_6112" />
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_4852_6112" result="shape" />
</filter>
<radialGradient id="paint0_radial_4852_6112" cx="0" cy="0" r="1" gradientTransform="matrix(-987.419 -112.408 219.823 -2016.77 351.693 300.327)" gradientUnits="userSpaceOnUse">
<stop stop-color="#D9D6FE" />
<stop offset="1" stop-color="white" stop-opacity="0" />
</radialGradient>
</defs>
</svg>
</div>
<style jsx>{`
.overlay-loader-dots {
width: 50px;
aspect-ratio: 1;
--_c: no-repeat radial-gradient(
farthest-side,
#7A5AF8 92%,
#0000
);
background:
var(--_c) top,
var(--_c) left,
var(--_c) right,
var(--_c) bottom;
background-size: 12px 12px;
animation: overlay-loader-l7 1s infinite;
}
@keyframes overlay-loader-l7 {
to {
transform: rotate(0.5turn);
}
}
`}</style>
</div>
)
}

View file

@ -46,11 +46,11 @@ export const ProgressBar = ({ duration, onComplete }: ProgressBarProps) => {
return (
<div className="w-full space-y-2">
<div className="flex justify-end items-center text-white/80 text-sm">
<div className="flex justify-end items-center text-sm">
{/* <span>Processing...</span> */}
<span className='font-inter text-end font-medium text-xs'>{Math.round(progress)}%</span>
<span className='font-inter text-[#191919]/80 text-end font-medium text-xs'>{Math.round(progress)}%</span>
</div>
<div className="w-full bg-white rounded-full h-2 overflow-hidden">
<div className="w-full bg-white/40 rounded-full h-2 overflow-hidden">
<div
className="h-full bg-gradient-to-r from-[#9034EA] via-[#5146E5] to-[#9034EA] rounded-full animate-gradient transition-all duration-300 ease-out"
style={{