feat: new templates & update template preview, template selection, custom template
This commit is contained in:
parent
c7860127f2
commit
4af3534c6d
44 changed files with 615484 additions and 1358 deletions
583634
servers/fastapi/assets/icons-vectorstore.json
Normal file
583634
servers/fastapi/assets/icons-vectorstore.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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]"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
|
@ -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"}}
|
||||
|
|
@ -0,0 +1 @@
|
|||
5f1b8cd78bc4fb444dd171e59b18f3a3af89a079
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../blobs/56c8c186de9040d4fea8daac2ca110f9d412bf04
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../blobs/bbd7b466f6d58e646fdc2bd5fd67b2f5e93c0b687011bd4548c420f7bd46f0c5
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../blobs/9bbecc17cabbcbd3112c14d6982b51403b264bfa
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../blobs/c17ed520ed8438736732a54957a69306b8822215
|
||||
|
|
@ -0,0 +1 @@
|
|||
../../blobs/61e23f16c75ff9995b1d2f251d720c6146d21338
|
||||
|
|
@ -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' />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -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;
|
||||
0
servers/nextjs/app/(presentation-generator)/template-preview/components/TemplatePreviewClient.tsx
Executable file → Normal file
0
servers/nextjs/app/(presentation-generator)/template-preview/components/TemplatePreviewClient.tsx
Executable file → Normal 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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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… we’ll 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>
|
||||
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
||||
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}),
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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={{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue