Merge branch 'feat/custom_schema_and_layout' into feat/hot_reload_layout_preview

This commit is contained in:
Shiva Raj Badu 2025-07-20 20:31:53 +05:45 committed by GitHub
commit ce4e665e5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 278 additions and 1000 deletions

View file

@ -42,85 +42,15 @@ You are an expert presentation creator. Generate structured presentations based
- Ensure titles create a **logical flow** through the presentation
- Keep titles **concise but meaningful**
### Slide Body Content
- Use **full markdown formatting** for rich content structure
- Apply consistent formatting:
- `**bold**` for key concepts and emphasis
- `*italic*` for secondary emphasis or definitions
- `- or *` for bullet points and lists
- `> ` for quotes or callouts
- `### ` for subsections within slides
- ``` for code blocks (when applicable)
- `inline code` for technical terms or specific terminology
### Content Structure Per Slide
- **Opening/Hook**: Start with engaging content
- **Main Points**: 3-5 key points maximum per slide
- **Supporting Details**: Brief explanations or examples
- **Visual Cues**: Suggest where charts, images, or diagrams would be beneficial
- **Transitions**: Natural flow to next slide topic
### Speaker Notes
- Include **comprehensive speaker notes** for each slide
- Provide **additional context** not covered in slide content
- Add **timing suggestions** and **delivery tips**
- Include **visual element descriptions** (charts, images, icons)
- Specify if notes apply to **specific slides** or **entire presentation**
- Add **interaction opportunities** (questions, polls, discussions)
## Quality Standards
### Content Quality
- Ensure **factual accuracy** and **current information**
- Maintain **consistent tone** throughout presentation
- Create **logical progression** between slides
- Include **actionable insights** where appropriate
- Balance **depth and accessibility** for target audience
### Formatting Consistency
- Use **uniform markdown styling** across all slides
- Maintain **consistent bullet point structure**
- Apply **appropriate heading levels**
- Ensure **readable content density**
### Language and Tone
- Generate content in the **specified language**
- Adapt **tone and complexity** to target audience
- Use **active voice** and **clear, direct language**
- Include **engaging elements** (questions, scenarios, examples)
## Special Considerations
### Slide Count Compliance
- Generate **exactly** the number of slides requested
- Distribute content **evenly** across slides
- Ensure **no slide is significantly longer** than others
- Create **balanced information flow**
### Visual Integration
- Suggest **relevant visual elements** in notes
- Indicate **optimal placement** for charts, graphs, images
- Recommend **slide layouts** for different content types
- Specify **color schemes** or **design elements** when relevant
### Interactivity Elements
- Include **audience engagement opportunities**
- Suggest **discussion points** or **questions**
- Recommend **interactive elements** (polls, breakout sessions)
- Provide **transition phrases** between sections
## Validation Checklist
Before finalizing, ensure:
- [ ] Exact number of slides generated
- [ ] All titles are plain text (no markdown)
- [ ] All slide bodies use proper markdown formatting
- [ ] Comprehensive notes provided for each slide
- [ ] Logical flow between slides
- [ ] Consistent formatting throughout
- [ ] Content appropriate for specified language
- [ ] No slide title appears in slide body
- [ ] Speaker notes clearly indicate scope (slide-specific or presentation-wide)
"""

View file

@ -9,7 +9,7 @@ from utils.llm_provider import (
get_small_model,
is_google_selected,
)
from utils.schema_utils import remove_fields_from_schema
from utils.schema_utils import remove_fields_from_schema, generate_constraint_sentences
system_prompt = """
Generate structured slide based on provided title and outline, follow mentioned steps and notes and provide structured output.
@ -19,6 +19,7 @@ system_prompt = """
2. Generate structured slide based on the outline and title.
# Notes
- **Strictly follow the max and min character limit for each property in the slide.**
- Slide body should not use words like "This slide", "This presentation".
- Rephrase the slide body to make it flow naturally.
- Do not use markdown formatting in slide body.
@ -35,11 +36,12 @@ def get_user_prompt(title: str, outline: str):
"""
def get_prompt_to_generate_slide_content(title: str, outline: str):
def get_prompt_to_generate_slide_content(title: str, outline: str, schema_constraints: str = ""):
return [
{
"role": "system",
"content": system_prompt,
"content": system_prompt + f"\n{schema_constraints}",
},
{
"role": "user",
@ -52,6 +54,8 @@ async def get_slide_content_from_type_and_outline(
slide_layout: SlideLayoutModel, outline: SlideOutlineModel
):
model = get_small_model()
schema_constraints = generate_constraint_sentences(slide_layout.json_schema)
if not is_google_selected():
client = get_llm_client()
@ -60,6 +64,7 @@ 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",
@ -79,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,
system_instruction=system_prompt + f"\n{schema_constraints}",
response_mime_type="application/json",
response_json_schema=slide_layout.json_schema,
),

View file

@ -16,6 +16,7 @@ from utils.get_env import (
)
def get_llm_provider():
try:
return LLMProvider(get_llm_provider_env())
@ -77,9 +78,9 @@ def get_llm_api_key():
def get_llm_client():
client = AsyncOpenAI(
base_url=get_model_base_url(),
api_key=get_llm_api_key(),
)
base_url=get_model_base_url(),
api_key=get_llm_api_key(),
)
return client

View file

@ -48,3 +48,72 @@ def remove_fields_from_schema(schema: dict, fields_to_remove: List[str]):
]
return schema
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")
elif max_length is not None:
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")
# 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")
elif max_items is not None:
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")
# 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}[*]")
# 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):
extract_constraints_recursive(value, prefix)
# Start extraction from the root schema
extract_constraints_recursive(schema)
return "\n".join(constraints)

View file

@ -76,6 +76,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
<div className="">
<div
id="presentation-slides-wrapper"
className="mx-auto flex flex-col items-center overflow-hidden justify-center "
>
@ -102,7 +103,6 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
presentationData.slides.length > 0 &&
presentationData.slides.map((slide: any, index: number) => (
<div key={index} className="w-full">
{renderSlideContent(slide, false)}
</div>
))}

View file

@ -1,15 +1,13 @@
import { ApiError } from "@/models/errors";
import { NextRequest, NextResponse } from "next/server";
import puppeteer, { Browser, ElementHandle } from "puppeteer";
import puppeteer, { Browser, ElementHandle, Page } from "puppeteer";
import { ElementAttributes, SlideAttributesResult } from "@/types/element_attibutes";
import { convertElementAttributesToPptxSlides } from "@/utils/pptx_models_utils";
import { PptxPresentationModel } from "@/types/pptx_models";
import fs from "fs";
import path from "path";
import crypto from "crypto";
import sharp from "sharp";
import { v4 as uuidv4 } from 'uuid';
// Interface for getAllChildElementsAttributes function arguments
interface GetAllChildElementsAttributesArgs {
element: ElementHandle<Element>;
rootRect?: { left: number; top: number; width: number; height: number } | null;
@ -23,41 +21,27 @@ interface GetAllChildElementsAttributesArgs {
export async function GET(request: NextRequest) {
let browser: Browser | null = null;
let page: Page | null = null;
try {
const id = await getPresentationId(request);
browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
[browser, page] = await getBrowserAndPage(id);
const screenshotsDir = getScreenshotsDir();
// Ensure screenshots directory exists
const tempDir = process.env.TEMP_DIRECTORY;
if (!tempDir) {
console.warn('TEMP_DIRECTORY environment variable not set, skipping screenshot');
return undefined;
}
const screenshotsDir = path.join(tempDir, 'screenshots');
if (!fs.existsSync(screenshotsDir)) {
fs.mkdirSync(screenshotsDir, { recursive: true });
}
const slides = await getSlides(browser, id);
const slides = await getSlides(page);
const slides_attributes = await getSlidesAttributes(slides, screenshotsDir);
const slides_pptx_models = convertElementAttributesToPptxSlides(slides_attributes.elements, slides_attributes.backgroundColors);
await postProcessSlidesAttributes(slides_attributes, screenshotsDir);
const slides_pptx_models = convertElementAttributesToPptxSlides(slides_attributes);
const presentation_pptx_model: PptxPresentationModel = {
slides: slides_pptx_models,
};
if (browser) {
await browser.close();
}
await closeBrowserAndPage(browser, page);
return NextResponse.json(presentation_pptx_model);
} catch (error: any) {
console.error(error);
if (browser) {
await browser.close();
}
await closeBrowserAndPage(browser, page);
if (error instanceof ApiError) {
return NextResponse.json(error, { status: 400 });
}
@ -73,42 +57,19 @@ async function getPresentationId(request: NextRequest) {
return id;
}
async function getSlidesAttributes(slides: ElementHandle<Element>[], screenshotsDir: string) {
const slideResults: SlideAttributesResult[] = [];
//? Can't use Promise.all because of the screenshot
//? taking screenshot with mess up position of elements
for (const slide of slides) {
const result = await getAllChildElementsAttributes({ element: slide, screenshotsDir });
slideResults.push(result);
}
async function getBrowserAndPage(id: string): Promise<[Browser, Page]> {
const browser = await puppeteer.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--disable-web-security',
'--window-size=1920,1080'
],
});
const elements = slideResults.map(result => result.elements);
const backgroundColors = slideResults.map(result => result.backgroundColor);
return {
elements,
backgroundColors
};
}
async function getSlides(browser: Browser, id: string) {
const slides_wrapper = await getSlidesWrapper(browser, id);
const slides = await slides_wrapper.$$(":scope > div > div");
return slides;
}
async function getSlidesWrapper(browser: Browser, id: string): Promise<ElementHandle<Element>> {
const page = await getPresentationPage(browser, id);
const slides_wrapper = await page.$("#presentation-slides-wrapper");
if (!slides_wrapper) {
throw new ApiError("Presentation slides not found");
}
return slides_wrapper;
}
async function getPresentationPage(browser: Browser, id: string) {
const page = await browser.newPage();
page.on('console', (msg) => {
@ -118,16 +79,116 @@ async function getPresentationPage(browser: Browser, id: string) {
});
await page.setViewport({ width: 1920, height: 1080, deviceScaleFactor: 1 });
await page.goto(`http://localhost/presentation?id=${id}`, {
await page.goto(`http://localhost/pdf-maker?id=${id}`, {
waitUntil: "networkidle0",
timeout: 60000,
});
return page;
return [browser, page];
}
async function closeBrowserAndPage(browser: Browser | null, page: Page | null) {
await page?.close();
await browser?.close();
}
function getScreenshotsDir() {
const tempDir = process.env.TEMP_DIRECTORY;
if (!tempDir) {
console.warn('TEMP_DIRECTORY environment variable not set, skipping screenshot');
throw new ApiError('TEMP_DIRECTORY environment variable not set');
}
const screenshotsDir = path.join(tempDir, 'screenshots');
if (!fs.existsSync(screenshotsDir)) {
fs.mkdirSync(screenshotsDir, { recursive: true });
}
return screenshotsDir;
}
async function postProcessSlidesAttributes(slidesAttributes: SlideAttributesResult[], screenshotsDir: string) {
for (const slideAttributes of slidesAttributes) {
for (const element of slideAttributes.elements) {
if (element.should_screenshot) {
const screenshotPath = await screenshotElement(element, screenshotsDir);
element.imageSrc = screenshotPath;
element.should_screenshot = false;
element.element = undefined;
}
}
}
}
async function screenshotElement(element: ElementAttributes, screenshotsDir: string) {
const screenshotPath = path.join(screenshotsDir, `${uuidv4()}.png`) as `${string}.png`;
// Hide all elements except the target element and its ancestors
await element.element?.evaluate((el) => {
const originalDisplays = new Map();
const hideAllExcept = (targetElement: Element) => {
const allElements = document.querySelectorAll('*');
allElements.forEach((elem) => {
if (targetElement === elem || targetElement.contains(elem) || elem.contains(targetElement)) {
return;
}
const computedStyle = window.getComputedStyle(elem);
originalDisplays.set(elem, computedStyle.display);
(elem as HTMLElement).style.display = 'none';
});
};
hideAllExcept(el);
(el as any).__restoreDisplays = () => {
originalDisplays.forEach((display, elem) => {
(elem as HTMLElement).style.display = display;
});
};
});
const screenshot = await element.element?.screenshot({ path: screenshotPath });
if (!screenshot) {
throw new ApiError("Failed to screenshot element");
}
await element.element?.evaluate((el) => {
if ((el as any).__restoreDisplays) {
(el as any).__restoreDisplays();
}
});
return screenshotPath;
}
async function getSlidesAttributes(slides: ElementHandle<Element>[], screenshotsDir: string): Promise<SlideAttributesResult[]> {
const slideAttributes = await Promise.all(slides.map(async (slide) => {
return await getAllChildElementsAttributes({ element: slide, screenshotsDir });
}));
return slideAttributes;
}
async function getSlides(page: Page) {
const slides_wrapper = await getSlidesWrapper(page);
const slides = await slides_wrapper.$$(":scope > div > div");
return slides;
}
async function getSlidesWrapper(page: Page): Promise<ElementHandle<Element>> {
const slides_wrapper = await page.$("#presentation-slides-wrapper");
if (!slides_wrapper) {
throw new ApiError("Presentation slides not found");
}
return slides_wrapper;
}
async function getAllChildElementsAttributes({ element, rootRect = null, depth = 0, inheritedFont, inheritedBackground, inheritedBorderRadius, screenshotsDir }: GetAllChildElementsAttributesArgs): Promise<SlideAttributesResult> {
// Get rootRect if not provided (first call)
const currentRootRect = rootRect || await element.evaluate((el) => {
const rect = el.getBoundingClientRect();
return {
@ -138,67 +199,23 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
};
});
// Check if this element is SVG or canvas or table
const tagName = await element.evaluate((el) => el.tagName.toLowerCase());
if (tagName === 'svg' || tagName === 'canvas' || tagName === 'table') {
return {
elements: [],
backgroundColor: undefined
};
// // Get basic attributes for the element
// const attributes = await getElementAttributes(element);
// // Take screenshot of SVG/canvas/table element with accurate colors and opacity
// const screenshotPath = await takeElementScreenshot(element, screenshotsDir);
// // Update image source to point to the screenshot
// if (screenshotPath) {
// attributes.imageSrc = screenshotPath;
// }
// // Adjust position relative to root
// if (attributes.position && attributes.position.left !== undefined && attributes.position.top !== undefined) {
// attributes.position = {
// left: attributes.position.left - currentRootRect.left,
// top: attributes.position.top - currentRootRect.top,
// width: attributes.position.width,
// height: attributes.position.height,
// };
// }
// // Return early without processing children for these elements
// return {
// elements: [attributes],
// backgroundColor: undefined
// };
}
// Get direct children only (not all descendants)
const directChildElementHandles = await element.$$(':scope > *');
const allResults: { attributes: ElementAttributes; depth: number }[] = [];
// Process direct children recursively
for (const childElementHandle of directChildElementHandles) {
// Get attributes for current child
const attributes = await getElementAttributes(childElementHandle);
// Apply inherited font only on elements that have direct text
if (inheritedFont && !attributes.font && attributes.innerText && attributes.innerText.trim().length > 0) {
attributes.font = inheritedFont;
}
// Apply inherited background only on elements that have shadow
if (inheritedBackground && !attributes.background && attributes.shadow) {
attributes.background = inheritedBackground;
}
// Apply inherited border radius if element doesn't have it
if (inheritedBorderRadius && !attributes.borderRadius) {
attributes.borderRadius = inheritedBorderRadius;
}
// Adjust position relative to root
if (attributes.position && attributes.position.left !== undefined && attributes.position.top !== undefined) {
attributes.position = {
left: attributes.position.left - currentRootRect.left,
@ -208,10 +225,18 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
};
}
// Add current child to results
if (attributes.tagName === 'svg' || attributes.tagName === 'canvas' || attributes.tagName === 'table') {
attributes.should_screenshot = true;
attributes.element = childElementHandle;
}
allResults.push({ attributes, depth });
// Recursively process children of this child
//? If the element is a svg, canvas, or table, we don't need to go deeper
if (attributes.should_screenshot) {
break;
}
const childResults = await getAllChildElementsAttributes({
element: childElementHandle,
rootRect: currentRootRect,
@ -224,7 +249,6 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
allResults.push(...childResults.elements.map(attr => ({ attributes: attr, depth: depth + 1 })));
}
// Find background color from elements with root position (only in first call)
let backgroundColor: string | undefined;
if (!rootRect) {
const elementsWithRootPosition = allResults.filter(({ attributes }) => {
@ -243,13 +267,15 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
}
}
// Filter results (only in first call)
const filteredResults = !rootRect ? allResults.filter(({ attributes }) => {
const hasBackground = attributes.background && attributes.background.color;
const hasBorder = attributes.border && attributes.border.color;
const hasShadow = attributes.shadow && attributes.shadow.color;
const hasText = attributes.innerText && attributes.innerText.trim().length > 0;
const hasImage = attributes.imageSrc;
const isSvg = attributes.tagName === 'svg';
const isCanvas = attributes.tagName === 'canvas';
const isTable = attributes.tagName === 'table';
const isRootPosition = attributes.position &&
attributes.position.left === 0 &&
@ -257,7 +283,7 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
attributes.position.width === currentRootRect.width &&
attributes.position.height === currentRootRect.height;
const hasOtherProperties = hasBackground || hasBorder || hasShadow || hasText || hasImage;
const hasOtherProperties = hasBackground || hasBorder || hasShadow || hasText || hasImage || isSvg || isCanvas || isTable;
return hasOtherProperties && !isRootPosition;
}) : allResults;
@ -274,7 +300,6 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
return zIndexB - zIndexA;
})
.map(({ attributes }) => {
// Set background color to backgroundColor for elements that have shadow but no background color
if (attributes.shadow && attributes.shadow.color && (!attributes.background || !attributes.background.color) && backgroundColor) {
attributes.background = {
color: backgroundColor,
@ -416,8 +441,6 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
} = {};
if (boxShadow && boxShadow !== 'none') {
// Handle multiple shadows (comma-separated) - find the first meaningful one
// Need to split on commas but not inside function calls like rgba()
const shadows: string[] = [];
let currentShadow = '';
let parenCount = 0;
@ -429,7 +452,6 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
} else if (char === ')') {
parenCount--;
} else if (char === ',' && parenCount === 0) {
// This comma is outside of any function call, so it separates shadows
shadows.push(currentShadow.trim());
currentShadow = '';
continue;
@ -437,7 +459,6 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
currentShadow += char;
}
// Add the last shadow
if (currentShadow.trim()) {
shadows.push(currentShadow.trim());
}
@ -450,7 +471,6 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
for (let i = 0; i < shadows.length; i++) {
const shadowStr = shadows[i];
// Parse the shadow to check if it has meaningful values
const shadowParts = shadowStr.split(' ');
const numericParts: number[] = [];
const colorParts: string[] = [];
@ -458,7 +478,6 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
let currentColor = '';
let inColorFunction = false;
// Parse each part
for (let j = 0; j < shadowParts.length; j++) {
const part = shadowParts[j];
const trimmedPart = part.trim();
@ -469,23 +488,19 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
continue;
}
// Check if this part starts a color function (rgba, rgb, hsl, hsla)
if (trimmedPart.match(/^(rgba?|hsla?)\s*\(/i)) {
inColorFunction = true;
currentColor = trimmedPart;
continue;
}
// If we're inside a color function, keep building it
if (inColorFunction) {
currentColor += ' ' + trimmedPart;
// Check if we've reached the end of the color function
const openParens = (currentColor.match(/\(/g) || []).length;
const closeParens = (currentColor.match(/\)/g) || []).length;
if (openParens <= closeParens) {
// Color function is complete
colorParts.push(currentColor);
currentColor = '';
inColorFunction = false;
@ -501,7 +516,6 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
}
}
// Check if the color is not completely transparent using colorToHex
let hasVisibleColor = false;
if (colorParts.length > 0) {
const shadowColor = colorParts.join(' ');
@ -509,34 +523,28 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
hasVisibleColor = !!(colorResult.hex && colorResult.hex !== '000000' && colorResult.opacity !== 0);
}
// Check if we have any non-zero numeric values (offset, blur, or spread)
const hasNonZeroValues = numericParts.some(value => value !== 0);
// Calculate a score for this shadow (higher is better)
let shadowScore = 0;
if (hasNonZeroValues) {
// Count non-zero numeric values
shadowScore += numericParts.filter(value => value !== 0).length;
}
if (hasVisibleColor) {
shadowScore += 2; // Bonus for visible color
shadowScore += 2;
}
// Select this shadow if it has a better score
if ((hasNonZeroValues || hasVisibleColor) && shadowScore > bestShadowScore) {
selectedShadow = shadowStr;
bestShadowScore = shadowScore;
}
}
// If no meaningful shadow found, use the first one
if (!selectedShadow && shadows.length > 0) {
selectedShadow = shadows[0];
}
if (selectedShadow) {
// Parse the selected shadow
const shadowParts = selectedShadow.split(' ');
const numericParts: number[] = [];
const colorParts: string[] = [];
@ -544,7 +552,6 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
let currentColor = '';
let inColorFunction = false;
// Parse each part
for (let i = 0; i < shadowParts.length; i++) {
const part = shadowParts[i];
const trimmedPart = part.trim();
@ -555,23 +562,19 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
continue;
}
// Check if this part starts a color function (rgba, rgb, hsl, hsla)
if (trimmedPart.match(/^(rgba?|hsla?)\s*\(/i)) {
inColorFunction = true;
currentColor = trimmedPart;
continue;
}
// If we're inside a color function, keep building it
if (inColorFunction) {
currentColor += ' ' + trimmedPart;
// Check if we've reached the end of the color function
const openParens = (currentColor.match(/\(/g) || []).length;
const closeParens = (currentColor.match(/\)/g) || []).length;
if (openParens <= closeParens) {
// Color function is complete
colorParts.push(currentColor);
currentColor = '';
inColorFunction = false;
@ -587,14 +590,12 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
}
}
// Handle different shadow formats
if (numericParts.length >= 2) {
const offsetX = numericParts[0];
const offsetY = numericParts[1];
const blurRadius = numericParts.length >= 3 ? numericParts[2] : 0;
const spreadRadius = numericParts.length >= 4 ? numericParts[3] : 0;
// Handle color - it can be anywhere in the parts
let shadowColor = 'rgba(0, 0, 0, 0.3)'; // default color
if (colorParts.length > 0) {
shadowColor = colorParts.join(' ');
@ -602,7 +603,6 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
const shadowColorResult = colorToHex(shadowColor);
// Create shadow object if we have any meaningful values or visible color
const hasValidValues = offsetX !== 0 || offsetY !== 0 || blurRadius > 0 || spreadRadius !== 0 ||
(shadowColorResult.hex && shadowColorResult.hex !== '000000' && shadowColorResult.opacity !== 0);
@ -650,24 +650,19 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
const lineHeight = computedStyles.lineHeight;
const innerText = el.textContent || '';
// Check if text is multiline by looking for newline characters or checking if text wraps due to bounds
const htmlEl = el as HTMLElement;
// Get font size for comparison
const fontSize = parseFloat(computedStyles.fontSize);
const computedLineHeight = parseFloat(computedStyles.lineHeight);
// Estimate single line height (use computed line height if available, otherwise use font size * 1.2)
const singleLineHeight = !isNaN(computedLineHeight) ? computedLineHeight : fontSize * 1.2;
// Check for multiline text
const hasExplicitLineBreaks = innerText.includes('\n') || innerText.includes('\r') || innerText.includes('\r\n');
const hasTextWrapping = htmlEl.offsetHeight > singleLineHeight * 2; // Allow some tolerance
const hasTextWrapping = htmlEl.offsetHeight > singleLineHeight * 2;
const hasOverflow = htmlEl.scrollHeight > htmlEl.clientHeight;
const isMultiline = hasExplicitLineBreaks || hasTextWrapping || hasOverflow;
// Only return line height if text is multiline
if (isMultiline && lineHeight && lineHeight !== 'normal') {
const parsedLineHeight = parseFloat(lineHeight);
if (!isNaN(parsedLineHeight)) {
@ -804,53 +799,3 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
});
return attributes;
}
async function takeElementScreenshot(element: ElementHandle<Element>, screenshotsDir: string): Promise<string | undefined> {
try {
// Check element visibility and dimensions
const elementInfo = await element.evaluate((el) => {
const rect = el.getBoundingClientRect();
const styles = window.getComputedStyle(el);
// Check if element is visible
const isVisible = styles.visibility !== 'hidden' &&
styles.display !== 'none' &&
styles.opacity !== '0';
if (!isVisible || rect.width <= 0 || rect.height <= 0) {
return null;
}
return {
width: rect.width,
height: rect.height
};
});
if (!elementInfo) {
console.warn('Element is not visible or has invalid dimensions, skipping screenshot');
return undefined;
}
// Generate unique filename
const uuid = crypto.randomUUID();
const filename = `${uuid}.png`;
const filePath = path.join(screenshotsDir, filename);
// Take screenshot of the element with accurate colors and opacity
// This captures the element exactly as rendered in the browser with all CSS styles applied
await element.screenshot({
path: filePath as `${string}.png`,
type: 'png',
omitBackground: true // Use transparent background for better quality
});
console.log(`Screenshot saved: ${filePath}`);
return filePath;
} catch (error) {
console.error('Error taking element screenshot:', error);
return undefined;
}
}

View file

@ -35,7 +35,6 @@
"@tiptap/extension-underline": "^2.0.0",
"@tiptap/react": "^2.11.5",
"@tiptap/starter-kit": "^2.11.5",
"animejs": "^3.2.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
@ -44,19 +43,17 @@
"marked": "^15.0.11",
"next": "^14.2.14",
"next-themes": "^0.4.6",
"pptxgenjs": "^4.0.1",
"puppeteer": "^24.13.0",
"react": "^18",
"react-dom": "^18",
"react-element-to-jsx-string": "^15.0.0",
"react-redux": "^9.1.2",
"recharts": "^2.15.4",
"sharp": "^0.34.3",
"sonner": "^2.0.6",
"tailwind-merge": "^2.5.3",
"tailwind-scrollbar-hide": "^2.0.0",
"tailwindcss-animate": "^1.0.7",
"tiptap-markdown": "^0.8.10",
"uuid": "^11.1.0",
"zod": "^4.0.5"
},
"devDependencies": {
@ -65,7 +62,6 @@
"@types/puppeteer": "^5.4.7",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/sharp": "^0.32.0",
"@types/uuid": "^10.0.0",
"cypress": "^14.3.3",
"tailwindcss": "^3.4.1",
@ -119,12 +115,6 @@
"node": ">=6.9.0"
}
},
"node_modules/@base2/pretty-print-object": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz",
"integrity": "sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==",
"license": "BSD-2-Clause"
},
"node_modules/@cypress/request": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.8.tgz",
@ -238,15 +228,6 @@
"react": ">=16.8.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz",
"integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz",
@ -285,402 +266,6 @@
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz",
"integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.2.0"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz",
"integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.2.0"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz",
"integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz",
"integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz",
"integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz",
"integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz",
"integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==",
"cpu": [
"ppc64"
],
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz",
"integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==",
"cpu": [
"s390x"
],
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz",
"integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz",
"integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz",
"integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz",
"integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.2.0"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz",
"integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.2.0"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz",
"integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==",
"cpu": [
"ppc64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.2.0"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz",
"integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==",
"cpu": [
"s390x"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.2.0"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz",
"integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.2.0"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz",
"integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.2.0"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz",
"integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.2.0"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz",
"integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==",
"cpu": [
"wasm32"
],
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.4.4"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz",
"integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz",
"integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz",
"integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -2783,16 +2368,6 @@
"@types/react": "^18.0.0"
}
},
"node_modules/@types/sharp": {
"version": "0.32.0",
"resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.32.0.tgz",
"integrity": "sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw==",
"deprecated": "This is a stub types definition. sharp provides its own type definitions, so you do not need this installed.",
"dev": true,
"dependencies": {
"sharp": "*"
}
},
"node_modules/@types/sinonjs__fake-timers": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz",
@ -2852,12 +2427,6 @@
"node": ">=8"
}
},
"node_modules/animejs": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/animejs/-/animejs-3.2.2.tgz",
"integrity": "sha512-Ao95qWLpDPXXM+WrmwcKbl6uNlC5tjnowlaRYtuVDHHoygjtIPfDUoK9NthrlZsQSKjZXlmji2TrBUAVbiH0LQ==",
"license": "MIT"
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
@ -3597,18 +3166,6 @@
"react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -3627,15 +3184,6 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
@ -3691,6 +3239,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
"dev": true,
"license": "MIT"
},
"node_modules/cosmiconfig": {
@ -4014,14 +3563,6 @@
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
"engines": {
"node": ">=8"
}
},
"node_modules/detect-node-es": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
@ -4819,12 +4360,6 @@
"node": ">=0.10"
}
},
"node_modules/https": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz",
"integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==",
"license": "ISC"
},
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
@ -4869,27 +4404,6 @@
],
"license": "BSD-3-Clause"
},
"node_modules/image-size": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
"integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
"license": "MIT",
"dependencies": {
"queue": "6.0.2"
},
"bin": {
"image-size": "bin/image-size.js"
},
"engines": {
"node": ">=16.x"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/immer": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
@ -4926,12 +4440,6 @@
"node": ">=8"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ini": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
@ -5063,15 +4571,6 @@
"node": ">=8"
}
},
"node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@ -5105,12 +4604,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -5230,18 +4723,6 @@
"verror": "1.10.0"
}
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/lazy-ass": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
@ -5252,15 +4733,6 @@
"node": "> 0.8"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
@ -5848,12 +5320,6 @@
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"license": "BlueOak-1.0.0"
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -6091,27 +5557,6 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT"
},
"node_modules/pptxgenjs": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-4.0.1.tgz",
"integrity": "sha512-TeJISr8wouAuXw4C1F/mC33xbZs/FuEG6nH9FG1Zj+nuPcGMP5YRHl6X+j3HSUnS1f3at6k75ZZXPMZlA5Lj9A==",
"license": "MIT",
"dependencies": {
"@types/node": "^22.8.1",
"https": "^1.0.0",
"image-size": "^1.2.1",
"jszip": "^3.10.1"
}
},
"node_modules/pptxgenjs/node_modules/@types/node": {
"version": "22.16.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.4.tgz",
"integrity": "sha512-PYRhNtZdm2wH/NT2k/oAJ6/f2VD2N2Dag0lGlx2vWgMSJXGNmlce5MiTQzoWAiIJtso30mjnfQCOKVH+kAQC/g==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/pretty-bytes": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
@ -6135,12 +5580,6 @@
"node": ">= 0.6.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@ -6479,15 +5918,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/queue": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
"license": "MIT",
"dependencies": {
"inherits": "~2.0.3"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -6533,27 +5963,6 @@
"react": "^18.3.1"
}
},
"node_modules/react-element-to-jsx-string": {
"version": "15.0.0",
"resolved": "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz",
"integrity": "sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ==",
"license": "MIT",
"dependencies": {
"@base2/pretty-print-object": "1.0.1",
"is-plain-object": "5.0.0",
"react-is": "18.1.0"
},
"peerDependencies": {
"react": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0",
"react-dom": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0"
}
},
"node_modules/react-element-to-jsx-string/node_modules/react-is": {
"version": "18.1.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz",
"integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==",
"license": "MIT"
},
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@ -6692,27 +6101,6 @@
"pify": "^2.3.0"
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readable-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@ -6945,53 +6333,6 @@
"node": ">=10"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/sharp": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz",
"integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==",
"hasInstallScript": true,
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.4",
"semver": "^7.7.2"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.34.3",
"@img/sharp-darwin-x64": "0.34.3",
"@img/sharp-libvips-darwin-arm64": "1.2.0",
"@img/sharp-libvips-darwin-x64": "1.2.0",
"@img/sharp-libvips-linux-arm": "1.2.0",
"@img/sharp-libvips-linux-arm64": "1.2.0",
"@img/sharp-libvips-linux-ppc64": "1.2.0",
"@img/sharp-libvips-linux-s390x": "1.2.0",
"@img/sharp-libvips-linux-x64": "1.2.0",
"@img/sharp-libvips-linuxmusl-arm64": "1.2.0",
"@img/sharp-libvips-linuxmusl-x64": "1.2.0",
"@img/sharp-linux-arm": "0.34.3",
"@img/sharp-linux-arm64": "0.34.3",
"@img/sharp-linux-ppc64": "0.34.3",
"@img/sharp-linux-s390x": "0.34.3",
"@img/sharp-linux-x64": "0.34.3",
"@img/sharp-linuxmusl-arm64": "0.34.3",
"@img/sharp-linuxmusl-x64": "0.34.3",
"@img/sharp-wasm32": "0.34.3",
"@img/sharp-win32-arm64": "0.34.3",
"@img/sharp-win32-ia32": "0.34.3",
"@img/sharp-win32-x64": "0.34.3"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -7096,19 +6437,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/simple-swizzle/node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
"node_modules/slice-ansi": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
@ -7251,21 +6579,6 @@
"bare-events": "^2.2.0"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string_decoder/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@ -7817,6 +7130,7 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/universalify": {
@ -7897,6 +7211,18 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",

View file

@ -52,13 +52,13 @@
"react-dom": "^18",
"react-redux": "^9.1.2",
"recharts": "^2.15.4",
"sharp": "^0.34.3",
"sonner": "^2.0.6",
"tailwind-merge": "^2.5.3",
"tailwind-scrollbar-hide": "^2.0.0",
"tailwindcss-animate": "^1.0.7",
"tiptap-markdown": "^0.8.10",
"ws": "^8.18.0",
"uuid": "^11.1.0",
"zod": "^4.0.5"
},
"devDependencies": {
@ -67,7 +67,6 @@
"@types/puppeteer": "^5.4.7",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/sharp": "^0.32.0",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.5.13",
"cypress": "^14.3.3",

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(100).default('Product Overview').meta({
title: z.string().min(3).max(50).default('Product Overview').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(800).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(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: "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(100).default('Solutions').meta({
title: z.string().min(3).max(50).default('Solutions').meta({
description: "Main title of the slide",
}),
image: ImageSchema.default({
@ -17,10 +17,10 @@ const bulletIconsOnlySlideSchema = z.object({
description: "Supporting image for the slide",
}),
bulletPoints: z.array(z.object({
title: z.string().min(2).max(100).meta({
title: z.string().min(2).max(80).meta({
description: "Bullet point title",
}),
subtitle: z.string().min(5).max(200).optional().meta({
subtitle: z.string().min(5).max(180).optional().meta({
description: "Optional short subtitle or brief explanation",
}),
icon: IconSchema,

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(100).default('Problem').meta({
title: z.string().min(3).max(50).default('Problem').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(500).default('Businesses face challenges with outdated technology and rising costs, limiting efficiency and growth in competitive markets.').meta({
description: z.string().max(180).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({
@ -20,10 +20,10 @@ const bulletWithIconsSlideSchema = z.object({
description: "Supporting image for the slide",
}),
bulletPoints: z.array(z.object({
title: z.string().min(2).max(100).meta({
title: z.string().min(2).max(80).meta({
description: "Bullet point title",
}),
description: z.string().min(10).max(300).meta({
description: z.string().min(10).max(180).meta({
description: "Bullet point description",
}),
icon: IconSchema,

View file

@ -17,10 +17,10 @@ const chartDataSchema = z.object({
});
const chartWithBulletsSlideSchema = z.object({
title: z.string().min(3).max(100).default('Market Size').meta({
title: z.string().min(3).max(50).default('Market Size').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(500).default('Businesses face challenges with outdated technology and rising costs, limiting efficiency and growth in competitive markets.').meta({
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: "Description text below the title",
}),
chartType: z.enum(['bar', 'line', 'pie', 'area', 'scatter']).default('bar').meta({
@ -51,10 +51,10 @@ const chartWithBulletsSlideSchema = z.object({
description: "Whether to show chart tooltip",
}),
bulletPoints: z.array(z.object({
title: z.string().min(2).max(100).meta({
title: z.string().min(2).max(80).meta({
description: "Bullet point title",
}),
description: z.string().min(10).max(300).meta({
description: z.string().min(10).max(180).meta({
description: "Bullet point description",
}),
icon: IconSchema,

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(100).default('Product Overview').meta({
title: z.string().min(3).max(50).default('Product Overview').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(800).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(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: "Main description text content",
}),
presenterName: z.string().min(2).max(50).default('John Doe').meta({

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(100).default('Competitive Advantage').meta({
title: z.string().min(3).max(50).default('Competitive Advantage').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(600).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(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: "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(100).default('Market Validation').meta({
title: z.string().min(3).max(50).default('Market Validation').meta({
description: "Main title of the slide",
}),
image: ImageSchema.default({
@ -17,10 +17,10 @@ const numberedBulletsSlideSchema = z.object({
description: "Supporting image for the slide",
}),
bulletPoints: z.array(z.object({
title: z.string().min(2).max(100).meta({
title: z.string().min(2).max(80).meta({
description: "Bullet point title",
}),
description: z.string().min(10).max(300).meta({
description: z.string().min(10).max(180).meta({
description: "Bullet point description",
}),
})).min(1).max(4).default([

View file

@ -10,7 +10,7 @@ const tableOfContentsSlideSchema = z.object({
number: z.number().min(1).meta({
description: "Section number"
}),
title: z.string().min(1).max(100).meta({
title: z.string().min(1).max(80).meta({
description: "Section title"
}),
pageNumber: z.string().min(1).max(10).meta({

View file

@ -13,17 +13,17 @@ const teamMemberSchema = z.object({
position: z.string().min(2).max(50).meta({
description: "Job title or position"
}),
description: z.string().min(10).max(120).meta({
description: z.string().max(180).meta({
description: "Brief description of the team member (around 100 characters)"
}),
image: ImageSchema
});
const teamSlideSchema = z.object({
title: z.string().min(3).max(100).default('Our Team Members').meta({
description: "Main title of the slide",
title: z.string().min(3).max(50).default('Our Team Members').meta({
description: "Main title of the slide",
}),
companyDescription: z.string().min(10).max(600).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(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({
description: "Company description or team introduction text",
}),
teamMembers: z.array(teamMemberSchema).min(2).max(6).default([

View file

@ -1,3 +1,5 @@
import { ElementHandle } from "puppeteer";
export interface ElementAttributes {
tagName: string;
id?: string;
@ -57,6 +59,8 @@ export interface ElementAttributes {
shape?: 'rectangle' | 'circle';
connectorType?: string;
textWrap?: boolean;
should_screenshot?: boolean;
element?: ElementHandle<Element>;
}
export interface SlideAttributesResult {

View file

@ -1,4 +1,4 @@
import { ElementAttributes } from "@/types/element_attibutes";
import { ElementAttributes, SlideAttributesResult } from "@/types/element_attibutes";
import {
PptxSlideModel,
PptxTextBoxModel,
@ -63,15 +63,14 @@ function convertLineHeightToRelative(lineHeight?: number, fontSize?: number): nu
}
/**
* Converts ElementAttributes[][] to PptxSlideModel[]
* Each inner array represents elements on a slide
* Converts SlideAttributesResult[] to PptxSlideModel[]
* Each SlideAttributesResult represents elements on a slide
*/
export function convertElementAttributesToPptxSlides(
slidesAttributes: ElementAttributes[][],
backgroundColors?: (string | undefined)[]
slidesAttributes: SlideAttributesResult[]
): PptxSlideModel[] {
return slidesAttributes.map((slideElements, index) => {
const shapes = slideElements.map(element => {
return slidesAttributes.map((slideAttributes) => {
const shapes = slideAttributes.elements.map(element => {
return convertElementToPptxShape(element);
}).filter(Boolean); // Remove any null/undefined shapes
@ -80,9 +79,9 @@ export function convertElementAttributesToPptxSlides(
};
// Add background color if available
if (backgroundColors && backgroundColors[index]) {
if (slideAttributes.backgroundColor) {
slide.background = {
color: backgroundColors[index]!,
color: slideAttributes.backgroundColor,
opacity: 1.0
};
}