diff --git a/electron/resources/ui/homepage/index.html b/electron/resources/ui/homepage/index.html index 7922ce57..9578692f 100644 --- a/electron/resources/ui/homepage/index.html +++ b/electron/resources/ui/homepage/index.html @@ -6,13 +6,6 @@ Presenton

{data.title}

-
-
+
+
{data.requestSnippet?.fileName}

-
-
-                  
-                    {data.requestSnippet?.content}
-                  
-                
+
+
+                
{data.responseSnippet?.fileName}

-
-
-                
-                  {data.responseSnippet?.content}
-                
-              
+
+
+              
diff --git a/electron/servers/nextjs/app/presentation-templates/Code/CardsGridSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Code/CardsGridSlide.tsx index 683afe62..ffae5143 100644 --- a/electron/servers/nextjs/app/presentation-templates/Code/CardsGridSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Code/CardsGridSlide.tsx @@ -139,7 +139,7 @@ const CodeSlide04FeatureGrid = ({ data }: { data: Partial }) => { url={feature.icon?.__icon_url__} strokeColor={"currentColor"} className="h-[24px] w-[24px] object-contain" - color="var(--primary-text, #000000)" + color="var(--primary-text, #ffffff)" title={feature.icon.__icon_query__} /> diff --git a/electron/servers/nextjs/app/presentation-templates/Code/CodeExplanationSplitSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Code/CodeExplanationSplitSlide.tsx index 258e3057..b060a29e 100644 --- a/electron/servers/nextjs/app/presentation-templates/Code/CodeExplanationSplitSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Code/CodeExplanationSplitSlide.tsx @@ -1,126 +1,5 @@ import * as z from "zod"; - -const CODE_BLOCK_MAX_FONT_SIZE = 16; -const CODE_BLOCK_MIN_FONT_SIZE = 8; -const CODE_BLOCK_WIDTH = 506; -const CODE_BLOCK_HEIGHT = 430; -const CODE_CHAR_WIDTH_RATIO = 0.62; -const CODE_LINE_HEIGHT_RATIO = 1.25; -const CODE_FONT_FAMILY = "var(--code-font-family,'Liberation Mono', monospace)"; - -function splitCollapsedPythonImports(line: string) { - const importSegments = line - .split(/(?=\sfrom\s+[A-Za-z0-9_.]+\s+import\s+)/g) - .map((segment) => segment.trim()) - .filter(Boolean); - - return importSegments.length > 1 ? importSegments : [line]; -} - -function expandInlinePythonStatement(line: string) { - const inlineReturnMatch = line.match(/^(\s*def\s+[^(]+\([^)]*\):)\s+return\s+(.+)$/); - - if (!inlineReturnMatch) { - return [line]; - } - - return [inlineReturnMatch[1], ` return ${inlineReturnMatch[2]}`]; -} - -function expandPathListAssignment(line: string) { - const trimmedLine = line.trim(); - - if (!trimmedLine.startsWith("urlpatterns = [") || !trimmedLine.endsWith("]")) { - return [line]; - } - - const pathCalls = trimmedLine.match(/path\([^)]*\)/g); - - if (!pathCalls?.length) { - return [line]; - } - - return [ - "urlpatterns = [", - ...pathCalls.map((pathCall) => ` ${pathCall},`), - "]", - ]; -} - -function normalizePythonCode(content: string) { - const normalizedLines: string[] = []; - - for (const line of content.split("\n")) { - const importLines = splitCollapsedPythonImports(line); - - for (const importLine of importLines) { - const expandedPathLines = expandPathListAssignment(importLine); - - for (const expandedPathLine of expandedPathLines) { - normalizedLines.push(...expandInlinePythonStatement(expandedPathLine)); - } - } - } - - return normalizedLines.join("\n").replace(/\n{3,}/g, "\n\n"); -} - -function normalizeCodeContent(language?: string, content?: string) { - let normalizedContent = (content || "") - .replace(/\r\n?/g, "\n") - .replace(/\\\[/g, "[") - .replace(/\\\]/g, "]") - .trimEnd(); - - if (language?.toLowerCase() === "python") { - normalizedContent = normalizePythonCode(normalizedContent); - } - - return normalizedContent; -} - -function getCodeBlockTypography(content?: string) { - const normalizedLines = (content || "").replace(/\t/g, " ").split("\n"); - const longestLineLength = Math.max( - 1, - ...normalizedLines.map((line) => line.length) - ); - - for (let fontSize = CODE_BLOCK_MAX_FONT_SIZE; fontSize >= CODE_BLOCK_MIN_FONT_SIZE; fontSize -= 0.5) { - const lineHeight = Math.round(fontSize * CODE_LINE_HEIGHT_RATIO); - const fitsWidth = longestLineLength * fontSize * CODE_CHAR_WIDTH_RATIO <= CODE_BLOCK_WIDTH; - const fitsHeight = normalizedLines.length * lineHeight <= CODE_BLOCK_HEIGHT; - - if (fitsWidth && fitsHeight) { - return { fontSize, lineHeight }; - } - } - - return { - fontSize: CODE_BLOCK_MIN_FONT_SIZE, - lineHeight: Math.round(CODE_BLOCK_MIN_FONT_SIZE * CODE_LINE_HEIGHT_RATIO), - }; -} - -function getCodeLineRuns(content: string, lineHeight: number) { - const codeLineRuns: { text: string; marginTop: number }[] = []; - let blankLineCount = 0; - - for (const line of content.split("\n")) { - if (line.length === 0) { - blankLineCount += 1; - continue; - } - - codeLineRuns.push({ - text: line, - marginTop: blankLineCount * lineHeight, - }); - blankLineCount = 0; - } - - return codeLineRuns; -} +import { fitCodeBlock, PRISM_CODE_BLOCK_STYLES } from "./codeBlockFitting"; export const slideLayoutId = "code-explanation-split-slide"; export const slideLayoutName = "Code Explanation Split Slide"; @@ -186,16 +65,19 @@ const CodeSlide02CodeExplanationSplit = ({ }: { data: Partial; }) => { - const normalizedCodeContent = normalizeCodeContent( - data.codeSnippet?.language, - data.codeSnippet?.content - ); - const codeTypography = getCodeBlockTypography(normalizedCodeContent); - const codeLineRuns = getCodeLineRuns(normalizedCodeContent, codeTypography.lineHeight); + const fittedCode = fitCodeBlock({ + language: data.codeSnippet?.language, + content: data.codeSnippet?.content, + maxWidth: 506, + maxHeight: 430, + maxFontSize: 16, + minFontSize: 8, + }); return ( <> +
- {codeLineRuns.map((codeLineRun, index) => ( -
- {codeLineRun.text} -
- ))} +
               
diff --git a/electron/servers/nextjs/app/presentation-templates/Code/WorkflowSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Code/WorkflowSlide.tsx index a9e45f54..95291b17 100644 --- a/electron/servers/nextjs/app/presentation-templates/Code/WorkflowSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Code/WorkflowSlide.tsx @@ -91,11 +91,16 @@ const CodeSlide06Workflow = ({ data }: { data: Partial }) => {

{data.title}

-
+
{data?.steps?.map((step, index) => (
segment.trim()) + .filter(Boolean); + + return importSegments.length > 1 ? importSegments : [line]; +} + +function expandInlinePythonStatement(line: string) { + const inlineReturnMatch = line.match(/^(\s*def\s+[^(]+\([^)]*\):)\s+return\s+(.+)$/); + + if (!inlineReturnMatch) { + return [line]; + } + + return [inlineReturnMatch[1], `return ${inlineReturnMatch[2]}`]; +} + +function expandPathListAssignment(line: string) { + const trimmedLine = line.trim(); + + if (!trimmedLine.startsWith("urlpatterns = [") || !trimmedLine.endsWith("]")) { + return [line]; + } + + const pathCalls = trimmedLine.match(/path\([^)]*\)/g); + + if (!pathCalls?.length) { + return [line]; + } + + return [ + "urlpatterns = [", + ...pathCalls.map((pathCall) => ` ${pathCall},`), + "]", + ]; +} + +function normalizePythonCode(content: string) { + const normalizedLines: string[] = []; + + for (const line of content.split("\n")) { + const importLines = splitCollapsedPythonImports(line); + + for (const importLine of importLines) { + const expandedPathLines = expandPathListAssignment(importLine); + + for (const expandedPathLine of expandedPathLines) { + normalizedLines.push(...expandInlinePythonStatement(expandedPathLine)); + } + } + } + + return normalizedLines.join("\n").replace(/\n{3,}/g, "\n\n"); +} + +function tryFormatJson(content: string) { + const trimmedContent = content.replace(/^\uFEFF/, "").trim(); + + if (!trimmedContent) { + return ""; + } + + const normalizedSeparatorsContent = trimmedContent + .replace(/^\s*\/\s*$/gm, ",") + .replace(/\r\n?/g, "\n") + .replace(/\n\s*:\s*/g, ": ") + .replace(/\n\s*,\s*/g, ", "); + + const parseAndFormat = (raw: string) => { + try { + const parsed = JSON.parse(raw); + + if (typeof parsed === "string") { + try { + return JSON.stringify(JSON.parse(parsed), null, 2); + } catch { + return JSON.stringify(parsed, null, 2); + } + } + + return JSON.stringify(parsed, null, 2); + } catch { + return null; + } + }; + + const extractedJsonMatch = normalizedSeparatorsContent.match(/[\[{][\s\S]*[\]}]/); + const extractedJsonCandidate = extractedJsonMatch?.[0]; + + const candidates = [ + normalizedSeparatorsContent, + trimmedContent, + extractedJsonCandidate, + ].filter((candidate): candidate is string => Boolean(candidate)); + + for (const candidate of candidates) { + const direct = parseAndFormat(candidate); + if (direct !== null) { + return direct; + } + + try { + const repairedJson = jsonrepair(candidate); + const repaired = parseAndFormat(repairedJson); + if (repaired !== null) { + return repaired; + } + } catch { + // Try next parsing strategy. + } + } + + const jsonLikeTokenMatch = normalizedSeparatorsContent.match( + /"(?:\\.|[^"\\])*"|true|false|null|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|[{}\[\]:,\/]/g + ); + + if (jsonLikeTokenMatch?.length) { + const normalizedTokens = jsonLikeTokenMatch.map((token) => (token === "/" ? "," : token)); + let rebuilt = ""; + + for (const token of normalizedTokens) { + if (token === ":" || token === ",") { + rebuilt = rebuilt.replace(/\s*$/, ""); + rebuilt += `${token} `; + continue; + } + + if (token === "}" || token === "]") { + rebuilt = rebuilt.replace(/\s*$/, ""); + rebuilt += token; + continue; + } + + if (token === "{" || token === "[") { + rebuilt = rebuilt.replace(/\s*$/, ""); + rebuilt += token; + continue; + } + + rebuilt += token; + } + + const rebuiltDirect = parseAndFormat(rebuilt); + if (rebuiltDirect !== null) { + return rebuiltDirect; + } + + try { + const rebuiltRepaired = parseAndFormat(jsonrepair(rebuilt)); + if (rebuiltRepaired !== null) { + return rebuiltRepaired; + } + } catch { + // Continue to best-effort line merge fallback below. + } + } + + const normalizedLines = normalizedSeparatorsContent + .replace(/\n\s*:\s*\n\s*/g, ": ") + .replace(/\n\s*\/\s*\n/g, ",\n") + .split("\n") + .map((line) => line.replace(/\s+$/g, "")); + + const mergedLines: string[] = []; + + for (const rawLine of normalizedLines) { + const trimmedLine = rawLine.trim(); + + if (!trimmedLine) { + continue; + } + + if (trimmedLine === ":") { + if (mergedLines.length > 0) { + mergedLines[mergedLines.length - 1] = `${mergedLines[mergedLines.length - 1]}:`; + } + continue; + } + + if (trimmedLine === "/") { + if (mergedLines.length > 0 && !mergedLines[mergedLines.length - 1].trim().endsWith(",")) { + mergedLines[mergedLines.length - 1] = `${mergedLines[mergedLines.length - 1]},`; + } + continue; + } + + const previousLine = mergedLines[mergedLines.length - 1]?.trim() || ""; + if (previousLine.endsWith(":")) { + mergedLines[mergedLines.length - 1] = `${mergedLines[mergedLines.length - 1]} ${trimmedLine}`; + continue; + } + + mergedLines.push(rawLine); + } + + for (let index = 0; index < mergedLines.length - 1; index += 1) { + const currentLine = mergedLines[index].trim(); + const nextLine = mergedLines[index + 1].trim(); + const currentEndsWithComma = currentLine.endsWith(","); + const currentIsContainerStart = currentLine.endsWith("{") || currentLine.endsWith("["); + const nextStartsNewKey = nextLine.startsWith("\""); + const nextIsContainerEnd = nextLine.startsWith("}") || nextLine.startsWith("]"); + + if (!currentEndsWithComma && !currentIsContainerStart && nextStartsNewKey && !nextIsContainerEnd) { + mergedLines[index] = `${mergedLines[index]},`; + } + } + + return mergedLines + .join("\n") + .replace(/,\s*([}\]])/g, "$1"); +} + +function isValidJsonContent(content: string) { + try { + JSON.parse(content.trim()); + return true; + } catch { + return false; + } +} + +function seemsJsonLike(content: string) { + const trimmed = content.trim(); + if (!trimmed) { + return false; + } + + if (/^[\[{]/.test(trimmed)) { + return true; + } + + return /"[^"\n]+"\s*:/.test(trimmed) || /[\[{][\s\S]*[\]}]/.test(trimmed); +} + +function unwrapMarkdownCodeFence(content: string) { + const trimmedContent = content.trim(); + const fencedCodeMatch = trimmedContent.match(/^```([^\n`]*)\n([\s\S]*?)\n```$/); + + if (!fencedCodeMatch) { + return { + content: content, + fenceLanguage: undefined as string | undefined, + }; + } + + return { + content: fencedCodeMatch[2], + fenceLanguage: fencedCodeMatch[1]?.trim().toLowerCase() || undefined, + }; +} + +function escapeHtml(content: string) { + return content + .replace(/&/g, "&") + .replace(//g, ">"); +} + +function resolvePrismLanguage(language?: string) { + const normalizedLanguage = language?.toLowerCase().trim(); + + if (!normalizedLanguage) { + return "clike"; + } + + if (normalizedLanguage.includes("json")) { + return "json"; + } + + if (normalizedLanguage.includes("python")) { + return "python"; + } + + if (normalizedLanguage === "ts") { + return "typescript"; + } + + if (normalizedLanguage === "js") { + return "javascript"; + } + + if (normalizedLanguage === "py") { + return "python"; + } + + if (normalizedLanguage === "sh" || normalizedLanguage === "shell") { + return "bash"; + } + + if (normalizedLanguage === "yml") { + return "yaml"; + } + + if (Prism.languages[normalizedLanguage]) { + return normalizedLanguage; + } + + return "clike"; +} + +function highlightCode(content: string, language?: string) { + const prismLanguage = resolvePrismLanguage(language); + const grammar = Prism.languages[prismLanguage]; + + if (!grammar) { + return { + html: escapeHtml(content), + prismLanguage, + }; + } + + try { + return { + html: Prism.highlight(content, grammar, prismLanguage), + prismLanguage, + }; + } catch { + return { + html: escapeHtml(content), + prismLanguage, + }; + } +} + +export function normalizeCodeContent(language?: string, content?: string) { + let normalizedContent = (content || "") + .replace(/\r\n?/g, "\n") + .replace(/\\\[/g, "[") + .replace(/\\\]/g, "]"); + const unwrappedContent = unwrapMarkdownCodeFence(normalizedContent); + normalizedContent = unwrappedContent.content.trimEnd(); + + const normalizedLanguage = language?.toLowerCase()?.trim() || unwrappedContent.fenceLanguage; + const isJsonLanguage = normalizedLanguage?.includes("json"); + const looksLikeJsonPayload = seemsJsonLike(normalizedContent); + + if (normalizedLanguage === "python") { + normalizedContent = normalizePythonCode(normalizedContent); + } else if (isJsonLanguage || looksLikeJsonPayload) { + const formattedJson = tryFormatJson(normalizedContent); + normalizedContent = formattedJson; + } + + return normalizedContent; +} + +function countRenderedLines(content: string, maxCharsPerLine: number) { + const rawLines = content.split("\n"); + let renderedLineCount = 0; + + for (const rawLine of rawLines) { + const expandedLine = rawLine.replace(/\t/g, " "); + + if (expandedLine.length === 0) { + renderedLineCount += 1; + continue; + } + + renderedLineCount += Math.max(1, Math.ceil(expandedLine.length / maxCharsPerLine)); + } + + return Math.max(1, renderedLineCount); +} + +function splitLineForLineBudget(line: string, maxCharsPerLine: number) { + if (line.length === 0) { + return [""]; + } + + const chunks: string[] = []; + + for (let start = 0; start < line.length; start += maxCharsPerLine) { + chunks.push(line.slice(start, start + maxCharsPerLine)); + } + + return chunks; +} + +function truncateContentToLineBudget( + content: string, + lineBudget: number, + maxCharsPerLine: number +) { + const linesForDisplay: string[] = []; + const rawLines = content.split("\n"); + + for (const rawLine of rawLines) { + const expandedLine = rawLine.replace(/\t/g, " "); + const chunks = splitLineForLineBudget(expandedLine, maxCharsPerLine); + + for (const chunk of chunks) { + if (linesForDisplay.length >= lineBudget) { + const lastLineIndex = Math.max(0, lineBudget - 1); + const ellipsis = "..."; + const existingLastLine = linesForDisplay[lastLineIndex] ?? ""; + linesForDisplay[lastLineIndex] = `${existingLastLine.slice( + 0, + Math.max(0, maxCharsPerLine - ellipsis.length) + )}${ellipsis}`; + return linesForDisplay.join("\n"); + } + + linesForDisplay.push(chunk); + } + } + + return linesForDisplay.join("\n"); +} + +function createTypographyCandidate( + normalizedContent: string, + fontSize: number, + maxWidth: number, + charWidthRatio: number, + lineHeightRatio: number +): TypographyCandidate { + const lineHeight = Math.max(1, Math.round(fontSize * lineHeightRatio)); + const maxCharsPerLine = Math.max(1, Math.floor(maxWidth / (fontSize * charWidthRatio))); + const renderedLineCount = countRenderedLines(normalizedContent, maxCharsPerLine); + + return { + lineHeight, + maxCharsPerLine, + renderedLineCount, + }; +} + +function findFittingTypography( + normalizedContent: string, + startFontSize: number, + minFontSize: number, + maxWidth: number, + maxHeight: number, + fontStep: number, + charWidthRatio: number, + lineHeightRatio: number +) { + for (let fontSize = startFontSize; fontSize >= minFontSize; fontSize -= fontStep) { + const candidate = createTypographyCandidate( + normalizedContent, + fontSize, + maxWidth, + charWidthRatio, + lineHeightRatio + ); + + if (candidate.renderedLineCount * candidate.lineHeight <= maxHeight) { + return { + candidate, + fontSize, + }; + } + } + + return null; +} + +export function fitCodeBlock({ + language, + content, + maxWidth, + maxHeight, + maxFontSize = 16, + minFontSize = 8, + fontStep = DEFAULT_FONT_STEP, + charWidthRatio = DEFAULT_CODE_CHAR_WIDTH_RATIO, + lineHeightRatio = DEFAULT_CODE_LINE_HEIGHT_RATIO, +}: FitCodeBlockOptions): FittedCodeBlock { + const normalizedContent = normalizeCodeContent(language, content); + const highlightLanguage = + isValidJsonContent(normalizedContent) || seemsJsonLike(normalizedContent) + ? "json" + : language; + const preferredMinFont = Math.max(1, minFontSize); + const hardMinFont = Math.max(1, Math.min(preferredMinFont, HARD_MIN_FONT_SIZE)); + const startFont = Math.max(maxFontSize, preferredMinFont); + + const preferredFit = findFittingTypography( + normalizedContent, + startFont, + preferredMinFont, + maxWidth, + maxHeight, + fontStep, + charWidthRatio, + lineHeightRatio + ); + + if (preferredFit) { + const highlighted = highlightCode(normalizedContent, highlightLanguage); + return { + text: normalizedContent, + highlightedHtml: highlighted.html, + prismLanguage: highlighted.prismLanguage, + fontSize: Math.round(preferredFit.fontSize * 10) / 10, + lineHeight: preferredFit.candidate.lineHeight, + fontFamily: DEFAULT_CODE_FONT_FAMILY, + }; + } + + if (hardMinFont < preferredMinFont) { + const emergencyFit = findFittingTypography( + normalizedContent, + preferredMinFont - fontStep, + hardMinFont, + maxWidth, + maxHeight, + fontStep, + charWidthRatio, + lineHeightRatio + ); + + if (emergencyFit) { + const highlighted = highlightCode(normalizedContent, highlightLanguage); + return { + text: normalizedContent, + highlightedHtml: highlighted.html, + prismLanguage: highlighted.prismLanguage, + fontSize: Math.round(emergencyFit.fontSize * 10) / 10, + lineHeight: emergencyFit.candidate.lineHeight, + fontFamily: DEFAULT_CODE_FONT_FAMILY, + }; + } + } + + const fallback = createTypographyCandidate( + normalizedContent, + hardMinFont, + maxWidth, + charWidthRatio, + lineHeightRatio + ); + const fallbackLineBudget = Math.max(1, Math.floor(maxHeight / fallback.lineHeight)); + const fallbackText = truncateContentToLineBudget( + normalizedContent, + fallbackLineBudget, + fallback.maxCharsPerLine + ); + const highlighted = highlightCode(fallbackText, highlightLanguage); + return { + text: fallbackText, + highlightedHtml: highlighted.html, + prismLanguage: highlighted.prismLanguage, + fontSize: Math.round(hardMinFont * 10) / 10, + lineHeight: fallback.lineHeight, + fontFamily: DEFAULT_CODE_FONT_FAMILY, + }; +} diff --git a/electron/servers/nextjs/app/presentation-templates/Education/EducationStatisticsGridSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Education/EducationStatisticsGridSlide.tsx index 3a76a0e5..a686cf0a 100644 --- a/electron/servers/nextjs/app/presentation-templates/Education/EducationStatisticsGridSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Education/EducationStatisticsGridSlide.tsx @@ -9,7 +9,7 @@ const StatisticSchema = z.object({ value: z.string().max(8).meta({ description: "Main metric value shown at the top of one card.", }), - label: z.string().max(20).meta({ + label: z.string().max(45).meta({ description: "Label shown under the value.", }), }); @@ -28,14 +28,14 @@ export const Schema = z.object({ .min(2) .max(8) .default([ - { value: "120", label: "Sales Team Strength" }, - { value: "15", label: "Senior Sales Officer" }, - { value: "1", label: "National Manager" }, - { value: "25", label: "Sales Officers" }, - { value: "2", label: "Regional Manager" }, - { value: "50", label: "Distributor Reps" }, - { value: "5", label: "Zonal Manager" }, - { value: "20", label: "Merchandising Team" }, + { value: "120", label: "Sales Team Strength with a long label to test the layouts" }, + { value: "15", label: "Senior Sales Officer with a long label to test the layout" }, + { value: "1", label: "National Manager with a long label to test the layout" }, + { value: "25", label: "Sales Officers with a long label to test the layout" }, + { value: "2", label: "Regional Manager with a long label to test the layout" }, + { value: "50", label: "Distributor Reps with a long label to test the layout" }, + { value: "5", label: "Zonal Manager with a long label to test the layout" }, + { value: "20", label: "Merchandising Team with a long label to the layout" }, ]) .meta({ description: "statistic cards, with value and label each in a card", @@ -93,29 +93,35 @@ const EducationStatisticsGridSlide = ({ data }: { data: Partial }) = {data.stats && data.stats?.length > 4 && data.stats?.length <= 8 && (() => { - const rightArray = data.stats?.slice(0, Math.floor(data.stats?.length / 2)); - const leftArray = data.stats?.slice(Math.floor(data.stats?.length / 2)); + // const rightArray = data.stats?.slice(0, Math.floor(data.stats?.length / 2)); + // const leftArray = data.stats?.slice(Math.floor(data.stats?.length / 2)); return ( -
-
+
+ {/*
*/} - {leftArray?.map((stat: any, index: number) => ( -
-

- {stat?.value} -

-

- {stat?.label} -

-
- ))} -
-
+ {data.stats?.map((stat: any, index: number) => ( +
+

+ {stat?.value} +

+

+ {stat?.label} +

+
+ ))} + + {/*
*/} + + {/*
{rightArray?.map((stat: any, index: number) => (
}) =

))} -
+
*/}
); })()} diff --git a/electron/servers/nextjs/app/presentation-templates/ProductOverview/ComparisonChartSlide.tsx b/electron/servers/nextjs/app/presentation-templates/ProductOverview/ComparisonChartSlide.tsx index 5d1f5e59..818e6173 100644 --- a/electron/servers/nextjs/app/presentation-templates/ProductOverview/ComparisonChartSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/ProductOverview/ComparisonChartSlide.tsx @@ -32,7 +32,7 @@ const RowSchema = z.union([GeneralRowSchema, LegacyRowSchema]); export const Schema = z.object({ - title: z.string().max(14).default("Comparison Chart").meta({ + title: z.string().max(24).default("Comparison Chart Comparison").meta({ description: "Main heading shown above the table.", }), subtitle: z.string().max(80).default( diff --git a/electron/servers/nextjs/app/presentation-templates/ProductOverview/CoverSlide.tsx b/electron/servers/nextjs/app/presentation-templates/ProductOverview/CoverSlide.tsx index f1629236..bc69728f 100644 --- a/electron/servers/nextjs/app/presentation-templates/ProductOverview/CoverSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/ProductOverview/CoverSlide.tsx @@ -8,15 +8,7 @@ export const slideLayoutDescription = "A cover slide with a compact logo in the top-left, a date/text/label in the top-right, a centered title, and a image anchored to the bottom with a soft fade into the background."; export const Schema = z.object({ - image: z.object({ - __image_url__: z.string().default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/images/placeholder.jpg"), - __image_prompt__: z.string().default("Image of the company"), - }).optional().default({ - __image_url__: - "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/images/placeholder.jpg", - __image_prompt__: "Image of the company", - }), label: z.string().min(3).max(16).optional().default("MARCH 2026").meta({ description: "Date/text/label shown at the top-right corner.", }), @@ -56,11 +48,7 @@ const CoverSlide = ({ data }: { data: Partial }) => { >
- {data.image?.__image_url__ ? {data.image?.__image_prompt__ :

} +

}) => { {title} -

+

}) => {

-
+
{items?.map((item, index) => (
diff --git a/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisDashboardSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisDashboardSlide.tsx index 92f1ac42..211ba519 100644 --- a/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisDashboardSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisDashboardSlide.tsx @@ -136,9 +136,9 @@ function SummaryCard({ return (
@@ -149,12 +149,7 @@ function SummaryCard({ color="var(--primary-text, #000000)" title={iconAlt ?? ""} /> - {/* {iconAlt */}

}) =>

}
- {halfChart && halfChart.length > 0 &&
+ {halfChart && halfChart.length > 0 &&
0 ? '200px' : 'auto', + }} + >
}) => >
- + 0 ? 200 : 400}> + + +
))}
} - {otherHalfChart && otherHalfChart.length > 0 &&
+ {otherHalfChart && otherHalfChart.length > 0 &&
}) => className="rounded-[6px] flex flex-col overflow-hidden" >
- + + -
))} diff --git a/electron/servers/nextjs/app/presentation-templates/Report/MilestoneSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/MilestoneSlide.tsx index 3cccddc7..3274e6bc 100644 --- a/electron/servers/nextjs/app/presentation-templates/Report/MilestoneSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Report/MilestoneSlide.tsx @@ -8,7 +8,7 @@ const MilestoneItemSchema = z.object({ description: "Heading displayed below the milestone marker.", }), description: z.string().min(10).max(80).meta({ - description: "Supporting milestone description shown under the heading.", + description: "Supporting milestone description shown under the heading. with max 80 characters", }), }); @@ -32,27 +32,27 @@ export const Schema = z.object({ { bulletNumber: "01", heading: "Heading", - description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet,", }, { bulletNumber: "02", heading: "Heading", - description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + description: "Lorem ipsum dolor sit amet, Lorem ipsum dolor sit amet,", }, { bulletNumber: "03", heading: "Heading", - description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + description: "Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, Lorem ipsum dolor sit amet", }, { bulletNumber: "04", heading: "Heading", - description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet,", }, { bulletNumber: "05", heading: "Heading", - description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet,", }, ]) .meta({ @@ -118,7 +118,7 @@ const MilestoneSlide = ({ data }: { data: Partial }) => {
0 ? 'pr-[33px]' : ''} ${index === 0 ? 'px-[33px]' : ''}`} + className={`text-center h-[130px] mt-[20px] text-[#232223] ${index > 0 ? 'pr-[33px]' : ''} ${index === 0 ? 'px-[33px]' : ''}`} style={{ color: "var(--background-text,#232223)" }} >

diff --git a/electron/servers/nextjs/app/presentation-templates/Report/flexibleReportChart.tsx b/electron/servers/nextjs/app/presentation-templates/Report/flexibleReportChart.tsx index 37164a1f..7be6fafd 100644 --- a/electron/servers/nextjs/app/presentation-templates/Report/flexibleReportChart.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Report/flexibleReportChart.tsx @@ -382,8 +382,6 @@ export function FlexibleReportChart({ case "bar": return ( - - { return (
-
+
{/* Processing... */} - {Math.round(progress)}% + {Math.round(progress)}%
-
+
+ + + + + + + + + ) +} + /** Toasts with both title and description (matches styled [data-title] / [data-description]). */ export const notify = { error: (title: string, description: string) => @@ -20,7 +36,7 @@ const Toaster = ({ icons, ...props }: ToasterProps) => { const defaultIcons: NonNullable = { success: