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:
commit
362ddd39a8
19 changed files with 261 additions and 231 deletions
|
|
@ -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}),
|
||||
|
|
|
|||
|
|
@ -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()}),
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
})
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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%">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue