From dd8af8e8cc5b8b90007296b89c50952add547988 Mon Sep 17 00:00:00 2001 From: shiva raj badu Date: Mon, 13 Apr 2026 12:13:26 +0545 Subject: [PATCH 1/3] refactor: Improve template structure & schema --- electron/resources/ui/homepage/index.html | 9 +- .../(dashboard)/settings/SettingPage.tsx | 3 +- .../components/SlidePreviewSection.tsx | 4 +- .../pdf-maker/PdfMakerPage.tsx | 4 + .../components/PresentationHeader.tsx | 3 + .../presentation/components/ThemeSelector.tsx | 2 +- .../presentation/hooks/usePresentationData.ts | 4 + .../Code/APIRequestResponseSlide.tsx | 66 +++- .../Code/CardsGridSlide.tsx | 2 +- .../Code/CodeExplanationSplitSlide.tsx | 162 ++-------- .../Code/WorkflowSlide.tsx | 9 +- .../Code/codeBlockFitting.ts | 305 ++++++++++++++++++ .../EducationStatisticsGridSlide.tsx | 66 ++-- .../ProductOverview/ComparisonChartSlide.tsx | 2 +- .../ProductOverview/CoverSlide.tsx | 14 +- .../ProductOverview/IntroductionSlide.tsx | 2 +- .../ProductOverview/ReportSnapshotSlide.tsx | 8 +- ...ulletListWithIconTitleDescriptionSlide.tsx | 18 +- .../Report/DataAnalysisDashboardSlide.tsx | 26 +- .../Report/MilestoneSlide.tsx | 14 +- .../Report/flexibleReportChart.tsx | 2 - .../nextjs/components/ui/progress-bar.tsx | 6 +- .../servers/nextjs/components/ui/sonner.tsx | 20 +- servers/nextjs/components/CodexConfig.tsx | 14 +- 24 files changed, 501 insertions(+), 264 deletions(-) create mode 100644 electron/servers/nextjs/app/presentation-templates/Code/codeBlockFitting.ts diff --git a/electron/resources/ui/homepage/index.html b/electron/resources/ui/homepage/index.html index 7922ce57..e78e009c 100644 --- a/electron/resources/ui/homepage/index.html +++ b/electron/resources/ui/homepage/index.html @@ -142,12 +142,7 @@ content: ""; position: absolute; inset: 0; - background: linear-gradient(90deg, - transparent 0%, - rgba(255, 255, 255, 0.12) 36%, - rgba(255, 255, 255, 0.58) 50%, - rgba(255, 255, 255, 0.12) 64%, - transparent 100%); + animation: shimmer 1.85s linear infinite; } @@ -209,4 +204,4 @@ - + \ No newline at end of file diff --git a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingPage.tsx b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingPage.tsx index 956cbb77..404a1265 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingPage.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingPage.tsx @@ -158,8 +158,7 @@ const SettingsPage = () => { isDisabled: false, text: "Save Configuration", })); - trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/upload" }); - router.push("/upload"); + } catch (error) { const message = error instanceof Error diff --git a/electron/servers/nextjs/app/(presentation-generator)/custom-template/components/SlidePreviewSection.tsx b/electron/servers/nextjs/app/(presentation-generator)/custom-template/components/SlidePreviewSection.tsx index 22252e46..1f1e6730 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/custom-template/components/SlidePreviewSection.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/custom-template/components/SlidePreviewSection.tsx @@ -76,7 +76,7 @@ export const SlidePreviewSection: React.FC = ({ size="lg" onClick={onInitTemplate} disabled={isLoading} - className="px-4 py-2 h-auto text-xs font-semibold rounded-full shadow-lg hover:shadow-xl transition-all duration-300 " + className="px-4 py-2 h-auto text-xs font-syne font-medium rounded-full shadow-lg hover:shadow-xl transition-all duration-300 " style={{ background: isLoading ? '#E5E7EB' @@ -91,7 +91,7 @@ export const SlidePreviewSection: React.FC = ({ ) : ( <> - + Generate Template diff --git a/electron/servers/nextjs/app/(presentation-generator)/pdf-maker/PdfMakerPage.tsx b/electron/servers/nextjs/app/(presentation-generator)/pdf-maker/PdfMakerPage.tsx index 67c9cef4..66d85b05 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/pdf-maker/PdfMakerPage.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/pdf-maker/PdfMakerPage.tsx @@ -62,6 +62,10 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => { const data = await DashboardApi.getPresentation(presentation_id); dispatch(setPresentationData(data)); setContentLoading(false); + + if (data.fonts) { + useFontLoader(data.fonts); + } if (data?.theme) { try { applyTheme(data.theme); diff --git a/electron/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationHeader.tsx b/electron/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationHeader.tsx index 8d5d86af..cff47881 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationHeader.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationHeader.tsx @@ -389,6 +389,9 @@ const PresentationHeader = ({ ) : ( titleBlock )} + + pdf-maker +
{isPresentationSaving &&
diff --git a/electron/servers/nextjs/app/(presentation-generator)/presentation/components/ThemeSelector.tsx b/electron/servers/nextjs/app/(presentation-generator)/presentation/components/ThemeSelector.tsx index 9b9f3c13..46e20aec 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/presentation/components/ThemeSelector.tsx +++ b/electron/servers/nextjs/app/(presentation-generator)/presentation/components/ThemeSelector.tsx @@ -27,7 +27,7 @@ const ThemeSelector = ({ current_theme, themes: allThemes }: { current_theme: an dispatch(updateTheme(theme)); }; const resetTheme = async () => { - dispatch(updateTheme({} as any)); + dispatch(updateTheme(null)); clearPresentationThemeFromElement( document.getElementById("presentation-slides-wrapper") ); diff --git a/electron/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts b/electron/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts index 8d162239..b5799a4e 100644 --- a/electron/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts +++ b/electron/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts @@ -6,6 +6,7 @@ import { DashboardApi } from '../../services/api/dashboard'; import { clearHistory } from "@/store/slices/undoRedoSlice"; import { applyPresentationThemeToElement } from "../utils/applyPresentationThemeDom"; import { resolveBackendAssetUrl } from "@/utils/api"; +import { useFontLoader } from "../../hooks/useFontLoad"; const normalizePresentationAssets = (input: T): T => { if (Array.isArray(input)) { @@ -46,6 +47,9 @@ export const usePresentationData = ( dispatch(clearHistory()); setLoading(false); } + if (data.fonts) { + useFontLoader(data.fonts); + } if (normalizedData?.theme) { const el = document.getElementById("presentation-slides-wrapper"); applyPresentationThemeToElement(el, normalizedData.theme); diff --git a/electron/servers/nextjs/app/presentation-templates/Code/APIRequestResponseSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Code/APIRequestResponseSlide.tsx index 2091120c..fbf2ecec 100644 --- a/electron/servers/nextjs/app/presentation-templates/Code/APIRequestResponseSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Code/APIRequestResponseSlide.tsx @@ -1,4 +1,5 @@ import * as z from "zod"; +import { fitCodeBlock } from "./codeBlockFitting"; export const slideLayoutId = "api-request-response-slide"; @@ -68,6 +69,23 @@ const CodeSlide03ApiRequestResponse = ({ }: { data: Partial; }) => { + const requestCode = fitCodeBlock({ + language: data.requestSnippet?.language, + content: data.requestSnippet?.content, + maxWidth: 540, + maxHeight: 230, + maxFontSize: 14, + minFontSize: 8, + }); + + const responseCode = fitCodeBlock({ + language: data.responseSnippet?.language, + content: data.responseSnippet?.content, + maxWidth: 540, + maxHeight: 430, + maxFontSize: 14, + minFontSize: 8, + }); return ( <> @@ -83,8 +101,8 @@ const CodeSlide03ApiRequestResponse = ({

{data.title}

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

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

-
-
-                
-                  {data.responseSnippet?.content}
-                
-              
+
+
+                  {responseCode.text}
+                
+
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..91505265 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 } from "./codeBlockFitting"; export const slideLayoutId = "code-explanation-split-slide"; export const slideLayoutName = "Code Explanation Split Slide"; @@ -186,12 +65,14 @@ 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 ( <> @@ -229,22 +110,19 @@ const CodeSlide02CodeExplanationSplit = ({ className="min-h-0 w-full flex-1 overflow-hidden px-[32px] py-[20px]" style={{ color: "var(--background-text,#ffffff)", - fontFamily: CODE_FONT_FAMILY, }} > - {codeLineRuns.map((codeLineRun, index) => ( -
- {codeLineRun.text} -
- ))} +
+                  {fittedCode.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) { + try { + return JSON.stringify(JSON.parse(content), null, 2); + } catch { + return content; + } +} + +export function normalizeCodeContent(language?: string, content?: string) { + let normalizedContent = (content || "") + .replace(/\r\n?/g, "\n") + .replace(/\\\[/g, "[") + .replace(/\\\]/g, "]") + .trimEnd(); + + const normalizedLanguage = language?.toLowerCase(); + const isJsonLanguage = normalizedLanguage?.includes("json"); + const looksLikeJsonPayload = !normalizedLanguage && /^[\[{]/.test(normalizedContent.trim()); + + if (normalizedLanguage === "python") { + normalizedContent = normalizePythonCode(normalizedContent); + } else if (isJsonLanguage || looksLikeJsonPayload) { + normalizedContent = tryFormatJson(normalizedContent); + } + + return normalizedContent; +} + +function wrapLineWithContinuation(line: string, maxCharsPerLine: number) { + if (line.length <= maxCharsPerLine) { + return [line]; + } + + const leadingWhitespace = line.match(/^\s*/)?.[0] ?? ""; + const continuationPrefix = `${leadingWhitespace} `; + const continuationCapacity = maxCharsPerLine - continuationPrefix.length; + + if (continuationCapacity <= 8) { + const chunks: string[] = []; + for (let start = 0; start < line.length; start += maxCharsPerLine) { + chunks.push(line.slice(start, start + maxCharsPerLine)); + } + return chunks; + } + + const chunks = [line.slice(0, maxCharsPerLine)]; + for ( + let start = maxCharsPerLine; + start < line.length; + start += continuationCapacity + ) { + chunks.push(`${continuationPrefix}${line.slice(start, start + continuationCapacity)}`); + } + return chunks; +} + +function wrapContentToWidth(content: string, maxCharsPerLine: number) { + const wrappedLines: string[] = []; + const rawLines = content.split("\n"); + + for (const rawLine of rawLines) { + const expandedLine = rawLine.replace(/\t/g, " "); + + if (expandedLine.length === 0) { + wrappedLines.push(""); + continue; + } + + wrappedLines.push(...wrapLineWithContinuation(expandedLine, maxCharsPerLine)); + } + + return wrappedLines.length ? wrappedLines : [""]; +} + +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 wrappedLines = wrapContentToWidth(normalizedContent, maxCharsPerLine); + + return { + lineHeight, + maxCharsPerLine, + wrappedLines, + }; +} + +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.wrappedLines.length * candidate.lineHeight <= maxHeight) { + return { + candidate, + fontSize, + }; + } + } + + return null; +} + +function truncateToLineBudget(lines: string[], lineBudget: number, maxCharsPerLine: number) { + const visibleLines = lines.slice(0, lineBudget); + + if (lines.length <= lineBudget || visibleLines.length === 0) { + return visibleLines; + } + + const lastIndex = visibleLines.length - 1; + const ellipsis = "..."; + const truncatedLastLine = + visibleLines[lastIndex].slice(0, Math.max(0, maxCharsPerLine - ellipsis.length)); + visibleLines[lastIndex] = `${truncatedLastLine}${ellipsis}`; + return visibleLines; +} + +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 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) { + return { + text: preferredFit.candidate.wrappedLines.join("\n"), + 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) { + return { + text: emergencyFit.candidate.wrappedLines.join("\n"), + 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 fallbackLines = truncateToLineBudget( + fallback.wrappedLines, + fallbackLineBudget, + fallback.maxCharsPerLine + ); + + return { + text: fallbackLines.join("\n"), + 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:
@@ -183,17 +196,19 @@ const CodeSlide03ApiRequestResponse = ({

-                  {responseCode.text}
-                
+ dangerouslySetInnerHTML={{ __html: responseCode.highlightedHtml }} + />
diff --git a/electron/servers/nextjs/app/presentation-templates/Code/CodeExplanationSplitSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Code/CodeExplanationSplitSlide.tsx index 91505265..b060a29e 100644 --- a/electron/servers/nextjs/app/presentation-templates/Code/CodeExplanationSplitSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Code/CodeExplanationSplitSlide.tsx @@ -1,5 +1,5 @@ import * as z from "zod"; -import { fitCodeBlock } from "./codeBlockFitting"; +import { fitCodeBlock, PRISM_CODE_BLOCK_STYLES } from "./codeBlockFitting"; export const slideLayoutId = "code-explanation-split-slide"; export const slideLayoutName = "Code Explanation Split Slide"; @@ -77,6 +77,7 @@ const CodeSlide02CodeExplanationSplit = ({ return ( <> +
-                  {fittedCode.text}
-                
+ dangerouslySetInnerHTML={{ __html: fittedCode.highlightedHtml }} + />
diff --git a/electron/servers/nextjs/app/presentation-templates/Code/codeBlockFitting.ts b/electron/servers/nextjs/app/presentation-templates/Code/codeBlockFitting.ts index c4f17693..85b19793 100644 --- a/electron/servers/nextjs/app/presentation-templates/Code/codeBlockFitting.ts +++ b/electron/servers/nextjs/app/presentation-templates/Code/codeBlockFitting.ts @@ -1,9 +1,83 @@ +import { jsonrepair } from "jsonrepair"; +import Prism from "prismjs"; +import "prismjs/components/prism-json"; +import "prismjs/components/prism-javascript"; +import "prismjs/components/prism-typescript"; +import "prismjs/components/prism-jsx"; +import "prismjs/components/prism-tsx"; +import "prismjs/components/prism-python"; +import "prismjs/components/prism-bash"; +import "prismjs/components/prism-yaml"; +import "prismjs/components/prism-markdown"; + const DEFAULT_CODE_CHAR_WIDTH_RATIO = 0.62; const DEFAULT_CODE_LINE_HEIGHT_RATIO = 1.25; const DEFAULT_FONT_STEP = 0.5; const HARD_MIN_FONT_SIZE = 4; export const DEFAULT_CODE_FONT_FAMILY = "var(--code-font-family,'Liberation Mono', monospace)"; +export const PRISM_CODE_BLOCK_STYLES = ` +.prism-code-block .token { + display: inline !important; + white-space: inherit !important; +} + +.prism-code-block .token.comment, +.prism-code-block .token.prolog, +.prism-code-block .token.doctype, +.prism-code-block .token.cdata { + color: #7b8ebf; +} + +.prism-code-block .token.punctuation { + color: #a8b7e0; +} + +.prism-code-block .token.property, +.prism-code-block .token.tag, +.prism-code-block .token.constant, +.prism-code-block .token.symbol, +.prism-code-block .token.deleted { + color: #7bc4ff; +} + +.prism-code-block .token.boolean, +.prism-code-block .token.number { + color: #f5c97b; +} + +.prism-code-block .token.selector, +.prism-code-block .token.attr-name, +.prism-code-block .token.string, +.prism-code-block .token.char, +.prism-code-block .token.builtin, +.prism-code-block .token.inserted { + color: #9fe6b8; +} + +.prism-code-block .token.operator, +.prism-code-block .token.entity, +.prism-code-block .token.url, +.prism-code-block .token.variable { + color: #f5a97f; +} + +.prism-code-block .token.atrule, +.prism-code-block .token.attr-value, +.prism-code-block .token.function, +.prism-code-block .token.class-name { + color: #b8a8ff; +} + +.prism-code-block .token.keyword { + color: #7aa2ff; +} + +.prism-code-block .token.regex, +.prism-code-block .token.important { + color: #f9e2af; +} +`; interface FitCodeBlockOptions { language?: string; @@ -20,11 +94,13 @@ interface FitCodeBlockOptions { interface TypographyCandidate { lineHeight: number; maxCharsPerLine: number; - wrappedLines: string[]; + renderedLineCount: number; } export interface FittedCodeBlock { text: string; + highlightedHtml: string; + prismLanguage: string; fontSize: number; lineHeight: number; fontFamily: string; @@ -46,7 +122,7 @@ function expandInlinePythonStatement(line: string) { return [line]; } - return [inlineReturnMatch[1], ` return ${inlineReturnMatch[2]}`]; + return [inlineReturnMatch[1], `return ${inlineReturnMatch[2]}`]; } function expandPathListAssignment(line: string) { @@ -88,10 +164,272 @@ function normalizePythonCode(content: string) { } 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 { - return JSON.stringify(JSON.parse(content), null, 2); + JSON.parse(content.trim()); + return true; } catch { - return content; + 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, + }; } } @@ -99,66 +437,85 @@ export function normalizeCodeContent(language?: string, content?: string) { let normalizedContent = (content || "") .replace(/\r\n?/g, "\n") .replace(/\\\[/g, "[") - .replace(/\\\]/g, "]") - .trimEnd(); + .replace(/\\\]/g, "]"); + const unwrappedContent = unwrapMarkdownCodeFence(normalizedContent); + normalizedContent = unwrappedContent.content.trimEnd(); - const normalizedLanguage = language?.toLowerCase(); + const normalizedLanguage = language?.toLowerCase()?.trim() || unwrappedContent.fenceLanguage; const isJsonLanguage = normalizedLanguage?.includes("json"); - const looksLikeJsonPayload = !normalizedLanguage && /^[\[{]/.test(normalizedContent.trim()); + const looksLikeJsonPayload = seemsJsonLike(normalizedContent); if (normalizedLanguage === "python") { normalizedContent = normalizePythonCode(normalizedContent); } else if (isJsonLanguage || looksLikeJsonPayload) { - normalizedContent = tryFormatJson(normalizedContent); + const formattedJson = tryFormatJson(normalizedContent); + normalizedContent = formattedJson; } return normalizedContent; } -function wrapLineWithContinuation(line: string, maxCharsPerLine: number) { - if (line.length <= maxCharsPerLine) { - return [line]; - } - - const leadingWhitespace = line.match(/^\s*/)?.[0] ?? ""; - const continuationPrefix = `${leadingWhitespace} `; - const continuationCapacity = maxCharsPerLine - continuationPrefix.length; - - if (continuationCapacity <= 8) { - const chunks: string[] = []; - for (let start = 0; start < line.length; start += maxCharsPerLine) { - chunks.push(line.slice(start, start + maxCharsPerLine)); - } - return chunks; - } - - const chunks = [line.slice(0, maxCharsPerLine)]; - for ( - let start = maxCharsPerLine; - start < line.length; - start += continuationCapacity - ) { - chunks.push(`${continuationPrefix}${line.slice(start, start + continuationCapacity)}`); - } - return chunks; -} - -function wrapContentToWidth(content: string, maxCharsPerLine: number) { - const wrappedLines: string[] = []; +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) { - wrappedLines.push(""); + renderedLineCount += 1; continue; } - wrappedLines.push(...wrapLineWithContinuation(expandedLine, maxCharsPerLine)); + renderedLineCount += Math.max(1, Math.ceil(expandedLine.length / maxCharsPerLine)); } - return wrappedLines.length ? wrappedLines : [""]; + 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( @@ -170,12 +527,12 @@ function createTypographyCandidate( ): TypographyCandidate { const lineHeight = Math.max(1, Math.round(fontSize * lineHeightRatio)); const maxCharsPerLine = Math.max(1, Math.floor(maxWidth / (fontSize * charWidthRatio))); - const wrappedLines = wrapContentToWidth(normalizedContent, maxCharsPerLine); + const renderedLineCount = countRenderedLines(normalizedContent, maxCharsPerLine); return { lineHeight, maxCharsPerLine, - wrappedLines, + renderedLineCount, }; } @@ -198,7 +555,7 @@ function findFittingTypography( lineHeightRatio ); - if (candidate.wrappedLines.length * candidate.lineHeight <= maxHeight) { + if (candidate.renderedLineCount * candidate.lineHeight <= maxHeight) { return { candidate, fontSize, @@ -209,21 +566,6 @@ function findFittingTypography( return null; } -function truncateToLineBudget(lines: string[], lineBudget: number, maxCharsPerLine: number) { - const visibleLines = lines.slice(0, lineBudget); - - if (lines.length <= lineBudget || visibleLines.length === 0) { - return visibleLines; - } - - const lastIndex = visibleLines.length - 1; - const ellipsis = "..."; - const truncatedLastLine = - visibleLines[lastIndex].slice(0, Math.max(0, maxCharsPerLine - ellipsis.length)); - visibleLines[lastIndex] = `${truncatedLastLine}${ellipsis}`; - return visibleLines; -} - export function fitCodeBlock({ language, content, @@ -236,6 +578,10 @@ export function fitCodeBlock({ 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); @@ -252,8 +598,11 @@ export function fitCodeBlock({ ); if (preferredFit) { + const highlighted = highlightCode(normalizedContent, highlightLanguage); return { - text: preferredFit.candidate.wrappedLines.join("\n"), + text: normalizedContent, + highlightedHtml: highlighted.html, + prismLanguage: highlighted.prismLanguage, fontSize: Math.round(preferredFit.fontSize * 10) / 10, lineHeight: preferredFit.candidate.lineHeight, fontFamily: DEFAULT_CODE_FONT_FAMILY, @@ -273,8 +622,11 @@ export function fitCodeBlock({ ); if (emergencyFit) { + const highlighted = highlightCode(normalizedContent, highlightLanguage); return { - text: emergencyFit.candidate.wrappedLines.join("\n"), + text: normalizedContent, + highlightedHtml: highlighted.html, + prismLanguage: highlighted.prismLanguage, fontSize: Math.round(emergencyFit.fontSize * 10) / 10, lineHeight: emergencyFit.candidate.lineHeight, fontFamily: DEFAULT_CODE_FONT_FAMILY, @@ -290,14 +642,16 @@ export function fitCodeBlock({ lineHeightRatio ); const fallbackLineBudget = Math.max(1, Math.floor(maxHeight / fallback.lineHeight)); - const fallbackLines = truncateToLineBudget( - fallback.wrappedLines, + const fallbackText = truncateContentToLineBudget( + normalizedContent, fallbackLineBudget, fallback.maxCharsPerLine ); - + const highlighted = highlightCode(fallbackText, highlightLanguage); return { - text: fallbackLines.join("\n"), + text: fallbackText, + highlightedHtml: highlighted.html, + prismLanguage: highlighted.prismLanguage, fontSize: Math.round(hardMinFont * 10) / 10, lineHeight: fallback.lineHeight, fontFamily: DEFAULT_CODE_FONT_FAMILY, From 4bc7faba7bbb2d8cda905cf0a6452765879678ad Mon Sep 17 00:00:00 2001 From: shiva raj badu Date: Mon, 13 Apr 2026 19:20:57 +0545 Subject: [PATCH 3/3] refactor: improve splash screen --- electron/resources/ui/homepage/index.html | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/electron/resources/ui/homepage/index.html b/electron/resources/ui/homepage/index.html index e78e009c..9578692f 100644 --- a/electron/resources/ui/homepage/index.html +++ b/electron/resources/ui/homepage/index.html @@ -6,13 +6,6 @@ Presenton