Merge pull request #115 from presenton/fix/schema-validation

fix(fastapi): removes seperate schema constraints from system prompt, fix(nextjs): improves layout schema constraints
This commit is contained in:
Saurav Niraula 2025-07-21 23:38:12 +05:45 committed by GitHub
commit 362ddd39a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 261 additions and 231 deletions

View file

@ -32,6 +32,9 @@ async def stream_outlines(presentation_id: str):
presentation.language,
presentation.summary,
):
# Give control to the event loop
await asyncio.sleep(0)
yield SSEResponse(
event="response",
data=json.dumps({"type": "chunk", "chunk": chunk}),

View file

@ -218,6 +218,9 @@ async def stream_presentation(presentation_id: str):
# This will mutate slide
async_assets_generation_tasks.append(process_slide_and_fetch_assets(slide))
# Give control to the event loop
await asyncio.sleep(0)
yield SSEResponse(
event="response",
data=json.dumps({"type": "chunk", "chunk": slide.model_dump_json()}),

View file

@ -17,8 +17,8 @@ class ContactInfoModel(BaseModel):
class ImageModel(BaseModel):
image_url__: str = Field(description="Image URL")
image_prompt__: str = Field(description="Image prompt")
__image_url__: str = Field(description="Image URL")
__image_prompt__: str = Field(description="Image prompt")
# First Slide Layout
@ -415,11 +415,5 @@ presentation_layout = PresentationLayoutModel(
],
)
# print(json.dumps(FirstSlideModel.model_json_schema()))
slide_schema = FirstSlideModel.model_json_schema()
slide_schema = remove_fields_from_schema(slide_schema, ["image_url__"])
print(slide_schema)
# print(PresentationOutlineModel.model_json_schema())
print(json.dumps(StatisticsSlideModel.model_json_schema()))

View file

@ -8,6 +8,7 @@ from utils.llm_provider import (
get_google_llm_client,
get_large_model,
get_llm_client,
get_nano_model,
is_google_selected,
)
@ -83,7 +84,7 @@ async def generate_ppt_outline(
language: Optional[str] = None,
content: Optional[str] = None,
):
model = get_large_model()
model = get_nano_model()
response_model = get_presentation_outline_model_with_n_slides(n_slides)
if not is_google_selected():

View file

@ -1,6 +1,6 @@
from models.presentation_layout import PresentationLayoutModel
from models.presentation_outline_model import PresentationOutlineModel
from utils.llm_provider import get_llm_client, get_small_model
from utils.llm_provider import get_llm_client, get_nano_model, get_small_model
from utils.get_dynamic_models import (
get_presentation_structure_model_with_n_slides,
)
@ -14,30 +14,37 @@ def get_prompt(presentation_layout: PresentationLayoutModel, n_slides: int, data
{
"role": "system",
"content": f"""
You're a professional presentation designer.
You're a professional presentation designer with creative freedom to design engaging presentations.
{presentation_layout.to_string()}
# CRITICAL RULES
- NEVER use layout type 1 (bullet points) for more than 30% of slides
- MUST use at least 3 different layout types across presentation
- NO consecutive slides with same layout type
# DESIGN PHILOSOPHY
- Create visually compelling and varied presentations
- Match layout to content purpose and audience needs
- Prioritize engagement over rigid formatting rules
# Selection Strategy
1. **Ignore bullet point format** - focus on slide PURPOSE
2. **Match content to layout**:
- Title/intro Title layouts
- Process/steps Visual process layouts
- Comparisons Side-by-side layouts
- Data Chart/graph layouts
- Concepts Image + text layouts
- Key messages Emphasis layouts
# Layout Selection Guidelines
1. **Content-driven choices**: Let the slide's purpose guide layout selection
- Opening/closing Title layouts
- Processes/workflows Visual process layouts
- Comparisons/contrasts Side-by-side layouts
- Data/metrics Chart/graph layouts
- Concepts/ideas Image + text layouts
- Key insights Emphasis layouts
3. **Force variety**: If recently used a layout type, pick different one
4. **Prioritize visual layouts** over text-heavy ones
2. **Visual variety**: Aim for diverse, engaging presentation flow
- Mix text-heavy and visual-heavy slides naturally
- Use your judgment on when repetition serves the content
- Balance information density across slides
**Think PURPOSE not FORMAT. Make it visually engaging.**
3. **Audience experience**: Consider how slides work together
- Create natural transitions between topics
- Use layouts that enhance comprehension
- Design for maximum impact and retention
Select layout index for each of the {n_slides} slides.
**Trust your design instincts. Focus on creating the most effective presentation for the content and audience.**
Select layout index for each of the {n_slides} slides based on what will best serve the presentation's goals.
""",
},
{
@ -55,7 +62,7 @@ async def generate_presentation_structure(
) -> PresentationStructureModel:
client = get_llm_client()
model = get_small_model()
model = get_nano_model()
response_model = get_presentation_structure_model_with_n_slides(
len(presentation_outline.slides)
)

View file

@ -6,10 +6,11 @@ from models.presentation_outline_model import SlideOutlineModel
from utils.llm_provider import (
get_google_llm_client,
get_llm_client,
get_nano_model,
get_small_model,
is_google_selected,
)
from utils.schema_utils import remove_fields_from_schema, generate_constraint_sentences
from utils.schema_utils import remove_fields_from_schema
system_prompt = """
Generate structured slide based on provided title and outline, follow mentioned steps and notes and provide structured output.
@ -36,14 +37,12 @@ def get_user_prompt(title: str, outline: str):
"""
def get_prompt_to_generate_slide_content(
title: str, outline: str, schema_constraints: str = ""
):
def get_prompt_to_generate_slide_content(title: str, outline: str):
return [
{
"role": "system",
"content": system_prompt + f"\n{schema_constraints}",
"content": system_prompt,
},
{
"role": "user",
@ -55,12 +54,11 @@ def get_prompt_to_generate_slide_content(
async def get_slide_content_from_type_and_outline(
slide_layout: SlideLayoutModel, outline: SlideOutlineModel
):
model = get_small_model()
model = get_nano_model()
response_schema = remove_fields_from_schema(
slide_layout.json_schema, ["__image_url__", "__icon_url__"]
)
schema_constraints = generate_constraint_sentences(response_schema)
if not is_google_selected():
client = get_llm_client()
@ -69,7 +67,6 @@ async def get_slide_content_from_type_and_outline(
messages=get_prompt_to_generate_slide_content(
outline.title,
outline.body,
schema_constraints,
),
response_format={
"type": "json_schema",
@ -87,7 +84,7 @@ async def get_slide_content_from_type_and_outline(
model=model,
contents=[get_user_prompt(outline.title, outline.body)],
config=GenerateContentConfig(
system_instruction=system_prompt + f"\n{schema_constraints}",
system_instruction=system_prompt,
response_mime_type="application/json",
response_json_schema=response_schema,
),

View file

@ -1,7 +1,7 @@
from models.presentation_layout import PresentationLayoutModel, SlideLayoutModel
from models.slide_layout_index import SlideLayoutIndex
from models.sql.slide import SlideModel
from utils.llm_provider import get_llm_client, get_small_model
from utils.llm_provider import get_llm_client, get_nano_model, get_small_model
def get_prompt_to_select_slide_layout(
@ -42,7 +42,7 @@ async def get_slide_layout_from_prompt(
) -> SlideLayoutModel:
client = get_llm_client()
model = get_small_model()
model = get_nano_model()
slide_layout_ids = list(map(lambda x: x.id, layout.slides))

View file

@ -1,7 +1,7 @@
from copy import deepcopy
from typing import List
from utils.dict_utils import get_dict_paths_with_key, get_dict_at_path, set_dict_at_path
from utils.dict_utils import get_dict_paths_with_key, get_dict_at_path
def resolve_refs(schema, defs):
@ -50,70 +50,93 @@ def remove_fields_from_schema(schema: dict, fields_to_remove: List[str]):
return schema
# ? Not used
def generate_constraint_sentences(schema: dict) -> str:
"""
Generate human-readable constraint sentences from a JSON schema.
Args:
schema: JSON schema dictionary
Returns:
String containing constraint sentences separated by newlines
"""
constraints = []
def extract_constraints_recursive(obj, prefix=""):
if isinstance(obj, dict):
if "properties" in obj:
properties = obj["properties"]
for prop_name, prop_def in properties.items():
current_path = f"{prefix}.{prop_name}" if prefix else prop_name
if isinstance(prop_def, dict):
prop_type = prop_def.get("type")
# Handle string constraints
if prop_type == "string":
min_length = prop_def.get("minLength")
max_length = prop_def.get("maxLength")
if min_length is not None and max_length is not None:
constraints.append(f" - {current_path} should be less than {max_length} characters and greater than {min_length} characters")
constraints.append(
f" - {current_path} should be less than {max_length} characters and greater than {min_length} characters"
)
elif max_length is not None:
constraints.append(f" - {current_path} should be less than {max_length} characters")
constraints.append(
f" - {current_path} should be less than {max_length} characters"
)
elif min_length is not None:
constraints.append(f" - {current_path} should be greater than {min_length} characters")
constraints.append(
f" - {current_path} should be greater than {min_length} characters"
)
# Handle array constraints
elif prop_type == "array":
min_items = prop_def.get("minItems")
max_items = prop_def.get("maxItems")
if min_items is not None and max_items is not None:
constraints.append(f" - {current_path} should have more than {min_items} items and less than {max_items} items")
constraints.append(
f" - {current_path} should have more than {min_items} items and less than {max_items} items"
)
elif max_items is not None:
constraints.append(f" - {current_path} should have less than {max_items} items")
constraints.append(
f" - {current_path} should have less than {max_items} items"
)
elif min_items is not None:
constraints.append(f" - {current_path} should have more than {min_items} items")
constraints.append(
f" - {current_path} should have more than {min_items} items"
)
# Recurse into nested objects
if prop_type == "object" or "properties" in prop_def:
extract_constraints_recursive(prop_def, current_path)
# Handle array items if they have properties
if prop_type == "array" and "items" in prop_def:
items_def = prop_def["items"]
if isinstance(items_def, dict) and ("properties" in items_def or items_def.get("type") == "object"):
extract_constraints_recursive(items_def, f"{current_path}[*]")
if isinstance(items_def, dict) and (
"properties" in items_def
or items_def.get("type") == "object"
):
extract_constraints_recursive(
items_def, f"{current_path}[*]"
)
# Also recurse into other nested structures
for key, value in obj.items():
if key not in ["properties", "type", "minLength", "maxLength", "minItems", "maxItems"] and isinstance(value, dict):
if key not in [
"properties",
"type",
"minLength",
"maxLength",
"minItems",
"maxItems",
] and isinstance(value, dict):
extract_constraints_recursive(value, prefix)
# Start extraction from the root schema
extract_constraints_recursive(schema)
return "\n".join(constraints)

View file

@ -66,13 +66,12 @@ async function getBrowserAndPage(id: string): Promise<[Browser, Page]> {
'--disable-dev-shm-usage',
'--disable-gpu',
'--disable-web-security',
'--window-size=1920,1080'
],
});
const page = await browser.newPage();
await page.setViewport({ width: 1920, height: 1080, deviceScaleFactor: 1 });
await page.setViewport({ width: 1280, height: 720, deviceScaleFactor: 1 });
await page.goto(`http://localhost/pdf-maker?id=${id}`, {
waitUntil: "networkidle0",
timeout: 60000,

View file

@ -6,7 +6,7 @@ export const ImageSchema = z.object({
}),
__image_prompt__: z.string().meta({
description: "Prompt used to generate the image",
}),
}).min(10).max(50),
})
export const IconSchema = z.object({
@ -15,5 +15,5 @@ export const IconSchema = z.object({
}),
__icon_query__: z.string().meta({
description: "Query used to search the icon",
}),
}).min(5).max(20),
})

View file

@ -7,10 +7,10 @@ export const layoutName = 'Basic Info'
export const layoutDescription = 'A clean slide layout with title, description text, and a supporting image.'
const basicInfoSlideSchema = z.object({
title: z.string().min(3).max(50).default('Product Overview').meta({
title: z.string().min(3).max(40).default('Product Overview').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(180).default('Our product offers customizable dashboards for real-time reporting and data-driven decisions. It integrates with third-party tools to enhance operations and scales with business growth for improved efficiency.').meta({
description: z.string().min(10).max(150).default('Our product offers customizable dashboards for real-time reporting and data-driven decisions. It integrates with third-party tools to enhance operations and scales with business growth for improved efficiency.').meta({
description: "Main description text content",
}),
image: ImageSchema.default({

View file

@ -7,7 +7,7 @@ export const layoutName = 'Bullet Icons Only'
export const layoutDescription = 'A slide layout with title, grid of bullet points with icons (no descriptions), and a supporting image.'
const bulletIconsOnlySlideSchema = z.object({
title: z.string().min(3).max(50).default('Solutions').meta({
title: z.string().min(3).max(40).default('Solutions').meta({
description: "Main title of the slide",
}),
image: ImageSchema.default({
@ -20,11 +20,11 @@ const bulletIconsOnlySlideSchema = z.object({
title: z.string().min(2).max(80).meta({
description: "Bullet point title",
}),
subtitle: z.string().min(5).max(180).optional().meta({
subtitle: z.string().min(5).max(150).optional().meta({
description: "Optional short subtitle or brief explanation",
}),
icon: IconSchema,
})).min(2).max(6).default([
})).min(2).max(4).default([
{
title: 'Custom Software',
subtitle: 'We create tailored software to optimize processes and boost efficiency.',
@ -72,7 +72,7 @@ interface BulletIconsOnlySlideLayoutProps {
const BulletIconsOnlySlideLayout: React.FC<BulletIconsOnlySlideLayoutProps> = ({ data: slideData }) => {
const bulletPoints = slideData?.bulletPoints || []
// Function to determine grid classes based on number of bullets
const getGridClasses = (count: number) => {
if (count <= 2) {
@ -87,12 +87,12 @@ const BulletIconsOnlySlideLayout: React.FC<BulletIconsOnlySlideLayoutProps> = ({
return (
<>
{/* Import Google Fonts */}
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div
<div
className="w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white relative z-20 mx-auto overflow-hidden"
style={{
fontFamily: 'Poppins, sans-serif'
@ -101,14 +101,14 @@ const BulletIconsOnlySlideLayout: React.FC<BulletIconsOnlySlideLayoutProps> = ({
{/* Decorative Wave Patterns */}
<div className="absolute top-0 left-0 w-32 h-full opacity-10 overflow-hidden">
<svg className="w-full h-full" viewBox="0 0 100 400" fill="none">
<path d="M0 100C25 150 50 50 75 100C87.5 125 100 100 100 100V0H0V100Z" fill="#8b5cf6" opacity="0.4"/>
<path d="M0 200C37.5 250 62.5 150 100 200V150C75 175 50 150 25 175L0 200Z" fill="#8b5cf6" opacity="0.3"/>
<path d="M0 100C25 150 50 50 75 100C87.5 125 100 100 100 100V0H0V100Z" fill="#8b5cf6" opacity="0.4" />
<path d="M0 200C37.5 250 62.5 150 100 200V150C75 175 50 150 25 175L0 200Z" fill="#8b5cf6" opacity="0.3" />
</svg>
</div>
<div className="absolute bottom-0 left-0 w-48 h-32 opacity-10 overflow-hidden">
<svg className="w-full h-full" viewBox="0 0 200 100" fill="none">
<path d="M0 50C50 25 100 75 150 50C175 37.5 200 50 200 50V100H0V50Z" fill="#8b5cf6" opacity="0.2"/>
<path d="M0 50C50 25 100 75 150 50C175 37.5 200 50 200 50V100H0V50Z" fill="#8b5cf6" opacity="0.2" />
</svg>
</div>
@ -124,19 +124,19 @@ const BulletIconsOnlySlideLayout: React.FC<BulletIconsOnlySlideLayoutProps> = ({
{/* Bullet Points Grid */}
<div className={`grid ${getGridClasses(bulletPoints.length)} flex-1 content-center`}>
{bulletPoints.map((bullet, index) => (
<div
key={index}
<div
key={index}
className={`flex items-start space-x-4 p-4 rounded-lg transition-all duration-200 hover:bg-gray-50`}
>
{/* Icon */}
<div className="flex-shrink-0 w-12 h-12 bg-purple-600 rounded-full flex items-center justify-center">
<img
src={bullet.icon.__icon_url__}
<img
src={bullet.icon.__icon_url__}
alt={bullet.icon.__icon_query__}
className="w-6 h-6 object-contain brightness-0 invert"
/>
</div>
{/* Content */}
<div className="flex-1">
<h3 className="text-lg sm:text-xl font-semibold text-gray-900 mb-1">
@ -158,10 +158,10 @@ const BulletIconsOnlySlideLayout: React.FC<BulletIconsOnlySlideLayoutProps> = ({
{/* Decorative Elements */}
<div className="absolute top-8 right-8 text-purple-600 opacity-60">
<svg width="32" height="32" viewBox="0 0 32 32" fill="currentColor">
<path d="M16 0l4.12 8.38L28 12l-7.88 3.62L16 24l-4.12-8.38L4 12l7.88-3.62L16 0z"/>
<path d="M16 0l4.12 8.38L28 12l-7.88 3.62L16 24l-4.12-8.38L4 12l7.88-3.62L16 0z" />
</svg>
</div>
<div className="absolute top-16 left-8 opacity-20">
<svg width="80" height="20" viewBox="0 0 80 20" className="text-purple-600">
<path

View file

@ -7,10 +7,10 @@ export const layoutName = 'Bullet with Icons'
export const layoutDescription = 'A bullets style slide with main content, supporting image, and bullet points with icons and descriptions.'
const bulletWithIconsSlideSchema = z.object({
title: z.string().min(3).max(50).default('Problem').meta({
title: z.string().min(3).max(40).default('Problem').meta({
description: "Main title of the slide",
}),
description: z.string().max(180).default('Businesses face challenges with outdated technology and rising costs, limiting efficiency and growth in competitive markets.').meta({
description: z.string().max(150).default('Businesses face challenges with outdated technology and rising costs, limiting efficiency and growth in competitive markets.').meta({
description: "Main description text explaining the problem or topic",
}),
image: ImageSchema.default({
@ -23,7 +23,7 @@ const bulletWithIconsSlideSchema = z.object({
title: z.string().min(2).max(80).meta({
description: "Bullet point title",
}),
description: z.string().min(10).max(180).meta({
description: z.string().min(10).max(150).meta({
description: "Bullet point description",
}),
icon: IconSchema,

View file

@ -8,39 +8,40 @@ export const layoutId = 'chart-with-bullets-slide'
export const layoutName = 'Chart with Bullet Boxes'
export const layoutDescription = 'A slide layout with title, description, chart on the left and colored bullet boxes with icons on the right. Only choose this if data is available.'
const chartDataSchema = z.object({
name: z.string().meta({ description: "Data point name" }),
value: z.number().meta({ description: "Data point value" }),
category: z.string().optional().meta({ description: "Category for grouping" }),
x: z.number().optional().meta({ description: "X coordinate for scatter plots" }),
y: z.number().optional().meta({ description: "Y coordinate for scatter plots" }),
});
const barPieLineAreaChartDataSchema = z.object({
type: z.union([z.literal('bar'), z.literal('pie'), z.literal('line'), z.literal('area')]),
data: z.array(z.object({
name: z.string().meta({ description: "Data point name" }),
value: z.number().meta({ description: "Data point value" }),
})).min(2).max(5)
})
const scatterChartDataSchema = z.object({
type: z.literal('scatter'),
data: z.array(z.object({
x: z.number().meta({ description: "X coordinate" }),
y: z.number().meta({ description: "Y coordinate" }),
})).min(2).max(100)
})
const chartWithBulletsSlideSchema = z.object({
title: z.string().min(3).max(50).default('Market Size').meta({
title: z.string().min(3).max(40).default('Market Size').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(180).default('Businesses face challenges with outdated technology and rising costs, limiting efficiency and growth in competitive markets.').meta({
description: z.string().min(10).max(150).default('Businesses face challenges with outdated technology and rising costs, limiting efficiency and growth in competitive markets.').meta({
description: "Description text below the title",
}),
chartType: z.enum(['bar', 'line', 'pie', 'area', 'scatter']).default('bar').meta({
description: "Type of chart to display",
}),
data: z.array(chartDataSchema).min(2).max(10).default([
{ name: '2021', value: 5 },
{ name: '2022', value: 12 },
{ name: '2023', value: 18 },
{ name: '2024', value: 23 },
{ name: '2025', value: 26 },
]).meta({
description: "Chart data points",
}),
dataKey: z.string().default('value').meta({
description: "Key field for chart values",
}),
categoryKey: z.string().default('name').meta({
description: "Key field for chart categories",
}),
chartData: z.union([barPieLineAreaChartDataSchema, scatterChartDataSchema]).default({
type: 'scatter',
data: [
{ x: 5, y: 5 },
{ x: 10, y: 12 },
{ x: 15, y: 18 },
{ x: 20, y: 23 },
{ x: 25, y: 26 },
]
}
),
color: z.string().default('#3b82f6').meta({
description: "Primary color for chart elements",
}),
@ -54,7 +55,7 @@ const chartWithBulletsSlideSchema = z.object({
title: z.string().min(2).max(80).meta({
description: "Bullet point title",
}),
description: z.string().min(10).max(180).meta({
description: z.string().min(10).max(150).meta({
description: "Bullet point description",
}),
icon: IconSchema,
@ -90,6 +91,8 @@ const chartWithBulletsSlideSchema = z.object({
export const Schema = chartWithBulletsSlideSchema
console.log(z.toJSONSchema(chartWithBulletsSlideSchema))
export type ChartWithBulletsSlideData = z.infer<typeof chartWithBulletsSlideSchema>
interface ChartWithBulletsSlideLayoutProps {
@ -106,21 +109,21 @@ const chartConfig = {
};
const CHART_COLORS = [
'#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6',
'#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6',
'#06b6d4', '#84cc16', '#f97316', '#ec4899', '#6366f1'
];
const BULLET_COLORS = [
'#7F31E9', '#2C78DA', '#F58AAB', '#10b981', '#f59e0b',
'#7F31E9', '#2C78DA', '#F58AAB', '#10b981', '#f59e0b',
'#06b6d4', '#84cc16', '#f97316', '#ec4899', '#6366f1'
];
const ChartWithBulletsSlideLayout: React.FC<ChartWithBulletsSlideLayoutProps> = ({ data: slideData }) => {
const chartData = slideData?.data || [];
const chartType = slideData?.chartType || 'bar';
const chartData = slideData?.chartData?.data || [];
const chartType = slideData?.chartData?.type;
const color = slideData?.color || '#3b82f6';
const dataKey = slideData?.dataKey || 'value';
const categoryKey = slideData?.categoryKey || 'name';
const xAxis = chartType === 'scatter' ? 'x' : 'name';
const yAxis = chartType === 'scatter' ? 'y' : 'value';
const showLegend = slideData?.showLegend || false;
const showTooltip = slideData?.showTooltip || true;
const bulletPoints = slideData?.bulletPoints || []
@ -136,50 +139,50 @@ const ChartWithBulletsSlideLayout: React.FC<ChartWithBulletsSlideLayoutProps> =
return (
<BarChart {...commonProps}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={categoryKey} />
<XAxis dataKey={xAxis} />
<YAxis />
{showTooltip && <ChartTooltip content={<ChartTooltipContent />} />}
{showLegend && <ChartLegend content={<ChartLegendContent />} />}
<Bar dataKey={dataKey} fill={color} radius={[4, 4, 0, 0]} />
<Bar dataKey={yAxis} fill={color} radius={[4, 4, 0, 0]} />
</BarChart>
);
case 'line':
return (
<LineChart {...commonProps}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={categoryKey} />
<XAxis dataKey={xAxis} />
<YAxis />
{showTooltip && <ChartTooltip content={<ChartTooltipContent />} />}
{showLegend && <ChartLegend content={<ChartLegendContent />} />}
<Line
type="monotone"
dataKey={dataKey}
stroke={color}
<Line
type="monotone"
dataKey={yAxis}
stroke={color}
strokeWidth={3}
dot={{ fill: color, strokeWidth: 2, r: 4 }}
/>
</LineChart>
);
case 'area':
return (
<AreaChart {...commonProps}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey={categoryKey} />
<XAxis dataKey={xAxis} />
<YAxis />
{showTooltip && <ChartTooltip content={<ChartTooltipContent />} />}
{showLegend && <ChartLegend content={<ChartLegendContent />} />}
<Area
type="monotone"
dataKey={dataKey}
stroke={color}
fill={color}
<Area
type="monotone"
dataKey={yAxis}
stroke={color}
fill={color}
fillOpacity={0.6}
/>
</AreaChart>
);
case 'pie':
return (
<PieChart margin={{ top: 20, right: 30, left: 40, bottom: 60 }}>
@ -191,7 +194,7 @@ const ChartWithBulletsSlideLayout: React.FC<ChartWithBulletsSlideLayoutProps> =
cy="40%"
outerRadius={70}
fill={color}
dataKey={dataKey}
dataKey={yAxis}
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
>
{chartData.map((entry, index) => (
@ -200,19 +203,19 @@ const ChartWithBulletsSlideLayout: React.FC<ChartWithBulletsSlideLayoutProps> =
</Pie>
</PieChart>
);
case 'scatter':
return (
<ScatterChart {...commonProps}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="x" type="number" />
<YAxis dataKey="y" type="number" />
<XAxis dataKey={xAxis} type="number" />
<YAxis dataKey={yAxis} type="number" />
{showTooltip && <ChartTooltip content={<ChartTooltipContent />} />}
{showLegend && <ChartLegend content={<ChartLegendContent />} />}
<Scatter dataKey="value" fill={color} />
</ScatterChart>
);
default:
return <div>Unsupported chart type</div>;
}
@ -221,12 +224,12 @@ const ChartWithBulletsSlideLayout: React.FC<ChartWithBulletsSlideLayoutProps> =
return (
<>
{/* Import Google Fonts */}
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div
<div
className="w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white relative z-20 mx-auto overflow-hidden"
style={{
fontFamily: 'Poppins, sans-serif'
@ -257,8 +260,8 @@ const ChartWithBulletsSlideLayout: React.FC<ChartWithBulletsSlideLayoutProps> =
{/* Right Section - Bullet Point Boxes */}
<div className="flex-shrink-0 w-80 flex flex-col justify-center space-y-4">
{bulletPoints.map((bullet, index) => (
<div
key={index}
<div
key={index}
className="rounded-2xl p-6 text-white"
style={{
backgroundColor: BULLET_COLORS[index % BULLET_COLORS.length]
@ -267,8 +270,8 @@ const ChartWithBulletsSlideLayout: React.FC<ChartWithBulletsSlideLayoutProps> =
{/* Icon and Title */}
<div className="flex items-center space-x-3 mb-3">
<div className="w-8 h-8 bg-white/20 rounded-lg flex items-center justify-center">
<img
src={bullet.icon.__icon_url__}
<img
src={bullet.icon.__icon_url__}
alt={bullet.icon.__icon_query__}
className="w-5 h-5 object-contain brightness-0 invert"
/>
@ -277,7 +280,7 @@ const ChartWithBulletsSlideLayout: React.FC<ChartWithBulletsSlideLayoutProps> =
{bullet.title}
</h3>
</div>
{/* Description */}
<p className="text-sm leading-relaxed opacity-90">
{bullet.description}

View file

@ -7,10 +7,10 @@ export const layoutName = 'Intro Slide'
export const layoutDescription = 'A clean slide layout with title, description text, presenter info, and a supporting image.'
const introSlideSchema = z.object({
title: z.string().min(3).max(50).default('Product Overview').meta({
title: z.string().min(3).max(40).default('Product Overview').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(180).default('Our product offers customizable dashboards for real-time reporting and data-driven decisions. It integrates with third-party tools to enhance operations and scales with business growth for improved efficiency.').meta({
description: z.string().min(10).max(150).default('Our product offers customizable dashboards for real-time reporting and data-driven decisions. It integrates with third-party tools to enhance operations and scales with business growth for improved efficiency.').meta({
description: "Main description text content",
}),
presenterName: z.string().min(2).max(50).default('John Doe').meta({

View file

@ -10,16 +10,16 @@ const metricsSlideSchema = z.object({
description: "Main title of the slide",
}),
metrics: z.array(z.object({
value: z.string().min(1).max(10).meta({
description: "Metric value (e.g., 150+, 95%, $2M). No long values. Keep simple number."
}),
label: z.string().min(2).max(100).meta({
description: "Metric label/title"
}),
description: z.string().min(10).max(300).meta({
value: z.string().min(1).max(10).meta({
description: "Metric value (e.g., 150+, 95%, $2M). No long values. Keep simple number."
}),
description: z.string().min(10).max(150).meta({
description: "Detailed description of the metric. Explanation of the metric."
}),
})).min(2).max(6).default([
})).min(2).max(3).default([
{
value: '150+',
label: 'Clients Onboarded',
@ -50,7 +50,7 @@ interface MetricsSlideLayoutProps {
const MetricsSlideLayout: React.FC<MetricsSlideLayoutProps> = ({ data: slideData }) => {
const metrics = slideData?.metrics || []
// Function to determine layout classes based on number of metrics
const getLayoutClasses = (count: number) => {
if (count === 1) {
@ -67,7 +67,7 @@ const MetricsSlideLayout: React.FC<MetricsSlideLayoutProps> = ({ data: slideData
return 'grid grid-cols-2 md:grid-cols-3'
}
}
// Function to get individual item classes
const getItemClasses = (count: number) => {
// All items use same classes now
@ -77,12 +77,12 @@ const MetricsSlideLayout: React.FC<MetricsSlideLayoutProps> = ({ data: slideData
return (
<>
{/* Import Google Fonts */}
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div
<div
className="w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white relative z-20 mx-auto overflow-hidden flex flex-col"
style={{
fontFamily: 'Poppins, sans-serif'
@ -91,17 +91,17 @@ const MetricsSlideLayout: React.FC<MetricsSlideLayoutProps> = ({ data: slideData
{/* Decorative Wave Patterns */}
<div className="absolute top-0 left-0 w-64 h-full opacity-10 overflow-hidden">
<svg className="w-full h-full" viewBox="0 0 200 400" fill="none">
<path d="M0 100C50 150 100 50 150 100C175 125 200 100 200 100V0H0V100Z" fill="#8b5cf6" opacity="0.3"/>
<path d="M0 200C75 250 125 150 200 200V150C150 175 100 150 50 175L0 200Z" fill="#8b5cf6" opacity="0.2"/>
<path d="M0 300C100 350 150 250 200 300V250C125 275 75 250 25 275L0 300Z" fill="#8b5cf6" opacity="0.1"/>
<path d="M0 100C50 150 100 50 150 100C175 125 200 100 200 100V0H0V100Z" fill="#8b5cf6" opacity="0.3" />
<path d="M0 200C75 250 125 150 200 200V150C150 175 100 150 50 175L0 200Z" fill="#8b5cf6" opacity="0.2" />
<path d="M0 300C100 350 150 250 200 300V250C125 275 75 250 25 275L0 300Z" fill="#8b5cf6" opacity="0.1" />
</svg>
</div>
<div className="absolute top-0 right-0 w-64 h-full opacity-10 overflow-hidden transform scale-x-[-1]">
<svg className="w-full h-full" viewBox="0 0 200 400" fill="none">
<path d="M0 100C50 150 100 50 150 100C175 125 200 100 200 100V0H0V100Z" fill="#8b5cf6" opacity="0.3"/>
<path d="M0 200C75 250 125 150 200 200V150C150 175 100 150 50 175L0 200Z" fill="#8b5cf6" opacity="0.2"/>
<path d="M0 300C100 350 150 250 200 300V250C125 275 75 250 25 275L0 300Z" fill="#8b5cf6" opacity="0.1"/>
<path d="M0 100C50 150 100 50 150 100C175 125 200 100 200 100V0H0V100Z" fill="#8b5cf6" opacity="0.3" />
<path d="M0 200C75 250 125 150 200 200V150C150 175 100 150 50 175L0 200Z" fill="#8b5cf6" opacity="0.2" />
<path d="M0 300C100 350 150 250 200 300V250C125 275 75 250 25 275L0 300Z" fill="#8b5cf6" opacity="0.1" />
</svg>
</div>
@ -121,31 +121,31 @@ const MetricsSlideLayout: React.FC<MetricsSlideLayoutProps> = ({ data: slideData
<div className="flex justify-center">
{/* Metrics Layout - Each metric grouped vertically */}
<div className={`${getLayoutClasses(metrics.length)} gap-6 lg:gap-8 place-content-center place-items-center`}>
{metrics.map((metric, index) => (
<div key={index} className={`text-center space-y-4 ${getItemClasses(metrics.length)}`}>
{/* Label */}
<div className="text-sm text-gray-600 font-medium">
{metric.label}
{metrics.map((metric, index) => (
<div key={index} className={`text-center space-y-4 ${getItemClasses(metrics.length)}`}>
{/* Label */}
<div className="text-sm text-gray-600 font-medium">
{metric.label}
</div>
{/* Large Metric Value */}
<div className="text-4xl sm:text-5xl lg:text-6xl font-bold text-purple-600">
{metric.value}
</div>
{/* Description Box */}
<div
className="bg-purple-50 rounded-lg p-4 lg:p-5 text-center mt-4"
style={{
backgroundColor: 'rgba(139, 92, 246, 0.08)'
}}
>
<p className="text-xs sm:text-sm text-gray-700 leading-relaxed">
{metric.description}
</p>
</div>
</div>
{/* Large Metric Value */}
<div className="text-4xl sm:text-5xl lg:text-6xl font-bold text-purple-600">
{metric.value}
</div>
{/* Description Box */}
<div
className="bg-purple-50 rounded-lg p-4 lg:p-5 text-center mt-4"
style={{
backgroundColor: 'rgba(139, 92, 246, 0.08)'
}}
>
<p className="text-xs sm:text-sm text-gray-700 leading-relaxed">
{metric.description}
</p>
</div>
</div>
))}
))}
</div>
</div>
</div>

View file

@ -7,10 +7,10 @@ export const layoutName = 'Metrics with Image'
export const layoutDescription = 'A slide layout with supporting image on the left and title, description, and metrics grid on the right. Can be used alternatively with MetricSlide.'
const metricsWithImageSlideSchema = z.object({
title: z.string().min(3).max(50).default('Competitive Advantage').meta({
title: z.string().min(3).max(40).default('Competitive Advantage').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(180).default('Ginyard International Co. stands out by offering custom digital solutions tailored to client needs, alongside long-term support to ensure lasting relationships and continuous adaptation.').meta({
description: z.string().min(10).max(150).default('Ginyard International Co. stands out by offering custom digital solutions tailored to client needs, alongside long-term support to ensure lasting relationships and continuous adaptation.').meta({
description: "Description text below the title",
}),
image: ImageSchema.default({

View file

@ -7,7 +7,7 @@ export const layoutName = 'Numbered Bullets'
export const layoutDescription = 'A slide layout with large title, supporting image, and numbered bullet points with descriptions.'
const numberedBulletsSlideSchema = z.object({
title: z.string().min(3).max(50).default('Market Validation').meta({
title: z.string().min(3).max(40).default('Market Validation').meta({
description: "Main title of the slide",
}),
image: ImageSchema.default({
@ -20,7 +20,7 @@ const numberedBulletsSlideSchema = z.object({
title: z.string().min(2).max(80).meta({
description: "Bullet point title",
}),
description: z.string().min(10).max(180).meta({
description: z.string().min(10).max(150).meta({
description: "Bullet point description",
}),
})).min(1).max(4).default([
@ -59,12 +59,12 @@ const NumberedBulletsSlideLayout: React.FC<NumberedBulletsSlideLayoutProps> = ({
return (
<>
{/* Import Google Fonts */}
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div
<div
className="w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white relative z-20 mx-auto overflow-hidden"
style={{
fontFamily: 'Poppins, sans-serif'
@ -104,7 +104,7 @@ const NumberedBulletsSlideLayout: React.FC<NumberedBulletsSlideLayoutProps> = ({
{String(index + 1).padStart(2, '0')}
</div>
</div>
{/* Content */}
<div className="flex-1 pt-2">
<h3 className="text-xl sm:text-2xl font-bold text-gray-900 mb-3">
@ -120,15 +120,15 @@ const NumberedBulletsSlideLayout: React.FC<NumberedBulletsSlideLayoutProps> = ({
{/* Decorative Wave Pattern at Bottom */}
<div className="absolute bottom-0 left-0 right-0 h-20 overflow-hidden">
<svg
className="w-full h-full opacity-20"
viewBox="0 0 1200 200"
fill="none"
<svg
className="w-full h-full opacity-20"
viewBox="0 0 1200 200"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0 100C300 150 600 50 900 100C1050 125 1125 100 1200 100V200H0V100Z"
fill="url(#wave-gradient)"
<path
d="M0 100C300 150 600 50 900 100C1050 125 1125 100 1200 100V200H0V100Z"
fill="url(#wave-gradient)"
/>
<defs>
<linearGradient id="wave-gradient" x1="0%" y1="0%" x2="100%" y2="0%">

View file

@ -13,20 +13,20 @@ const teamMemberSchema = z.object({
position: z.string().min(2).max(50).meta({
description: "Job title or position"
}),
description: z.string().max(180).meta({
description: z.string().max(150).meta({
description: "Brief description of the team member (around 100 characters)"
}),
image: ImageSchema
});
const teamSlideSchema = z.object({
title: z.string().min(3).max(50).default('Our Team Members').meta({
description: "Main title of the slide",
title: z.string().min(3).max(40).default('Our Team Members').meta({
description: "Main title of the slide",
}),
companyDescription: z.string().min(10).max(180).default('Ginyard International Co. is a leading provider of innovative digital solutions tailored for businesses. Our mission is to empower organizations to achieve their goals through cutting-edge technology and strategic partnerships.').meta({
companyDescription: z.string().min(10).max(150).default('Ginyard International Co. is a leading provider of innovative digital solutions tailored for businesses. Our mission is to empower organizations to achieve their goals through cutting-edge technology and strategic partnerships.').meta({
description: "Company description or team introduction text",
}),
teamMembers: z.array(teamMemberSchema).min(2).max(6).default([
teamMembers: z.array(teamMemberSchema).min(2).max(4).default([
{
name: 'Juliana Silva',
position: 'CEO',
@ -78,7 +78,7 @@ interface TeamSlideLayoutProps {
const TeamSlideLayout: React.FC<TeamSlideLayoutProps> = ({ data: slideData }) => {
const teamMembers = slideData?.teamMembers || []
// Function to determine grid classes based on number of team members
const getGridClasses = (count: number) => {
if (count <= 2) {
@ -93,12 +93,12 @@ const TeamSlideLayout: React.FC<TeamSlideLayoutProps> = ({ data: slideData }) =>
return (
<>
{/* Import Google Fonts */}
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<div
<div
className="w-full rounded-sm max-w-[1280px] shadow-lg max-h-[720px] aspect-video bg-white relative z-20 mx-auto overflow-hidden"
style={{
fontFamily: 'Poppins, sans-serif'
@ -107,8 +107,8 @@ const TeamSlideLayout: React.FC<TeamSlideLayoutProps> = ({ data: slideData }) =>
{/* Decorative Wave Pattern */}
<div className="absolute bottom-0 left-0 w-80 h-40 opacity-10 overflow-hidden">
<svg className="w-full h-full" viewBox="0 0 300 150" fill="none">
<path d="M0 75C75 50 150 100 225 75C262.5 62.5 300 75 300 75V150H0V75Z" fill="#8b5cf6" opacity="0.3"/>
<path d="M0 100C100 125 200 75 300 100V125C225 112.5 150 125 75 112.5L0 100Z" fill="#8b5cf6" opacity="0.2"/>
<path d="M0 75C75 50 150 100 225 75C262.5 62.5 300 75 300 75V150H0V75Z" fill="#8b5cf6" opacity="0.3" />
<path d="M0 100C100 125 200 75 300 100V125C225 112.5 150 125 75 112.5L0 100Z" fill="#8b5cf6" opacity="0.2" />
</svg>
</div>
@ -143,7 +143,7 @@ const TeamSlideLayout: React.FC<TeamSlideLayoutProps> = ({ data: slideData }) =>
className="w-full h-full object-cover"
/>
</div>
{/* Member Info */}
<div>
<h3 className="text-lg font-semibold text-gray-900">