diff --git a/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide01RoadmapCover.tsx b/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide01RoadmapCover.tsx index 52f779b8..4eefd0ee 100644 --- a/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide01RoadmapCover.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide01RoadmapCover.tsx @@ -6,7 +6,7 @@ export const slideLayoutDescription = "A centered opening slide with company name, roadmap title, and supporting subtitle."; export const Schema = z.object({ - companyName: z.string().min(2).max(28).default("COMPANY NAME").meta({ + companyName: z.string().min(2).max(18).default("COMPANY NAME").meta({ description: "Organization name shown above the slide title.", }), title: z.string().min(8).max(28).default("Development Roadmap").meta({ @@ -15,7 +15,7 @@ export const Schema = z.object({ subtitle: z .string() .min(24) - .max(92) + .max(40) .default( "We transform ideas into market-ready solutions through systematic development processes." ) diff --git a/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide02CodeExplanationSplit.tsx b/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide02CodeExplanationSplit.tsx index fdee3e06..7edb0a22 100644 --- a/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide02CodeExplanationSplit.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide02CodeExplanationSplit.tsx @@ -45,7 +45,7 @@ export function UserAuth() { .min(40) .max(360) .default( - "This component manages credentials as local state and submits them through an async handler. The login utility abstracts network details while the handler keeps the UI flow predictable. Keep validation and side effects isolated so changes remain safe when authentication requirements evolve." + "This component manages credentials as local state and submits them through an async handler. The login utility abstracts network details while the handler keeps the UI flow predictable. Keep validation and side effects isolated so changes remain safe when authentication requirements evolve. " ) .meta({ description: "Explanation paragraph shown in the right column.", diff --git a/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide03ApiRequestResponse.tsx b/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide03ApiRequestResponse.tsx index 42e9f206..9fe00314 100644 --- a/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide03ApiRequestResponse.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide03ApiRequestResponse.tsx @@ -17,7 +17,7 @@ export const Schema = z.object({ description: "Endpoint path text.", }), headers: z - .array(z.string().min(12).max(44)) + .array(z.string().max(10)) .min(2) .max(2) .default(["Content-Type: application/json", "Authorization: Bearer "]) diff --git a/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide04FeatureGrid.tsx b/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide04FeatureGrid.tsx index b7700234..950b2dc6 100644 --- a/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide04FeatureGrid.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide04FeatureGrid.tsx @@ -1,17 +1,17 @@ import * as z from "zod"; const FeatureCardSchema = z.object({ - title: z.string().min(3).max(20).meta({ + title: z.string().min(3).max(17).meta({ description: "Feature title shown on each card.", }), - description: z.string().min(18).max(82).meta({ + description: z.string().min(18).max(80).meta({ description: "Supporting feature description.", }), icon: z.object({ - __icon_url__: z.string().min(10).max(180).meta({ + __icon_url__: z.string().meta({ description: "URL to icon", }), - __icon_query__: z.string().min(3).max(28).meta({ + __icon_query__: z.string().meta({ description: "Query used to search the icon", }), }).default({ @@ -45,7 +45,7 @@ export const Schema = z.object({ }, }, { - title: "Component Library", + title: "Component Library ", description: "Reusable UI components with consistent design patterns.", icon: { __icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg", diff --git a/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide05ComparisonTable.tsx b/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide05ComparisonTable.tsx index f76a12c0..2ff35020 100644 --- a/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide05ComparisonTable.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide05ComparisonTable.tsx @@ -1,17 +1,17 @@ import * as z from "zod"; const ComparisonRowSchema = z.object({ - feature: z.string().min(4).max(20).meta({ + feature: z.string().min(4).max(17).meta({ description: "Feature label shown in the first column.", }), - react: z.string().min(1).max(12).meta({ - description: "React cell value.", + column1: z.string().max(10).meta({ + description: "Column 1 cell value.", }), - vue: z.string().min(1).max(12).meta({ - description: "Vue cell value.", + column2: z.string().max(10).meta({ + description: "Column 2 cell value.", }), - angular: z.string().min(1).max(12).meta({ - description: "Angular cell value.", + column3: z.string().max(10).meta({ + description: "Column 3 cell value.", }), }); @@ -24,17 +24,20 @@ export const Schema = z.object({ title: z.string().min(6).max(18).default("Comparison").meta({ description: "Slide title shown above the table.", }), + tableColumns: z.array(z.string().max(4)).meta({ + description: "Table columns shown in the first row.", + }).default(["Feature", "Column 1", "Column 2", "Column 3"]), rows: z .array(ComparisonRowSchema) .min(6) .max(6) .default([ - { feature: "Component-based", react: "check", vue: "check", angular: "check" }, - { feature: "TypeScript Support", react: "check", vue: "check", angular: "check" }, - { feature: "Learning Curve", react: "Medium", vue: "Easy", angular: "Steep" }, - { feature: "Bundle Size", react: "40KB", vue: "34KB", angular: "167KB" }, - { feature: "Performance", react: "Excellent", vue: "Excellent", angular: "Good" }, - { feature: "Community Size", react: "Huge", vue: "Large", angular: "Large" }, + { feature: "Component-based", column1: "check", column2: "check", column3: "check" }, + { feature: "TypeScript Support", column1: "check", column2: "check", column3: "check" }, + { feature: "Learning Curve", column1: "Medium", column2: "Easy", column3: "Steep" }, + { feature: "Bundle Size", column1: "40KB", column2: "34KB", column3: "167KB" }, + { feature: "Performance", column1: "Excellent", column2: "Excellent", column3: "Good" }, + { feature: "Community Size", column1: "Huge", column2: "Large", column3: "Large" }, ]) .meta({ description: "Six comparison rows shown in the table.", @@ -62,23 +65,30 @@ const CodeSlide05ComparisonTable = ({ data }: { data: Partial }) =>

{data.title}

-
-

Feature

-

React

-

Vue

-

Angular

+
+ + {data?.tableColumns?.map((column) => ( +

{column}

+ ))}
{data?.rows?.map((row) => (

{row.feature}

-
{renderCell(row.react)}
-
{renderCell(row.vue)}
-
{renderCell(row.angular)}
+
{renderCell(row.column1)}
+
{renderCell(row.column2)}
+
{renderCell(row.column3)}
))}
diff --git a/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide06Workflow.tsx b/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide06Workflow.tsx index 15ad1f1a..f573e69e 100644 --- a/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide06Workflow.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide06Workflow.tsx @@ -5,12 +5,16 @@ const WorkflowStepSchema = z.object({ title: z.string().min(3).max(12).meta({ description: "Step title shown in each workflow card.", }), - description: z.string().min(18).max(58).meta({ + description: z.string().min(18).max(50).meta({ description: "Short step description text.", }), icon: z.object({ - __icon_url__: z.string().min(10).max(180), - __icon_query__: z.string().min(3).max(28), + __icon_url__: z.string().meta({ + description: "URL to icon", + }), + __icon_query__: z.string().meta({ + description: "Query used to search the icon", + }), }).default({ __icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg", __icon_query__: "check icon", diff --git a/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide07UseCaseList.tsx b/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide07UseCaseList.tsx index 69013541..2d44f2a5 100644 --- a/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide07UseCaseList.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide07UseCaseList.tsx @@ -6,11 +6,11 @@ export const slideLayoutDescription = "A two-column numbered use-case list with eight compact items."; export const Schema = z.object({ - title: z.string().min(6).max(16).default("Usecase").meta({ + title: z.string().min(6).max(30).default("Usecase").meta({ description: "Slide title shown above the numbered list.", }), items: z - .array(z.string().min(16).max(58)) + .array(z.string().min(4).max(8)) .min(4) .max(8) .default([ diff --git a/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide08CodeExplanationText.tsx b/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide08CodeExplanationText.tsx index 0cf80abb..b22c127f 100644 --- a/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide08CodeExplanationText.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide08CodeExplanationText.tsx @@ -6,7 +6,7 @@ export const slideLayoutDescription = "A text-only explanation slide with generous whitespace for narrative documentation."; export const Schema = z.object({ - title: z.string().min(8).max(24).default("Code + Explanation").meta({ + title: z.string().min(8).max(30).default("Code + Explanation").meta({ description: "Main slide title shown at the top-left.", }), explanationTitle: z.string().min(4).max(20).default("Explanation").meta({ @@ -14,7 +14,7 @@ export const Schema = z.object({ }), explanation: z .string() - .min(60) + .max(360) .default( "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." diff --git a/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide10MetricsSplit.tsx b/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide10MetricsSplit.tsx index 66569317..666ba65c 100644 --- a/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide10MetricsSplit.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide10MetricsSplit.tsx @@ -1,7 +1,7 @@ import * as z from "zod"; const MetricSchema = z.object({ - value: z.string().min(2).max(8).meta({ + value: z.string().min(2).max(6).meta({ description: "Primary metric value.", }), label: z.string().min(3).max(14).meta({ @@ -26,7 +26,6 @@ export const Schema = z.object({ }), explanation: z .string() - .min(60) .max(320) .default( "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." diff --git a/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide11MetricsGrid.tsx b/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide11MetricsGrid.tsx index 6dd3ea05..ac0d5659 100644 --- a/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide11MetricsGrid.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Code/CodeSlide11MetricsGrid.tsx @@ -1,7 +1,7 @@ import * as z from "zod"; const MetricSchema = z.object({ - value: z.string().min(2).max(8).meta({ + value: z.string().min(2).max(6).meta({ description: "Primary metric value.", }), label: z.string().min(3).max(14).meta({ diff --git a/electron/servers/nextjs/app/presentation-templates/Education/EducationChartPrimitives.tsx b/electron/servers/nextjs/app/presentation-templates/Education/EducationChartPrimitives.tsx new file mode 100644 index 00000000..a27e2a44 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Education/EducationChartPrimitives.tsx @@ -0,0 +1,511 @@ +"use client"; + +import { + Area, + AreaChart, + Bar, + BarChart, + CartesianGrid, + Cell, + LabelList, + Legend, + Line, + LineChart, + Pie, + PieChart, + ReferenceLine, + ResponsiveContainer, + Scatter, + ScatterChart, + Tooltip, + XAxis, + YAxis, +} from "recharts"; + +export type EducationChartType = + | "bar" + | "bar-horizontal" + | "bar-grouped-vertical" + | "bar-grouped-horizontal" + | "bar-stacked-vertical" + | "bar-stacked-horizontal" + | "bar-clustered" + | "bar-diverging" + | "line" + | "area" + | "area-stacked" + | "pie" + | "donut" + | "scatter"; + +export type SimpleDatum = { + name: string; + value: number; +}; + +export type MultiSeriesDatum = { + name: string; + values: Record; +}; + +export type DivergingDatum = { + name: string; + positive: number; + negative: number; +}; + +export type ScatterDatum = { + x: number; + y: number; + name?: string; +}; + +export type EducationChartDatum = SimpleDatum | MultiSeriesDatum | DivergingDatum | ScatterDatum; + +type TooltipPayloadItem = { + color?: string; + dataKey?: string | number; + name?: string; + value?: string | number; +}; + +type TooltipProps = { + active?: boolean; + label?: string | number; + payload?: TooltipPayloadItem[]; +}; + +type PieLabelProps = { + name?: string; + percent?: number; + textAnchor?: "start" | "middle" | "end"; + x?: number; + y?: number; +}; + +const DEFAULT_COLORS = [ + "#4A15A8", + "#5B45AD", + "#7E6CC0", + "#9F94CD", + "#6A31B8", + "#4D2A97", +]; + +const AXIS = "#7C7A83"; +const GRID = "#CFCBD8"; +const PRIMARY_TEXT = "#3E3C45"; + +function formatComma(value: number | string) { + if (typeof value === "number") { + return value.toLocaleString("en-US"); + } + + const parsed = Number(value); + if (Number.isFinite(parsed)) { + return parsed.toLocaleString("en-US"); + } + + return value; +} + +function isSimpleDatum(item: EducationChartDatum): item is SimpleDatum { + return typeof (item as SimpleDatum).name === "string" && typeof (item as SimpleDatum).value === "number"; +} + +function isMultiSeriesDatum(item: EducationChartDatum): item is MultiSeriesDatum { + return ( + typeof (item as MultiSeriesDatum).name === "string" && + typeof (item as MultiSeriesDatum).values === "object" && + (item as MultiSeriesDatum).values !== null + ); +} + +function isDivergingDatum(item: EducationChartDatum): item is DivergingDatum { + return ( + typeof (item as DivergingDatum).name === "string" && + typeof (item as DivergingDatum).positive === "number" && + typeof (item as DivergingDatum).negative === "number" + ); +} + +function isScatterDatum(item: EducationChartDatum): item is ScatterDatum { + return typeof (item as ScatterDatum).x === "number" && typeof (item as ScatterDatum).y === "number"; +} + +function toSimpleData(data: EducationChartDatum[]) { + return data + .filter(isSimpleDatum) + .map((item) => ({ + name: item.name, + value: item.value, + })); +} + +function toScatterData(data: EducationChartDatum[]) { + const scatterData = data.filter(isScatterDatum); + + if (scatterData.length > 0) { + return scatterData.map((item, index) => ({ + x: item.x, + y: item.y, + name: item.name ?? String(index + 1), + })); + } + + return data + .filter(isSimpleDatum) + .map((item, index) => ({ + x: index + 1, + y: item.value, + name: item.name, + })); +} + +const renderPieInsideLabel = (props: any) => { + const { + cx = 0, + cy = 0, + midAngle = 0, + innerRadius: ir = 0, + outerRadius: or = 0, + percent = 0, + name, + } = props; + if (percent < 0.08) return null; + const toNum = (v: unknown) => { + if (typeof v === "number" && Number.isFinite(v)) return v; + if (typeof v === "string" && v.trim().endsWith("%")) return NaN; + const n = Number(v); + return Number.isFinite(n) ? n : NaN; + }; + let inner = toNum(ir); + let outer = toNum(or); + if (!Number.isFinite(outer)) { + outer = 140; + inner = Number.isFinite(inner) ? inner : 0; + } + if (!Number.isFinite(inner)) inner = 0; + const midR = inner + (outer - inner) * 0.5; + const rad = (-midAngle * Math.PI) / 180; + const x = cx + midR * Math.cos(rad); + const y = cy + midR * Math.sin(rad); + const nm = String(name ?? ""); + const short = nm.length <= 10; + const pct = `${(percent * 100).toFixed(0)}%`; + const fontSize = short ? 10 : 9; + const labelText = short ? `${name} ${pct}` : pct; + return ( + + {labelText} + + ); +}; + +function transformMultiSeriesData(data: EducationChartDatum[], series: string[]) { + return data + .filter(isMultiSeriesDatum) + .map((item) => { + const transformed: Record = { + name: item.name, + }; + + series.forEach((serie) => { + transformed[serie] = item.values?.[serie] ?? 0; + }); + + return transformed; + }); +} + +function transformDivergingData(data: EducationChartDatum[]) { + return data + .filter(isDivergingDatum) + .map((item) => ({ + name: item.name, + positive: item.positive, + negative: -Math.abs(item.negative), + })); +} + +function CustomTooltip({ active, payload, label }: TooltipProps) { + if (!active || !payload || payload.length === 0) { + return null; + } + + return ( +
+

{label}

+ {payload.map((entry, index) => ( +

+ {entry.name ?? entry.dataKey}: {String(entry.value)} +

+ ))} +
+ ); +} + +function getChartColor(index: number) { + return DEFAULT_COLORS[index % DEFAULT_COLORS.length]; +} + +function renderPieLabel({ name, percent = 0, x = 0, y = 0, textAnchor = "middle" }: PieLabelProps) { + if (percent < 0.08) { + return null; + } + + return ( + + {`${name ?? ""} ${(percent * 100).toFixed(0)}%`} + + ); +} + +function ChartLegend({ showLegend }: { showLegend: boolean }) { + if (!showLegend) { + return null; + } + + return ; +} + +export default function EducationChartPrimitives({ + chartType, + chartData, + series, + showLegend, + + divergingLabels, +}: { + chartType: EducationChartType; + chartData: EducationChartDatum[]; + series: string[]; + showLegend: boolean; + showTooltip: boolean; + divergingLabels: [string, string]; +}) { + const axisProps = { + tick: { fill: AXIS, fontSize: 12, fontFamily: "serif" }, + axisLine: { stroke: GRID }, + tickLine: { stroke: GRID }, + } as const; + + const gridProps = { + strokeDasharray: "0", + stroke: GRID, + opacity: 1, + } as const; + + const commonMargin = { top: 10, right: 12, left: 6, bottom: 8 }; + + const simpleData = toSimpleData(chartData); + + const chart = (() => { + switch (chartType) { + case "bar": + return ( + + + + + + {simpleData.map((_, index) => ( + + ))} + + + + + + ); + + case "bar-horizontal": + return ( + + + + + + + + + {simpleData.map((_, index) => ( + + ))} + + + + ); + + + + + + + + + + + case "line": + return ( + + + + + + + + + + + ); + + case "area": + return ( + + + + + + + + + + + + + + + + + ); + + + + case "pie": + return ( + + + + + {simpleData.map((_, index) => ( + + ))} + + + + ); + + case "donut": + return ( + + + + + {simpleData.map((_, index) => ( + + ))} + + + + ); + + case "scatter": { + const scatterData = toScatterData(chartData); + const labelMap = new Map(); + const xTicks = Array.from(new Set(scatterData.map((item) => item.x))).sort((a, b) => a - b); + const minTick = xTicks[0] ?? 0; + const maxTick = xTicks[xTicks.length - 1] ?? 1; + + scatterData.forEach((item) => { + labelMap.set(item.x, item.name); + }); + + return ( + + + + labelMap.get(Number(value)) ?? String(value)} + /> + + + + + {scatterData.map((_, index) => ( + + ))} + + + + ); + } + + default: + return ( +
+ Unsupported chart type +
+ ); + } + })(); + + return {chart}; +} diff --git a/electron/servers/nextjs/app/presentation-templates/Education/EducationReportChartSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Education/EducationReportChartSlide.tsx new file mode 100644 index 00000000..24d272b7 --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Education/EducationReportChartSlide.tsx @@ -0,0 +1,180 @@ +import * as z from "zod"; + +import EducationChartPrimitives, { + type EducationChartDatum, + type EducationChartType, +} from "./EducationChartPrimitives"; + +export const slideLayoutId = "education-report-chart-slide"; +export const slideLayoutName = "Education Report Chart Slide"; +export const slideLayoutDescription = + "A split education report slide with one unified schema that supports multiple Recharts chart types in the right panel."; + +const ChartTypeSchema = z.enum([ + "bar", + + "line", + "area", + + "pie", + "donut", + "scatter", +]); + +const SimpleDataSchema = z.object({ + name: z.string().min(1).max(20).meta({ + description: "Simple chart category label.", + }), + value: z.number().min(-100000).max(100000).meta({ + description: "Simple chart numeric value.", + }), +}); + +const MultiSeriesDataSchema = z.object({ + name: z.string().min(1).max(20).meta({ + description: "Grouped/stacked category label.", + }), + values: z.record(z.string(), z.number()).meta({ + description: "Series-to-value map for grouped or stacked charts.", + }), +}); + +const DivergingDataSchema = z.object({ + name: z.string().min(1).max(20).meta({ + description: "Diverging chart category label.", + }), + positive: z.number().min(0).max(100000).meta({ + description: "Positive side value.", + }), + negative: z.number().min(0).max(100000).meta({ + description: "Negative side value.", + }), +}); + +const ScatterDataSchema = z.object({ + x: z.number().min(-100000).max(100000).meta({ + description: "Scatter X coordinate.", + }), + y: z.number().min(-100000).max(100000).meta({ + description: "Scatter Y coordinate.", + }), + name: z.string().min(1).max(20).optional().meta({ + description: "Optional scatter tick label.", + }), +}); + +const UnifiedChartDataSchema = z.union([ + z.array(SimpleDataSchema), + z.array(MultiSeriesDataSchema), + z.array(DivergingDataSchema), + z.array(ScatterDataSchema), +]); + +export const Schema = z.object({ + title: z.string().min(3).max(20).default("Report").meta({ + description: "Left-side report title.", + }), + body: z.string().min(80).max(260).default( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." + ).meta({ + description: "Left-side report body paragraph.", + }), + footnote: z.string().min(20).max(150).default( + "(Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.)" + ).meta({ + description: "Left-side footnote line.", + }), + chartTitle: z.string().min(8).max(42).default("Students by Grade Level").meta({ + description: "Right-panel chart heading.", + }), + dateRange: z.string().min(8).max(22).default("Apr 10 - Apr 17").meta({ + description: "Right-panel date range label.", + }), + chartType: ChartTypeSchema.default("area").meta({ + description: + "Chart type selector. Supports bar, grouped, stacked, clustered, diverging, line, area, pie/donut, and scatter.", + }), + chartData: UnifiedChartDataSchema.default([ + { name: "Option A", value: 17.07 }, + { name: "Option B", value: 45.23 }, + { name: "Option C", value: 21.61 }, + { name: "Option D", value: 16.36 }, + ]).meta({ + description: "Unified chart data payload. Shape depends on chartType.", + }), + series: z.array(z.string().min(1).max(20)).max(6).default(["Series A", "Series B"]).meta({ + description: "Series names for grouped/stacked/clustered/area-stacked charts.", + }), + divergingLabels: z.tuple([z.string().min(1).max(24), z.string().min(1).max(24)]).default([ + "Positive", + "Negative", + ]).meta({ + description: "Legend labels for bar-diverging charts.", + }), + showLegend: z.boolean().default(true).meta({ + description: "Show or hide chart legend.", + }), + + showStatusMessage: z.boolean().default(false).meta({ + description: "Show callout message under chart (useful for weekly/performance styles).", + }), + statusMessageTitle: z.string().min(8).max(40).default("You are doing good!").meta({ + description: "Callout headline under chart.", + }), + statusMessageBody: z.string().min(10).max(80).default("You almost reached your goal").meta({ + description: "Callout subtitle under chart.", + }), +}); + +export type SchemaType = z.infer; + + + +const EducationReportChartSlide = ({ data }: { data: Partial }) => { + const slideData = Schema.parse(data); + + const chartHeightClass = slideData.showStatusMessage ? "h-[372px]" : "h-[486px]"; + + return ( +
+
+
+
+

+ {slideData.title} +

+

+ {slideData.body} +

+
+ +

+ {slideData.footnote} +

+
+ +
+

+ {slideData.chartTitle} +

+

+ {slideData.dateRange} +

+ +
+ +
+
+
+
+ ); +}; + +export default EducationReportChartSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/Education/EducationReportDonutSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Education/EducationReportDonutSlide.tsx deleted file mode 100644 index 7bff8123..00000000 --- a/electron/servers/nextjs/app/presentation-templates/Education/EducationReportDonutSlide.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import * as z from "zod"; - - -export const slideLayoutId = "education-report-donut-slide"; -export const slideLayoutName = "Education Report Donut Slide"; -export const slideLayoutDescription = - "A report slide with left-side title/content and a right-side donut chart with legend values."; - -const SegmentSchema = z.object({ - label: z.string().min(3).max(12).meta({ - description: "Legend label for one donut chart segment.", - }), - value: z.number().min(1).max(100).meta({ - description: "Percentage value for one chart segment.", - }), - color: z.string().min(4).max(20).meta({ - description: "Hex color value for one chart segment.", - }), -}); - -export const Schema = z.object({ - title: z.string().min(3).max(14).default("Report").meta({ - description: "Main heading in the left content area.", - }), - body: z.string().min(80).max(220).default( - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris." - ).meta({ - description: "Main report paragraph on the left.", - }), - footnote: z.string().min(20).max(110).default( - "(Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt.)" - ).meta({ - description: "Footnote text shown at the bottom of the left area.", - }), - chartTitle: z.string().min(8).max(26).default("Students by Grade Level").meta({ - description: "Heading shown above the donut chart.", - }), - dateRange: z.string().min(8).max(22).default("Apr 10 - Apr 17").meta({ - description: "Date range label under the chart heading.", - }), - segments: z - .array(SegmentSchema) - .min(4) - .max(4) - .default([ - { label: "Option A", value: 17.07, color: "#4A15A8" }, - { label: "Option B", value: 45.23, color: "#5B45AD" }, - { label: "Option C", value: 21.61, color: "#876FC1" }, - { label: "Option D", value: 16.36, color: "#A89ACF" }, - ]) - .meta({ - description: "Four donut segments with labels and percentages.", - }), - -}); - -export type SchemaType = z.infer; - -const EducationReportDonutSlide = ({ data }: { data: Partial }) => { - const { title, body, footnote, chartTitle, dateRange, segments } = data; - - const total = segments?.reduce((sum, item) => sum + item.value, 0) || 0; - let cursor = 0; - const conicStops = segments - ?.map((segment) => { - const start = cursor; - const span = total > 0 ? (segment.value / total) * 100 : 0; - cursor += span; - return `${segment.color} ${start}% ${cursor}%`; - }) - .join(", "); - - return ( -
-
-
-
- - -
- -
-

- {title} -

- -

- {body} -

-
- -

- {footnote} -

-
- -
-

- {chartTitle} -

-

{dateRange}

- -
-
-
-
-
- -
- {segments?.map((segment, index) => ( -
- - {segment.label} - - {segment.value.toFixed(2)}% - -
- ))} -
-
-
- -
- ); -}; - -export default EducationReportDonutSlide; diff --git a/electron/servers/nextjs/app/presentation-templates/ProductOverview/ReportSnapshotSlide.tsx b/electron/servers/nextjs/app/presentation-templates/ProductOverview/ReportSnapshotSlide.tsx index bb0063ec..e980999c 100644 --- a/electron/servers/nextjs/app/presentation-templates/ProductOverview/ReportSnapshotSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/ProductOverview/ReportSnapshotSlide.tsx @@ -1,14 +1,81 @@ import * as z from "zod"; - +import { + ResponsiveContainer, + BarChart, + Bar, + CartesianGrid, + XAxis, + YAxis, + PieChart, + Pie, + Cell, + LineChart, + Line, + LabelList, +} from "recharts"; export const slideLayoutId = "product-overview-report-snapshot-slide"; export const slideLayoutName = "Product Overview Report Snapshot Slide"; export const slideLayoutDescription = - "A report summary slide with a left-edge photo strip, title and intro copy, a compact bar chart card, and a KPI callout card on the right."; + "A report summary slide with a left-edge photo strip, title and intro copy, a chart card that supports four visual styles, and KPI callout cards on the right."; -const BarSchema = z.object({ +const LegacyBarSchema = z.object({ value: z.number().min(10).max(100).meta({ - description: "Relative bar value used in the spending mini chart.", + description: "Relative bar value used by legacy data.", + }), +}); + +const MiniBarPointSchema = z.object({ + label: z.string().min(1).max(10).meta({ + description: "Category label for the mini bar chart.", + }), + primary: z.number().min(0).max(1000).meta({ + description: "Primary series value.", + }), + secondary: z.number().min(0).max(1000).meta({ + description: "Secondary series value.", + }), +}); + +const DonutPointSchema = z.object({ + name: z.string().min(1).max(20).meta({ + description: "Donut segment label.", + }), + value: z.number().min(1).max(100).meta({ + description: "Donut segment value.", + }), +}); + +const GroupedBarPointSchema = z.object({ + label: z.string().min(1).max(12).meta({ + description: "X-axis label for grouped bars.", + }), + optionA: z.number().min(0).max(220).meta({ + description: "Option A value.", + }), + optionB: z.number().min(0).max(220).meta({ + description: "Option B value.", + }), +}); + +const TrendPointSchema = z.object({ + label: z.string().min(1).max(12).meta({ + description: "X-axis label for trend lines.", + }), + optionA: z.number().min(0).max(100).meta({ + description: "Option A trend value.", + }), + optionB: z.number().min(0).max(100).meta({ + description: "Option B trend value.", + }), +}); + +const MetricCardSchema = z.object({ + value: z.string().min(1).max(8).meta({ + description: "KPI value text.", + }), + body: z.string().min(10).max(40).meta({ + description: "KPI supporting sentence.", }), }); @@ -19,8 +86,8 @@ export const Schema = z.object({ taglineLabel: z.string().min(3).max(10).default("TAGLINE").meta({ description: "Small label above intro paragraph.", }), - taglineBody: z.string().min(40).max(100).default( - "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea." + taglineBody: z.string().min(40).max(120).default( + "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." ).meta({ description: "Intro paragraph shown beneath the heading.", }), @@ -34,13 +101,90 @@ export const Schema = z.object({ }).meta({ description: "Left-side vertical image strip.", }), + chartStyle: z.enum(["mini-bars", "donut", "grouped-bars", "dual-line"]).default("donut").meta({ + description: "Chart style variant matching Image #1 to Image #4.", + }), chartTitle: z.string().min(3).max(20).default("Sandro Tavares").meta({ description: "Name displayed in the chart card.", }), - bars: z - .array(BarSchema) + miniBars: z + .array(MiniBarPointSchema) .min(8) - .max(8) + .max(9) + .default([ + { label: "1", primary: 320, secondary: 560 }, + { label: "2", primary: 140, secondary: 840 }, + { label: "3", primary: 230, secondary: 520 }, + { label: "4", primary: 320, secondary: 660 }, + { label: "5", primary: 150, secondary: 460 }, + { label: "6", primary: 160, secondary: 850 }, + { label: "7", primary: 640, secondary: 320 }, + { label: "8", primary: 320, secondary: 440 }, + { label: "9", primary: 420, secondary: 620 }, + ]) + .meta({ + description: "Data for Image #1 mini dual-series bar chart.", + }), + donutData: z + .array(DonutPointSchema) + .min(3) + .max(3) + .default([ + { name: "Option A", value: 60 }, + { name: "Option B", value: 20 }, + { name: "Option C", value: 20 }, + ]) + .meta({ + description: "Data for Image #2 donut chart.", + }), + groupedBars: z + .array(GroupedBarPointSchema) + .min(4) + .max(4) + .default([ + { label: "label", optionA: 120, optionB: 200 }, + { label: "label", optionA: 150, optionB: 80 }, + { label: "label", optionA: 70, optionB: 110 }, + { label: "label", optionA: 130, optionB: 130 }, + ]) + .meta({ + description: "Data for Image #3 grouped bar chart.", + }), + trendLines: z + .array(TrendPointSchema) + .min(6) + .max(7) + .default([ + { label: "label", optionA: 8, optionB: 2 }, + { label: "label", optionA: 45, optionB: 65 }, + { label: "label", optionA: 35, optionB: 40 }, + { label: "label", optionA: 95, optionB: 100 }, + { label: "label", optionA: 50, optionB: 35 }, + { label: "label", optionA: 5, optionB: 75 }, + { label: "label", optionA: 55, optionB: 50 }, + ]) + .meta({ + description: "Data for Image #4 dual-line chart.", + }), + legendLabels: z.array(z.string().min(1).max(18)).min(2).max(3).default(["Option A", "Option B", "Option C"]).meta({ + description: "Legend labels used by donut/grouped/line variants.", + }), + xAxisName: z.string().min(3).max(16).default("X axis name").meta({ + description: "X axis title used in the dual-line variant.", + }), + yAxisName: z.string().min(3).max(16).default("Y axis name").meta({ + description: "Y axis title used in the dual-line variant.", + }), + footerLabel: z.string().min(10).max(60).default("Current margin: April Spendings").meta({ + description: "Footer label under mini bar chart.", + }), + footerValue: z.string().min(6).max(24).default("$350.00 / $640.00").meta({ + description: "Footer value under mini bar chart.", + }), + bars: z + .array(LegacyBarSchema) + .min(8) + .max(9) .default([ { value: 52 }, { value: 24 }, @@ -52,14 +196,25 @@ export const Schema = z.object({ { value: 55 }, ]) .meta({ - description: "Eight bars used in the spending card chart.", + description: "Legacy fallback bar values used when miniBars is not supplied.", }), metricValue: z.string().min(1).max(8).default("X 5").meta({ - description: "KPI value in the callout card.", + description: "Legacy single KPI value.", }), - metricBody: z.string().min(10).max(18).default("Lorem ipsum.").meta({ - description: "KPI short text in the callout card.", + metricBody: z.string().min(10).max(40).default("Lorem ipsum dolor sit.").meta({ + description: "Legacy single KPI body.", }), + metricCards: z + .array(MetricCardSchema) + .min(1) + .max(2) + .default([ + { value: "X 5", body: "Lorem ipsum dolor sit." }, + { value: "X 5", body: "Lorem ipsum dolor sit." }, + ]) + .meta({ + description: "One or two KPI cards shown on the right.", + }), metricIcon: z.object({ __icon_url__: z.string().default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"), __icon_query__: z.string().min(3).max(30).default("pulse icon"), @@ -74,86 +229,357 @@ export const Schema = z.object({ export type SchemaType = z.infer; +const MINI_BAR_DARK = "#0B4B40"; +const MINI_BAR_LIGHT = "#CED3D1"; +const DONUT_COLORS = ["#0B4B40", "#4B6B61", "#7B938C"]; +const KPI_ICON_BG = "#063C73"; + +const PulseIcon = () => ( + +); + +const renderDonutPercentLabel = ({ cx, cy, midAngle, outerRadius, percent }: any) => { + const radius = (outerRadius ?? 0) + 18; + const x = (cx ?? 0) + radius * Math.cos((-(midAngle ?? 0) * Math.PI) / 180); + const y = (cy ?? 0) + radius * Math.sin((-(midAngle ?? 0) * Math.PI) / 180); + + return ( + + + + {`${Math.round((percent ?? 0) * 100)}%`} + + + ); +}; + const ReportSnapshotSlide = ({ data }: { data: Partial }) => { const { title, taglineLabel, taglineBody, sideImage, + chartStyle, chartTitle, + miniBars, + donutData, + groupedBars, + trendLines, + legendLabels, + xAxisName, + yAxisName, + footerLabel, + footerValue, bars, metricValue, metricBody, + metricCards, metricIcon, } = data; + const resolvedMiniBars = + miniBars && miniBars.length >= 8 + ? miniBars + : (bars ?? []).map((bar, index) => { + const scaledPrimary = Math.round(bar.value * 8 + 80); + const scaledSecondary = Math.min(1000, scaledPrimary + 220); + + return { + label: `${index + 1}`, + primary: scaledPrimary, + secondary: scaledSecondary, + }; + }); + + const fallbackMetric = { + value: metricValue ?? "X 5", + body: metricBody ?? "Lorem ipsum dolor sit.", + }; + + const resolvedMetricCards = + metricCards && metricCards.length > 0 + ? metricCards + : [fallbackMetric, fallbackMetric]; + + const visibleMetricCards = + (chartStyle ?? "mini-bars") === "mini-bars" + ? resolvedMetricCards.slice(0, 1) + : resolvedMetricCards.slice(0, 2); + + const usePulseFallback = !metricIcon?.__icon_url__ || metricIcon.__icon_url__.includes("placeholder.svg"); + const activeChartStyle = chartStyle ?? "mini-bars"; + const donutTotal = (donutData ?? []).reduce((sum, item) => sum + item.value, 0) || 1; + return (
{sideImage?.__image_url__ && ( {sideImage?.__image_prompt__} )} -
-

+
+

{title}

-
-

+

+

{taglineLabel}

-

{taglineBody}

-
-
- -
-

Spendings

-

- {chartTitle} -

- -
- {bars?.map((bar, index) => ( -
- ))} -
- -
-

Current margin: April Spendings

-

$350.00 / $640.00

-
-
- -
-
-
- {metricIcon?.__icon_query__} -
-

- {metricValue} +

+ {taglineBody}

-

- {metricBody} -

+
+ +
+

{chartTitle}

+ + {activeChartStyle === "mini-bars" && ( + <> +
+ + + + + `$${value}`} + axisLine={false} + tickLine={false} + tick={{ fill: "#6C7271", fontSize: 10 }} + /> + + + + +
+ +
+

{footerLabel}

+

{footerValue}

+
+ + )} + + {activeChartStyle === "donut" && ( +
+
+ + + + {(donutData ?? []).map((entry, index) => ( + + ))} + + + +
+ +
+ {(donutData ?? []).map((entry, index) => { + const percent = Math.round((entry.value / donutTotal) * 100); + return ( +
+
+ +

+ {legendLabels?.[index] ?? entry.name} +

+
+

{percent}%

+
+ ); + })} +
+
+ )} + + {activeChartStyle === "grouped-bars" && ( +
+
+ + + + + + + + + + + + + +
+ +
+
+ +

{legendLabels?.[0] ?? "Option A"}

+
+
+ +

{legendLabels?.[1] ?? "Option B"}

+
+
+
+ )} + + {activeChartStyle === "dual-line" && ( +
+
+ + + + + + + + + +
+ +
+
+ +

{legendLabels?.[0] ?? "Option A"}

+
+
+ +

{legendLabels?.[1] ?? "Option B"}

+
+
+
+ )} +
+ +
+
+ {visibleMetricCards.map((metric, index) => ( +
+
+
+ {usePulseFallback ? ( + + ) : ( + {metricIcon?.__icon_query__} + )} +
+

{metric.value}

+
+

{metric.body}

+
+ ))} +
); diff --git a/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisBarSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisBarSlide.tsx index 6778d07e..f1cc57b2 100644 --- a/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisBarSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisBarSlide.tsx @@ -1,10 +1,13 @@ +"use client"; + import * as z from "zod"; +import { ResponsiveContainer } from "recharts"; -import { WorkflowBarChart } from "./chartPrimitives"; +import { FlexibleReportChart, flexibleChartDataSchema } from "./flexibleReportChart"; const InsightItemSchema = z.object({ - title: z.string().min(3).max(18).meta({ + title: z.string().min(3).max(12).meta({ description: "Short insight title shown next to the icon.", }), description: z.string().min(20).max(84).meta({ @@ -12,15 +15,6 @@ const InsightItemSchema = z.object({ }), }); -const ChartPointSchema = z.object({ - label: z.string().min(1).max(12).meta({ - description: "Chart axis label.", - }), - value: z.number().min(0).max(1000).meta({ - description: "Bar chart value.", - }), -}); - export const slideLayoutId = "data-analysis-bar-slide"; export const slideLayoutName = "Data Analysis Bar Slide"; export const slideLayoutDescription = @@ -30,16 +24,20 @@ export const Schema = z.object({ title: z.string().min(3).max(28).default("Data Analysis").meta({ description: "Slide title shown at the top-left.", }), - itemIcon: z.object({ - __icon_url__: z.string().default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"), - __icon_query__: z.string().default("pulse icon"), - }).default({ - __icon_url__: - "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg", - __icon_query__: "pulse icon", - }).meta({ - description: "Icon shown in each analysis item badge.", - }), + itemIcon: z + .object({ + __icon_url__: z + .string() + .default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"), + __icon_query__: z.string().default("pulse icon"), + }) + .default({ + __icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg", + __icon_query__: "pulse icon", + }) + .meta({ + description: "Icon shown in each analysis item badge.", + }), items: z .array(InsightItemSchema) .min(3) @@ -52,22 +50,19 @@ export const Schema = z.object({ .meta({ description: "Three analysis points shown in the left column.", }), - chartData: z - .array(ChartPointSchema) - .min(7) - .max(7) - .default([ - { label: "Mon", value: 120 }, - { label: "Tue", value: 200 }, - { label: "Wed", value: 150 }, - { label: "Thu", value: 80 }, - { label: "Fri", value: 70 }, - { label: "Sat", value: 110 }, - { label: "Sun", value: 130 }, - ]) - .meta({ - description: "Weekly values shown in the bar chart.", - }), + chartData: flexibleChartDataSchema.default({ + type: "pie", + data: [ + { name: "Mon", value: 120 }, + { name: "Tue", value: 200 }, + { name: "Wed", value: 150 }, + { name: "Thu", value: 80 }, + { name: "Fri", value: 70 }, + { name: "Sat", value: 110 }, + { name: "Sun", value: 130 }, + ], + + }), legendLabel: z.string().min(3).max(32).default("Traditional Workflow").meta({ description: "Legend label shown below the chart.", }), @@ -76,20 +71,17 @@ export const Schema = z.object({ export type SchemaType = z.infer; const DataAnalysisBarSlide = ({ data }: { data: Partial }) => { - const { title, itemIcon, items, chartData, legendLabel } = data; + const rows = chartData?.data ?? []; + const chartType = chartData?.type ?? "bar"; + const series = chartData?.series ?? []; return (
-
+
-

- {title} -

+

{title}

@@ -105,20 +97,18 @@ const DataAnalysisBarSlide = ({ data }: { data: Partial }) => { style={{ filter: "brightness(0) invert(1)" }} />
-

- {item.title} -

+

{item.title}

-

- {item.description} -

+

{item.description}

))}
- + + +
diff --git a/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisDashboardSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisDashboardSlide.tsx index c5af4502..02fc5580 100644 --- a/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisDashboardSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisDashboardSlide.tsx @@ -1,13 +1,20 @@ +"use client"; + import type { ReactNode } from "react"; import * as z from "zod"; +import { ResponsiveContainer } from "recharts"; + import { - AreaTrendChart, - CompactBarChart, - CompactPieChart, - SemiDonutChart, - TrendLineChart, -} from "./chartPrimitives"; + DivergingDataPointSchema, + FlexibleReportChart, + MultiSeriesDataPointSchema, + ScatterDataPointSchema, + SimpleDataPointSchema, + flexibleChartDataSchema, + flexibleChartTypeSchema, + type FlexibleChartData, +} from "./flexibleReportChart"; const SummaryCardSchema = z.object({ value: z.string().min(1).max(8).meta({ @@ -16,162 +23,111 @@ const SummaryCardSchema = z.object({ label: z.string().min(3).max(20).meta({ description: "Short summary card label.", }), -}); - -const ChartPointSchema = z.object({ - label: z.string().min(1).max(12).meta({ - description: "Chart axis label.", - }), - value: z.number().min(0).max(1000).meta({ - description: "Single-series chart value.", + icon: z.object({ + __icon_url__: z.string().default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"), + __icon_query__: z.string().default("pulse icon"), + }).optional().meta({ + description: "Icon shown in each compact summary card.", + }).default({ + __icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg", + __icon_query__: "pulse icon", }), }); -const DualChartPointSchema = z.object({ - label: z.string().min(1).max(12).meta({ - description: "Chart axis label.", - }), - valueA: z.number().min(0).max(1000).meta({ - description: "First series value.", - }), - valueB: z.number().min(0).max(1000).meta({ - description: "Second series value.", - }), -}); -const PieSegmentSchema = z.object({ - name: z.string().min(1).max(18).meta({ - description: "Category name shown in chart legends.", - }), - value: z.number().min(1).max(1000).meta({ - description: "Category value used in the chart.", - }), -}); + + + + + export const slideLayoutId = "data-analysis-dashboard-slide"; export const slideLayoutName = "Data Analysis Dashboard Slide"; export const slideLayoutDescription = - "A dashboard-style slide with a title at the top, a row of compact summary cards underneath, and two stacked dashboard panels below. Each panel is split into three chart cells, creating a six-chart overview made of bar, donut, line, area, pie, and comparison charts."; + "A dashboard-style slide with a title, summary cards, and a responsive grid of chart panels (1–9). Each panel uses the same flexible chart types as other report slides; labels and margins are compact for small cells."; + +const ChartItemSchema = z.object({ + + type: flexibleChartTypeSchema.default('bar'), + data: z.union([ + z.array(SimpleDataPointSchema), + z.array(MultiSeriesDataPointSchema), + z.array(DivergingDataPointSchema), + z.array(ScatterDataPointSchema), + ]).default([ + { name: 'Q1', value: 45 }, + { name: 'Q2', value: 72 }, + { name: 'Q3', value: 58 }, + { name: 'Q4', value: 89 }, + ]), + series: z.array(z.string()).optional(), +}); export const Schema = z.object({ - title: z.string().min(3).max(28).default("Data Analysis").meta({ + title: z.string().min(3).max(12).default("Data Analysis").meta({ description: "Slide title shown at the top-left.", }), - summaryIcon: z.object({ - __icon_url__: z.string().default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"), - __icon_query__: z.string().default("pulse icon"), - }).default({ - __icon_url__: - "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg", - __icon_query__: "pulse icon", - }).meta({ - description: "Icon shown in each compact summary card.", - }), + summaryCards: z .array(SummaryCardSchema) - .min(4) + .min(2) .max(4) + .optional() .default([ - { value: "5", label: "Text 1" }, - { value: "52", label: "Text 2" }, - { value: "4", label: "Text 3" }, - { value: "80%", label: "Text 4" }, + { + value: "5", label: "Text 1", icon: { + __icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg", + __icon_query__: "placeholder icon", + } + }, + { + value: "52", label: "Text 2", icon: { + __icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg", + __icon_query__: "placeholder icon", + } + }, + { + value: "4", label: "Text 3", icon: { + __icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg", + __icon_query__: "placeholder icon", + } + }, + { + value: "80%", label: "Text 4", icon: { + __icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg", + __icon_query__: "placeholder icon", + } + }, ]) .meta({ description: "Four compact summary cards displayed above the dashboard panels.", }), - workflowBars: z - .array(ChartPointSchema) - .min(7) - .max(7) - .default([ - { label: "Mon", value: 120 }, - { label: "Tue", value: 200 }, - { label: "Wed", value: 150 }, - { label: "Thu", value: 80 }, - { label: "Fri", value: 70 }, - { label: "Sat", value: 110 }, - { label: "Sun", value: 130 }, - ]) - .meta({ - description: "Bar chart data shown in the top-left dashboard cell.", - }), - gaugeSegments: z - .array(PieSegmentSchema) - .min(3) - .max(3) - .default([ - { name: "Category A", value: 45 }, - { name: "Category B", value: 30 }, - { name: "Category C", value: 25 }, - ]) - .meta({ - description: "Three segments used in the top-center semi-donut chart.", - }), - trendSeries: z - .array(DualChartPointSchema) - .min(7) - .max(7) - .default([ - { label: "Label", valueA: 22, valueB: 35 }, - { label: "Label", valueA: 54, valueB: 26 }, - { label: "Label", valueA: 44, valueB: 70 }, - { label: "Label", valueA: 78, valueB: 52 }, - { label: "Label", valueA: 50, valueB: 44 }, - { label: "Label", valueA: 32, valueB: 60 }, - { label: "Label", valueA: 58, valueB: 40 }, - ]) - .meta({ - description: "Two-series line chart data shown in the top-right cell.", - }), - detailedArea: z - .array(ChartPointSchema) - .min(7) - .max(7) - .default([ - { label: "12:00", value: 22 }, - { label: "13:00", value: 64 }, - { label: "14:00", value: 48 }, - { label: "15:00", value: 56 }, - { label: "16:00", value: 41 }, - { label: "17:00", value: 58 }, - { label: "18:00", value: 63 }, - ]) - .meta({ - description: "Area chart data shown in the bottom-left dashboard cell.", - }), - shareBreakdown: z - .array(PieSegmentSchema) - .min(3) - .max(3) - .default([ - { name: "Category A", value: 50 }, - { name: "Category B", value: 30 }, - { name: "Category C", value: 20 }, - ]) - .meta({ - description: "Pie chart data shown in the bottom-center dashboard cell.", - }), - comparisonBars: z - .array(ChartPointSchema) - .min(7) - .max(7) - .default([ - { label: "Jan", value: 70 }, - { label: "Feb", value: 170 }, - { label: "Mar", value: 110 }, - { label: "Apr", value: 42 }, - { label: "May", value: 88 }, - { label: "Jun", value: 106 }, - { label: "Jul", value: 112 }, - ]) - .meta({ - description: "Bar chart data shown in the bottom-right dashboard cell.", - }), + charts: z.array(ChartItemSchema).min(1).max(6).default([ + { type: 'bar', data: [{ name: 'Q1', value: 125000 }, { name: 'Q2', value: 158000 }, { name: 'Q3', value: 142000 }, { name: 'Q4', value: 189000 }] }, + { type: 'donut', data: [{ name: 'North America', value: 35 }, { name: 'Europe', value: 28 }, { name: 'Asia Pacific', value: 25 }, { name: 'Others', value: 12 }] }, + { type: 'line', data: [{ name: 'Jan', value: 30 }, { name: 'Feb', value: 45 }, { name: 'Mar', value: 52 }, { name: 'Apr', value: 48 }, { name: 'May', value: 67 }, { name: 'Jun', value: 82 }] }, + { type: 'bar', data: [{ name: 'Sales', value: 87 }, { name: 'Marketing', value: 72 }, { name: 'Engineering', value: 95 }, { name: 'Support', value: 68 }] }, + { type: 'bar-clustered', data: [{ name: 'Q1', values: { 'Product A': 45, 'Product B': 62 } }, { name: 'Q2', values: { 'Product A': 58, 'Product B': 71 } }, { name: 'Q3', values: { 'Product A': 72, 'Product B': 65 } }], series: ['Product A', 'Product B'] }, + { type: 'bar-diverging', data: [{ name: 'Quality', positive: 78, negative: 22 }, { name: 'Service', positive: 65, negative: 35 }, { name: 'Price', positive: 42, negative: 58 }], series: ['Satisfied', 'Unsatisfied'] }, + ]), }); export type SchemaType = z.infer; + + + + + + + +const getChartHeight = (count: number, hasMetrics: boolean) => { + if (count <= 2) return hasMetrics ? 230 : 280; + if (count <= 3) return hasMetrics ? 210 : 260; + return hasMetrics ? 160 : 180; +}; + + function SummaryCard({ value, label, @@ -184,164 +140,96 @@ function SummaryCard({ iconAlt?: string; }) { return ( -
-
+
+
{iconAlt
-
-

- {value} -

-

{label}

+
+

{value}

+

{label}

); } -function ChartCell({ - children, - footer, - topLegend, -}: { - children: ReactNode; - footer?: ReactNode; - topLegend?: ReactNode; -}) { - return ( -
- {topLegend &&
{topLegend}
} -
{children}
- {footer &&
{footer}
} -
- ); -} -function DotLegend({ - items, -}: { - items: { label: string; color: string }[]; -}) { - return ( -
- {items.map((item) => ( - - - {item.label} - - ))} -
- ); -} + + const DataAnalysisDashboardSlide = ({ data }: { data: Partial }) => { - - const { title, summaryIcon, summaryCards, workflowBars, gaugeSegments, trendSeries, detailedArea, shareBreakdown, comparisonBars } = data; + const { title, summaryCards, charts } = data; + const halfChart = charts?.slice(0, Math.ceil(charts.length / 2)); + const otherHalfChart = charts?.slice(Math.ceil(charts.length / 2)); return ( -
-
+
+
-
-

- {title} -

+
+

{title}

-
+ {summaryCards && summaryCards.length > 0 &&
{summaryCards?.map((card, index) => ( ))} -
+
} +
-
-
- - } + {halfChart && halfChart.length > 0 &&
+
- - + {halfChart?.map((chart, index) => ( +
- } - > - - + > - -

Category A

-

Category B

+
+ +
- } + ))} +
+
} + {otherHalfChart && otherHalfChart.length > 0 &&
+
- - -
+ {otherHalfChart?.map((chart, index) => ( +
- - } - > - - + > - - } - > - - +
+ + + - - } - > - - -
+
+
+ ))} +
+
}
); diff --git a/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisInsightBarSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisInsightBarSlide.tsx index bb4839a1..ae8d24d7 100644 --- a/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisInsightBarSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisInsightBarSlide.tsx @@ -1,57 +1,54 @@ +"use client"; + import * as z from "zod"; +import { ResponsiveContainer } from "recharts"; -import { WorkflowBarChart } from "./chartPrimitives"; - -const ChartPointSchema = z.object({ - label: z.string().min(1).max(12).meta({ - description: "Chart axis label.", - }), - value: z.number().min(0).max(1000).meta({ - description: "Bar chart value.", - }), -}); +import { FlexibleReportChart, flexibleChartDataSchema } from "./flexibleReportChart"; export const slideLayoutId = "data-analysis-insight-bar-slide"; export const slideLayoutName = "Data Analysis Insight Bar Slide"; export const slideLayoutDescription = - "A slide with a title at the top, a single featured insight block on the left containing an icon badge and a paragraph, and a bar chart on the right with a legend below it."; + "A slide with a title at the top, a single featured insight block on the left containing an icon badge and a paragraph, and a chart on the right with a legend below it."; + export const Schema = z.object({ - title: z.string().min(3).max(28).default("Data Analysis").meta({ + title: z.string().min(3).max(12).default("Data Analysis").meta({ description: "Slide title shown at the top-left.", }), - insightIcon: z.object({ - __icon_url__: z.string().default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"), - __icon_query__: z.string().default("pulse icon"), - }).default({ - __icon_url__: - "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg", - __icon_query__: "pulse icon", - }).meta({ - description: "Icon shown in the featured insight badge.", - }), - insightBody: z.string().min(80).max(320).default( - "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis" - ).meta({ - description: "Featured insight paragraph shown in the left content area.", - }), - chartData: z - .array(ChartPointSchema) - .min(7) - .max(7) - .default([ - { label: "Mon", value: 120 }, - { label: "Tue", value: 200 }, - { label: "Wed", value: 150 }, - { label: "Thu", value: 80 }, - { label: "Fri", value: 70 }, - { label: "Sat", value: 110 }, - { label: "Sun", value: 130 }, - ]) + insightIcon: z + .object({ + __icon_url__: z + .string() + .default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg"), + __icon_query__: z.string().default("pulse icon"), + }) + .default({ + __icon_url__: "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg", + __icon_query__: "pulse icon", + }) .meta({ - description: "Weekly values shown in the right-side bar chart.", + description: "Icon shown in the featured insight badge.", }), + insightBody: z + .string() + .min(80) + .max(320) + .default( + "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis" + ) + .meta({ + description: "Featured insight paragraph shown in the left content area.", + }), + chartData: flexibleChartDataSchema.default({ + type: "pie", + data: [ + { name: "Q1", value: 45 }, + { name: "Q2", value: 72 }, + { name: "Q3", value: 58 }, + { name: "Q4", value: 89 }, + ], + }), legendLabel: z.string().min(3).max(32).default("Traditional Workflow").meta({ description: "Legend label shown below the chart.", }), @@ -64,46 +61,40 @@ const DataAnalysisInsightBarSlide = ({ }: { data: Partial; }) => { - - const { title, insightIcon, insightBody, chartData, legendLabel } = data; + const chartData = data?.chartData?.data ?? []; + const chartType = data?.chartData?.type ?? "bar"; + const series = data?.chartData?.series ?? []; return (
-
+
-

- {title} -

+

{data.title}

-
-
+
+
{insightIcon?.__icon_query__}
-

- {insightBody} -

+

{data.insightBody}

-
-
- -
+
+ + +
-

{legendLabel}

+

{data.legendLabel}

diff --git a/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisLineStatsSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisLineStatsSlide.tsx index c19cb3d7..e946fec1 100644 --- a/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisLineStatsSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisLineStatsSlide.tsx @@ -1,18 +1,11 @@ +"use client"; + +import { Fragment } from "react/jsx-runtime"; import * as z from "zod"; -import { DualLineChart } from "./chartPrimitives"; +import { ResponsiveContainer } from "recharts"; -const LinePointSchema = z.object({ - label: z.string().min(1).max(12).meta({ - description: "Chart axis label.", - }), - valueA: z.number().min(0).max(1000).meta({ - description: "First series value.", - }), - valueB: z.number().min(0).max(1000).meta({ - description: "Second series value.", - }), -}); +import { FlexibleReportChart, flexibleChartDataSchema } from "./flexibleReportChart"; const MetricSchema = z.object({ value: z.string().min(1).max(12).meta({ @@ -38,7 +31,7 @@ export const slideLayoutDescription = "A slide with a title at the top, a two-series line chart in the left content area, and two tall metric cards arranged side by side on the right. Each metric card contains two stacked metric blocks."; export const Schema = z.object({ - title: z.string().min(3).max(28).default("Data Analysis").meta({ + title: z.string().min(3).max(12).default("Data Analysis").meta({ description: "Slide title shown at the top-left.", }), seriesALabel: z.string().min(3).max(20).default("Category A").meta({ @@ -47,11 +40,9 @@ export const Schema = z.object({ seriesBLabel: z.string().min(3).max(20).default("Category B").meta({ description: "Legend label for the second line series.", }), - lineData: z - .array(LinePointSchema) - .min(7) - .max(7) - .default([ + chartData: flexibleChartDataSchema.default({ + type: "pie", + data: [ { label: "label", valueA: 24, valueB: 40 }, { label: "label", valueA: 55, valueB: 72 }, { label: "label", valueA: 50, valueB: 98 }, @@ -59,10 +50,11 @@ export const Schema = z.object({ { label: "label", valueA: 70, valueB: 52 }, { label: "label", valueA: 42, valueB: 78 }, { label: "label", valueA: 63, valueB: 51 }, - ]) - .meta({ - description: "Line chart data displayed on the left side of the slide.", - }), + ], + }), + legendLabel: z.string().min(3).max(32).default("Traditional Workflow").meta({ + description: "Legend label shown below the chart.", + }), statColumns: z .array(StatColumnSchema) .min(2) @@ -94,60 +86,41 @@ type StatMetric = { description: string; }; -function StatPill({ - metrics, - -}: { - metrics: StatMetric[]; - -}) { - - +function StatPill({ metrics }: { metrics: StatMetric[] }) { return ( -
- +
{metrics.map((metric, index) => ( - <> -
-

- {metric.value} -

+ +
+

{metric.value}

{metric.label}

-

- {metric.description} -

+

{metric.description}

- {index === 0 &&
- - - - -
- } - + {index === 0 && ( +
+ + + +
+ )} +
))} - -
); } + const DataAnalysisLineStatsSlide = ({ data }: { data: Partial }) => { - const { title, seriesALabel, seriesBLabel, lineData, statColumns } = data; + const { title, seriesALabel, seriesBLabel, chartData, statColumns, legendLabel } = data; + const rows = chartData?.data ?? []; + const chartType = chartData?.type ?? "line-dual"; + const series = chartData?.series ?? []; return (
-
+
-

- {title} -

+

{title}

@@ -164,11 +137,20 @@ const DataAnalysisLineStatsSlide = ({ data }: { data: Partial }) =>
- + + +
-
- X axis name +
+ +

{data.legendLabel}

diff --git a/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisListSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisListSlide.tsx index 60c07657..5f09fad3 100644 --- a/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisListSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Report/DataAnalysisListSlide.tsx @@ -2,10 +2,10 @@ import * as z from "zod"; const AnalysisItemSchema = z.object({ - title: z.string().min(3).max(18).meta({ + title: z.string().max(12).meta({ description: "Short item title displayed next to the icon.", }), - description: z.string().min(20).max(84).meta({ + description: z.string().max(30).meta({ description: "Supporting sentence shown below the title.", }), }); @@ -16,7 +16,7 @@ export const slideLayoutDescription = "A slide with a title at the top and a two-column list of analysis points underneath. Each point contains a small circular icon badge, a short title on the same row, and a supporting description directly below."; export const Schema = z.object({ - title: z.string().min(3).max(28).default("Data Analysis").meta({ + title: z.string().min(3).max(12).default("Data Analysis").meta({ description: "Slide title shown at the top-left.", }), itemIcon: z.object({ @@ -31,7 +31,7 @@ export const Schema = z.object({ }), items: z .array(AnalysisItemSchema) - .min(6) + .max(6) .default([ { title: "Title 1", description: "Ut enim ad minima veniam, quis." }, diff --git a/electron/servers/nextjs/app/presentation-templates/Report/IntroSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/IntroSlide.tsx index d84fa166..1af15451 100644 --- a/electron/servers/nextjs/app/presentation-templates/Report/IntroSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Report/IntroSlide.tsx @@ -1,10 +1,10 @@ import * as z from "zod"; export const Schema = z.object({ - title: z.string().min(1).default("Company's "), - subtitle: z.string().min(1).default("Report"), - name: z.string().min(1).default("John Doe"), - position: z.string().min(1).default("Company Name | Strategy, Content, growth"), + title: z.string().min(1).max(12).default("Company's "), + subtitle: z.string().min(1).max(15).default("Report"), + name: z.string().min(1).max(10).default("John Doe"), + position: z.string().min(1).max(20).default("Company Name | Strategy, Content, growth"), }) export type SchemaType = z.infer; export const slideLayoutId = "intro-slide"; diff --git a/electron/servers/nextjs/app/presentation-templates/Report/IntroductionImageSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/IntroductionImageSlide.tsx index b9529372..327e7f8a 100644 --- a/electron/servers/nextjs/app/presentation-templates/Report/IntroductionImageSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Report/IntroductionImageSlide.tsx @@ -7,17 +7,17 @@ export const slideLayoutDescription = "A slide with a title at the top-left, a paragraph block beneath the title, a short bulleted list in the lower-left area, and a large supporting image anchored on the right side of the slide."; export const Schema = z.object({ - title: z.string().min(3).max(32).default("Introduction").meta({ + title: z.string().min(3).max(12).default("Introduction").meta({ description: "Slide title shown at the top-left.", }), - body: z.string().min(60).max(280).default( + body: z.string().max(250).default( "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis" ).meta({ description: "Primary paragraph shown under the title.", }), bullets: z - .array(z.string().min(20).max(80)) - .min(4) + .array(z.string().max(35)) + .max(4) .default([ "Ut enim ad minima veniam, quis nostrum", diff --git a/electron/servers/nextjs/app/presentation-templates/Report/IntroductionStatsSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/IntroductionStatsSlide.tsx index f938a485..930d58ea 100644 --- a/electron/servers/nextjs/app/presentation-templates/Report/IntroductionStatsSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Report/IntroductionStatsSlide.tsx @@ -1,13 +1,14 @@ +import { Fragment } from "react/jsx-runtime"; import * as z from "zod"; const MetricSchema = z.object({ - value: z.string().min(1).max(12).meta({ + value: z.string().min(1).max(6).meta({ description: "Primary metric value shown in the card.", }), - label: z.string().min(3).max(24).meta({ + label: z.string().min(3).max(10).meta({ description: "Short metric label shown below the value.", }), - description: z.string().min(6).max(36).meta({ + description: z.string().min(6).max(20).meta({ description: "Supporting text shown below the label.", }), }); @@ -24,17 +25,17 @@ export const slideLayoutDescription = "A slide with a title and explanatory text on the left, a bulleted list underneath the text, and two tall metric cards placed side by side on the right. Each metric card contains two stacked metric blocks."; export const Schema = z.object({ - title: z.string().min(3).max(32).default("Introduction").meta({ + title: z.string().min(3).max(12).default("Introduction").meta({ description: "Slide title shown at the top-left.", }), - body: z.string().min(60).max(320).default( + body: z.string().max(250).default( "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alut enim ad minima veniam, quis" ).meta({ description: "Primary paragraph shown below the title.", }), bullets: z - .array(z.string().min(20).max(80)) - .min(4) + .array(z.string().max(35)) + .max(4) .default([ "Ut enim ad minima veniam, quis nostrum", @@ -47,7 +48,7 @@ export const Schema = z.object({ }), statColumns: z .array(StatColumnSchema) - .min(2) + .max(2) .default([ { @@ -89,7 +90,7 @@ function StatPill({
{metrics.map((metric, index) => ( - <> +
} - +
))} diff --git a/electron/servers/nextjs/app/presentation-templates/Report/MilestoneSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/MilestoneSlide.tsx index 5db5a866..94470948 100644 --- a/electron/servers/nextjs/app/presentation-templates/Report/MilestoneSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Report/MilestoneSlide.tsx @@ -4,10 +4,10 @@ const MilestoneItemSchema = z.object({ stepNumber: z.string().min(2).max(4).meta({ description: "Short milestone number such as 01 or 05.", }), - heading: z.string().min(3).max(18).meta({ + heading: z.string().min(3).max(10).meta({ description: "Heading displayed below the milestone marker.", }), - description: z.string().min(20).max(80).meta({ + description: z.string().min(20).max(50).meta({ description: "Supporting milestone description shown under the heading.", }), }); @@ -18,7 +18,7 @@ export const slideLayoutDescription = "A slide with a title at the top and a single horizontal milestone sequence below it. The sequence contains five circular markers aligned in one row, and each marker has a heading and description placed directly underneath. The activeIndex field controls which marker is emphasized while the remaining markers stay in the default state."; export const Schema = z.object({ - title: z.string().min(3).max(24).default("Milestone").meta({ + title: z.string().min(3).max(12).default("Milestone").meta({ description: "Slide title shown at the top-left.", }), activeIndex: z.number().int().min(0).max(4).default(4).meta({ diff --git a/electron/servers/nextjs/app/presentation-templates/Report/PerformanceSnapshotSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/PerformanceSnapshotSlide.tsx index eea9d05e..fa2902b3 100644 --- a/electron/servers/nextjs/app/presentation-templates/Report/PerformanceSnapshotSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Report/PerformanceSnapshotSlide.tsx @@ -1,13 +1,14 @@ +import { Fragment } from "react/jsx-runtime"; import * as z from "zod"; const MetricSchema = z.object({ - value: z.string().min(1).max(12).meta({ + value: z.string().min(1).max(6).meta({ description: "Primary metric value shown in the pill.", }), - label: z.string().min(3).max(24).meta({ + label: z.string().min(3).max(10).meta({ description: "Short label shown below the metric value.", }), - description: z.string().min(6).max(36).meta({ + description: z.string().min(6).max(20).meta({ description: "Supporting metric description shown below the label.", }), }); @@ -24,7 +25,7 @@ export const slideLayoutDescription = "A slide with a title at the top and three tall metric cards arranged horizontally below it. Each card can contain one or two stacked metric blocks, and each block includes a main value, a label, and a supporting description."; export const Schema = z.object({ - title: z.string().min(3).max(40).default("Performance Snapshot").meta({ + title: z.string().min(3).max(12).default("Performance Snapshot").meta({ description: "Slide title shown at the top-left.", }), columns: z @@ -82,7 +83,7 @@ function StatPill({
{metrics.map((metric, index) => ( - <> +
} - +
))} diff --git a/electron/servers/nextjs/app/presentation-templates/Report/ServicesSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/ServicesSlide.tsx index 497eebf1..9df93cdb 100644 --- a/electron/servers/nextjs/app/presentation-templates/Report/ServicesSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Report/ServicesSlide.tsx @@ -16,7 +16,7 @@ const ServiceItemSchema = z.object({ heading: z.string().min(3).max(18).meta({ description: "Heading shown below the service icon.", }), - description: z.string().min(20).max(84).meta({ + description: z.string().min(20).max(50).meta({ description: "Supporting description below the service heading.", }), }); @@ -27,7 +27,7 @@ export const slideLayoutDescription = "A slide with a title and a three-step horizontal service flow. Each step contains a circular icon area, a heading, and a description placed underneath. Directional connectors between the circles indicate sequence, and the activeIndex field determines which step is emphasized."; export const Schema = z.object({ - title: z.string().min(3).max(24).default("Services").meta({ + title: z.string().min(3).max(12).default("Services").meta({ description: "Slide title shown at the top-left.", }), diff --git a/electron/servers/nextjs/app/presentation-templates/Report/SolutionSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/SolutionSlide.tsx index e724d4a9..8e881bce 100644 --- a/electron/servers/nextjs/app/presentation-templates/Report/SolutionSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Report/SolutionSlide.tsx @@ -10,13 +10,13 @@ const CardSchema = z.object({ stepNumber: z.string().min(2).max(4).meta({ description: "Short card step number such as 01, 02, or 03.", }), - description: z.string().min(20).max(90).meta({ + description: z.string().min(20).max(50).meta({ description: "Card body copy displayed inside the feature pill.", }), }); export const Schema = z.object({ - title: z.string().min(3).max(24).default("Solution").meta({ + title: z.string().min(3).max(12).default("Solution").meta({ description: "Slide heading shown in the top-left corner.", }), showImage: z.boolean().default(true).meta({ diff --git a/electron/servers/nextjs/app/presentation-templates/Report/TeamSlide.tsx b/electron/servers/nextjs/app/presentation-templates/Report/TeamSlide.tsx index 0e4b62f3..21483463 100644 --- a/electron/servers/nextjs/app/presentation-templates/Report/TeamSlide.tsx +++ b/electron/servers/nextjs/app/presentation-templates/Report/TeamSlide.tsx @@ -2,10 +2,10 @@ import * as z from "zod"; const MemberSchema = z.object({ - title: z.string().min(2).max(24).meta({ + title: z.string().min(2).max(12).meta({ description: "Short role or title shown above the member name.", }), - name: z.string().min(2).max(32).meta({ + name: z.string().min(2).max(20).meta({ description: "Member name shown at the bottom of the card.", }), image: z.object({ @@ -25,7 +25,7 @@ export const slideLayoutDescription = export const Schema = z.object({ members: z .array(MemberSchema) - .min(5) + .min(2) .max(5) .default([ { diff --git a/electron/servers/nextjs/app/presentation-templates/Report/chartPrimitives.tsx b/electron/servers/nextjs/app/presentation-templates/Report/chartPrimitives.tsx deleted file mode 100644 index 87b7b82f..00000000 --- a/electron/servers/nextjs/app/presentation-templates/Report/chartPrimitives.tsx +++ /dev/null @@ -1,338 +0,0 @@ -"use client"; - -import { - Area, - AreaChart, - Bar, - BarChart, - CartesianGrid, - Cell, - LabelList, - Line, - LineChart, - Pie, - PieChart, - ResponsiveContainer, - XAxis, - YAxis, -} from "recharts"; - -type SimpleBarDatum = { - label: string; - value: number; -}; - -type DualSeriesDatum = { - label: string; - valueA: number; - valueB: number; -}; - -type PieDatum = { - name: string; - value: number; -}; - -type PieLabelProps = { - cx?: number; - cy?: number; - midAngle?: number; - innerRadius?: number; - outerRadius?: number; - percent?: number; -}; - -const PRIMARY = "#4d4ef3"; -const SECONDARY = "#9fb6ff"; -const LIGHT = "#e8eefb"; -const GRID = "#8f96aa"; - -function renderOutsidePieLabel({ - cx = 0, - cy = 0, - midAngle = 0, - outerRadius = 0, - percent = 0, -}: PieLabelProps) { - const radius = outerRadius + 18; - const x = cx + radius * Math.cos((-midAngle * Math.PI) / 180); - const y = cy + radius * Math.sin((-midAngle * Math.PI) / 180); - - return ( - cx ? "start" : "end"} - dominantBaseline="central" - fontSize="10" - fontWeight="500" - > - {(percent * 100).toFixed(1)}% - - ); -} - -export function CompactBarChart({ - data, -}: { - data: SimpleBarDatum[]; -}) { - return ( - - - - - - - - - - - ); -} - -export function WorkflowBarChart({ - data, -}: { - data: SimpleBarDatum[]; -}) { - return ( - - - - - - - - - - - ); -} - -export function TrendLineChart({ - data, -}: { - data: DualSeriesDatum[]; -}) { - return ( - - - - - - - - - - ); -} - -export function DualLineChart({ - data, -}: { - data: DualSeriesDatum[]; -}) { - return ( - - - - - - - - - - ); -} - -export function AreaTrendChart({ - data, - idPrefix, -}: { - data: SimpleBarDatum[]; - idPrefix: string; -}) { - return ( - - - - - - - - - - - - - - - ); -} - -export function SemiDonutChart({ - data, -}: { - data: PieDatum[]; -}) { - return ( - - - - {data.map((entry, index) => ( - - ))} - - - - ); -} - -export function CompactPieChart({ - data, -}: { - data: PieDatum[]; -}) { - return ( - - - - {data.map((entry, index) => ( - - ))} - - - - ); -} diff --git a/electron/servers/nextjs/app/presentation-templates/Report/flexibleReportChart.tsx b/electron/servers/nextjs/app/presentation-templates/Report/flexibleReportChart.tsx new file mode 100644 index 00000000..75be9b1c --- /dev/null +++ b/electron/servers/nextjs/app/presentation-templates/Report/flexibleReportChart.tsx @@ -0,0 +1,834 @@ +"use client"; + +import { useId } from "react"; +import * as z from "zod"; +import { + Area, + AreaChart, + Bar, + BarChart, + CartesianGrid, + Cell, + Line, + LineChart, + Pie, + PieChart, + ReferenceLine, + Scatter, + ScatterChart, + XAxis, + YAxis, + LabelList, + ResponsiveContainer, +} from "recharts"; + +export const simpleDataSchema = z.object({ + name: z.string().meta({ description: "Data point name" }), + value: z.number().meta({ description: "Data point value" }), +}); + +export const multiSeriesDataSchema = z.object({ + name: z.string().meta({ description: "Category name" }), + values: z.any().meta({ + description: + "Key-value pairs for each series (object with series names as keys and numbers as values)", + }), +}); + +export const divergingDataSchema = z.object({ + name: z.string().meta({ description: "Category name" }), + positive: z.number().meta({ description: "Positive value" }), + negative: z.number().meta({ description: "Negative value" }), +}); + +export const scatterDataSchema = z.object({ + x: z.number().meta({ description: "X coordinate" }), + y: z.number().meta({ description: "Y coordinate" }), +}); + +/** Two series over categorical labels (line stats slide). */ +export const dualLinePointSchema = z.object({ + label: z.string().meta({ description: "Chart axis label" }), + valueA: z.number().meta({ description: "First series value" }), + valueB: z.number().meta({ description: "Second series value" }), +}); +export const SimpleDataPointSchema = z.object({ + name: z.string(), + value: z.number(), +}); + +export const MultiSeriesDataPointSchema = z.object({ + name: z.string(), + values: z.any(), +}); + +export const DivergingDataPointSchema = z.object({ + name: z.string(), + positive: z.number(), + negative: z.number(), +}); + +export const ScatterDataPointSchema = z.object({ + x: z.number(), + y: z.number(), + name: z.string().optional(), +}); + + +export const flexibleChartTypeSchema = z.enum([ + "bar", + "bar-horizontal", + "bar-grouped-vertical", + "bar-grouped-horizontal", + "bar-stacked-vertical", + "bar-stacked-horizontal", + "bar-clustered", + "bar-diverging", + "line", + "line-dual", + "area", + "area-stacked", + "pie", + "donut", + "scatter", +]); + +export const flexibleChartDataSchema = z.object({ + type: flexibleChartTypeSchema.default("bar"), + data: z.union([ + z.array(simpleDataSchema), + z.array(multiSeriesDataSchema), + z.array(divergingDataSchema), + z.array(scatterDataSchema), + z.array(dualLinePointSchema), + ]), + series: z.array(z.string()).optional().meta({ description: "Series names for grouped/stacked charts" }), + divergingLabels: z.tuple([z.string(), z.string()]).optional(), +}); + +export type FlexibleChartData = z.infer; + +const formatComma = (value: number) => value.toLocaleString("en-US"); + +export function deriveSeriesNames(data: any[], explicit: string[]): string[] { + if (explicit.length > 0) return explicit; + const first = data[0]; + if (!first) return []; + if (first.values != null && typeof first.values === "object" && !Array.isArray(first.values)) { + return Object.keys(first.values); + } + if (typeof first.value === "number") return ["value"]; + return []; +} + +export function transformMultiSeriesData(data: any[], series: string[]) { + return data.map((item) => { + const result: Record = { name: item.name }; + series.forEach((s) => { + if (item.values != null && typeof item.values === "object" && s in item.values) { + result[s] = Number(item.values[s]) || 0; + } else if (s === "value" && typeof item.value === "number") { + result[s] = item.value; + } else if (typeof item[s] === "number") { + result[s] = item[s]; + } else { + result[s] = Number(item.values?.[s]) || 0; + } + }); + return result; + }); +} + +export function transformDivergingData(data: any[]) { + return data.map((item) => { + if (typeof item.positive === "number" && typeof item.negative === "number") { + return { + name: item.name, + positive: item.positive, + negative: -Math.abs(item.negative), + }; + } + const v = Number(item.value); + if (!Number.isNaN(v)) { + return { + name: item.name, + positive: Math.max(0, v), + negative: v < 0 ? v : 0, + }; + } + return { name: item.name, positive: 0, negative: 0 }; + }); +} + +export function normalizeScatterPoints(data: any[]) { + return data.map((item, i) => { + if (typeof item.x === "number" && typeof item.y === "number") { + return { ...item, x: item.x, y: item.y }; + } + if (typeof item.value === "number") { + return { ...item, x: typeof item.x === "number" ? item.x : i + 1, y: item.value }; + } + return { ...item, x: i + 1, y: 0 }; + }); +} + +/** Line-stats style rows: categorical `label` + two metrics (not a single `value` series). */ +function dataIsDualLineShape(data: any[]): boolean { + const row = data[0]; + return ( + !!row && + typeof row === "object" && + typeof row.label === "string" && + typeof row.valueA === "number" && + typeof row.valueB === "number" && + typeof row.value !== "number" + ); +} + +const MULTI_SERIES_CHART_TYPES: FlexibleChartData["type"][] = [ + "bar-grouped-vertical", + "bar-grouped-horizontal", + "bar-stacked-vertical", + "bar-stacked-horizontal", + "bar-clustered", + "area-stacked", +]; + +/** + * Aligns `data`/`series` with `chartType`. Line-stats slides often keep `{ label, valueA, valueB }` + * while bar/line/pie/etc. expect `name`/`value` or `values` + series keys. + */ +export function normalizeFlexibleChartData( + chartType: FlexibleChartData["type"], + data: any[], + seriesIn: string[], +): { data: any[]; series: string[] } { + const series = seriesIn ?? []; + const rows = data ?? []; + + if (chartType === "line-dual") { + if (dataIsDualLineShape(rows)) return { data: rows, series }; + return { + data: rows.map((r, i) => ({ + label: r.label ?? r.name ?? `P${i + 1}`, + valueA: typeof r.valueA === "number" ? r.valueA : typeof r.value === "number" ? r.value : 0, + valueB: typeof r.valueB === "number" ? r.valueB : typeof r.value === "number" ? r.value : 0, + })), + series, + }; + } + + if (!dataIsDualLineShape(rows)) { + return { data: rows, series }; + } + + const dual = rows as Array<{ label: string; valueA: number; valueB: number }>; + + if (MULTI_SERIES_CHART_TYPES.includes(chartType)) { + const keys = series.length >= 2 ? [series[0], series[1]] : ["A", "B"]; + const mapped = dual.map((r) => ({ + name: r.label, + values: { [keys[0]]: r.valueA, [keys[1]]: r.valueB }, + })); + return { data: mapped, series: keys }; + } + + if (chartType === "bar-diverging") { + const mapped = dual.map((r) => ({ + name: r.label, + positive: Math.max(0, r.valueA), + negative: Math.max(0, r.valueB), + })); + return { data: mapped, series }; + } + + const mapped = dual.map((r) => ({ + name: r.label, + value: r.valueA + r.valueB, + })); + return { data: mapped, series }; +} + +const graphVar = (index: number, fallback: string) => `var(--graph-${index % 8}, ${fallback})`; + +export type ChartDensity = "default" | "compact"; + +export type FlexibleReportChartProps = { + chartType: FlexibleChartData["type"]; + data: any[]; + series?: string[]; + colorFallback?: string; + /** For `line-dual` only */ + dualLineColors?: [string, string]; + /** Smaller type, margins, and labels for multi-chart dashboards */ + density?: ChartDensity; +}; + +export function FlexibleReportChart({ + chartType, + data: chartData, + series = [], + colorFallback = "#157CFF", + dualLineColors = ["#9fb6ff", "#4d4ef3"], + density = "default", +}: FlexibleReportChartProps) { + const areaGradientId = `flex-area-${useId().replace(/:/g, "")}`; + const compact = density === "compact"; + + const { data: normalizedData, series: normalizedSeries } = normalizeFlexibleChartData( + chartType, + chartData, + series, + ); + const effectiveSeries = deriveSeriesNames(normalizedData as any[], normalizedSeries); + const scatterPoints = normalizeScatterPoints(normalizedData as any[]); + + const ui = { + tickFs: compact ? 6 : 10, + catAxisW: compact ? 36 : 60, + barSize: compact ? 12 : 35, + labelFs: compact ? 8 : 14, + labelOffTop: compact ? 2 : 10, + labelOffSide: compact ? 2 : 8, + margin: compact + ? { top: 10, right: 15, left: -2, bottom: 0 } + : { top: 15, right: 20, left: 0, bottom: 5 }, + lineStroke: compact ? 2 : 3, + dotR: compact ? 2.5 : 4, + dotStroke: compact ? 1 : 2, + barRadiusLg: compact ? ([4, 4, 0, 0] as const) : ([8, 8, 0, 0] as const), + barRadiusMd: compact ? ([2, 2, 0, 0] as const) : ([4, 4, 0, 0] as const), + barRadiusH: compact ? ([0, 4, 4, 0] as const) : ([0, 6, 6, 0] as const), + pieOuter: compact ? "100%" : "100%", + donutOuter: compact ? "90%" : "100%", + donutInner: compact ? "60%" : "70%", + pieMargin: compact + ? { top: 0, right: 0, left: 0, bottom: 0 } + : { top: 8, right: 8, left: 8, bottom: 8 }, + pieLabelMinPct: compact ? 0.12 : 0.06, + pieMaxNameLen: compact ? 6 : 10, + }; + + const axisProps = { + tick: { fill: "var(--background-text, #232223)", fontSize: ui.tickFs, fontWeight: 500 }, + axisLine: { stroke: "var(--background-text, #232223)" }, + tickLine: { stroke: "var(--background-text, #232223)" }, + }; + + const gridProps = { + strokeDasharray: "3 3", + stroke: "var(--background-text, #232223)", + opacity: 0.7, + }; + + const renderPieInsideLabel = (props: any) => { + const { + cx = 0, + cy = 0, + midAngle = 0, + innerRadius: ir = 0, + outerRadius: or = 0, + percent = 0, + name, + } = props; + if (percent < ui.pieLabelMinPct) return null; + const toNum = (v: unknown) => { + if (typeof v === "number" && Number.isFinite(v)) return v; + if (typeof v === "string" && v.trim().endsWith("%")) return NaN; + const n = Number(v); + return Number.isFinite(n) ? n : NaN; + }; + let inner = toNum(ir); + let outer = toNum(or); + if (!Number.isFinite(outer)) { + outer = compact ? 56 : 140; + inner = Number.isFinite(inner) ? inner : 0; + } + if (!Number.isFinite(inner)) inner = 0; + const midR = inner + (outer - inner) * 0.5; + const rad = (-midAngle * Math.PI) / 180; + const x = cx + midR * Math.cos(rad); + const y = cy + midR * Math.sin(rad); + const nm = String(name ?? ""); + const short = nm.length <= ui.pieMaxNameLen; + const pct = `${(percent * 100).toFixed(0)}%`; + const fontSize = compact ? (short ? 6 : 5) : short ? 10 : 9; + const labelText = compact ? pct : short ? `${name} ${pct}` : pct; + return ( + + {labelText} + + ); + }; + + const commonProps = { + margin: ui.margin, + + }; + + switch (chartType) { + case "bar": + return ( + + + + + + + + + + + + + ); + + case "bar-horizontal": + return ( + + + + + + + + + + + + ); + + case "bar-grouped-vertical": { + const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries); + return ( + + + + + + + {effectiveSeries.map((s: string, index: number) => ( + + + + ))} + + + ); + } + + case "bar-grouped-horizontal": { + const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries); + return ( + + + + + + + {effectiveSeries.map((s: string, index: number) => ( + + + + ))} + + + ); + } + + case "bar-stacked-vertical": { + const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries); + return ( + + + + + + + {effectiveSeries.map((s: string, index: number) => ( + + + + ))} + + + ); + } + + case "bar-stacked-horizontal": { + const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries); + return ( + + + + + + + {effectiveSeries.map((s: string, index: number) => ( + + + + ))} + + + ); + } + + case "bar-clustered": { + const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries); + return ( + + + + + + + {effectiveSeries.map((s: string, index: number) => ( + + + + ))} + + + ); + } + + case "bar-diverging": { + const transformedData = transformDivergingData(normalizedData as any[]); + return ( + + + + + + + + + + + + + + + ); + } + + case "line": + return ( + + + + + + + + + + ); + + case "line-dual": + return ( + + + + + + + + + + ); + + case "area": + return ( + + + + + + + + + + + + + + + + + ); + + case "area-stacked": { + const transformedData = transformMultiSeriesData(normalizedData as any[], effectiveSeries); + return ( + + + + + + + {effectiveSeries.map((s: string, index: number) => ( + + ))} + + + ); + } + + case "pie": + return ( + + + + + {(normalizedData as any[]).map((_, index) => ( + + ))} + + + + ); + + case "donut": + return ( + + + + + {(normalizedData as any[]).map((_, index) => ( + + ))} + + + + ); + + case "scatter": + return ( + + + + + + + + {scatterPoints.map((_, index) => ( + + ))} + + + + ); + + default: + return ( +
Unsupported chart type
+ ); + } +} diff --git a/electron/servers/nextjs/app/presentation-templates/index.tsx b/electron/servers/nextjs/app/presentation-templates/index.tsx index e35e7e0c..d2783b16 100644 --- a/electron/servers/nextjs/app/presentation-templates/index.tsx +++ b/electron/servers/nextjs/app/presentation-templates/index.tsx @@ -22,7 +22,7 @@ import EducationTableOfContentsSlide, { Schema as EduTocSchema, slideLayoutId as import EducationAboutSlide, { Schema as EduAboutSchema, slideLayoutId as EduAboutId, slideLayoutName as EduAboutName, slideLayoutDescription as EduAboutDesc } from "./Education/EducationAboutSlide"; import EducationContentSplitSlide, { Schema as EduContentSplitSchema, slideLayoutId as EduContentSplitId, slideLayoutName as EduContentSplitName, slideLayoutDescription as EduContentSplitDesc } from "./Education/EducationContentSplitSlide"; import EducationImageGallerySlide, { Schema as EduImageGallerySchema, slideLayoutId as EduImageGalleryId, slideLayoutName as EduImageGalleryName, slideLayoutDescription as EduImageGalleryDesc } from "./Education/EducationImageGallerySlide"; -import EducationReportDonutSlide, { Schema as EduReportDonutSchema, slideLayoutId as EduReportDonutId, slideLayoutName as EduReportDonutName, slideLayoutDescription as EduReportDonutDesc } from "./Education/EducationReportDonutSlide"; +import EducationReportDonutSlide, { Schema as EduReportDonutSchema, slideLayoutId as EduReportDonutId, slideLayoutName as EduReportDonutName, slideLayoutDescription as EduReportDonutDesc } from "./Education/EducationReportChartSlide"; import EducationServicesSplitSlide, { Schema as EduServicesSplitSchema, slideLayoutId as EduServicesSplitId, slideLayoutName as EduServicesSplitName, slideLayoutDescription as EduServicesSplitDesc } from "./Education/EducationServicesSplitSlide"; import EducationStatisticsGridSlide, { Schema as EduStatisticsGridSchema, slideLayoutId as EduStatisticsGridId, slideLayoutName as EduStatisticsGridName, slideLayoutDescription as EduStatisticsGridDesc } from "./Education/EducationStatisticsGridSlide"; import EducationTimelineSlide, { Schema as EduTimelineSchema, slideLayoutId as EduTimelineId, slideLayoutName as EduTimelineName, slideLayoutDescription as EduTimelineDesc } from "./Education/EducationTimelineSlide";