import { z } from 'zod'; import { SLIDE_CONTENT_TYPE, SLIDE_ICON_SLOT_STATUS, SLIDE_IMAGE_SLOT_STATUS, } from '../../constants'; export enum SLIDE_CHART_TYPE { BAR = 'BAR', } export const ImageSlotSchema = z.object({ uuid: z .string() .uuid() .describe('Generate a valid uuid for image slot, that will be used for future lookups'), prompt: z .string() .describe( 'Image generation prompt. MUST be written in ENGLISH regardless of the presentation language. ' + 'AI image generators excel at: atmospheric scenes, landscapes, cityscapes, architecture, nature, abstract light and texture, anonymous silhouettes, artistic photography. ' + 'AI fundamentally cannot render accurately: legible text on any surface, complex diagrams or flowcharts, data charts/graphs, specific product logos, clocks, precise mechanical parts. ' + 'Choose scene types from the first category. Avoid anything in the second. ' + 'Structure: [subject/scene] → [mood/atmosphere] → [lighting] → [visual style]. ' + 'Evoke the emotional register of the slide — do not illustrate the topic literally. ' + 'MODERATION: if the slide topic involves conflict, suffering, politics, or crime, redirect to abstract/atmospheric imagery (fog, ruins, empty landscapes) rather than depicting the subject directly — flagged prompts produce empty placeholders. ' + 'GOOD: "A lone figure on an observation deck at dusk gazing over an illuminated city. Cinematic wide-angle, golden-hour light, photorealistic." ' + 'BAD: "A bar chart showing quarterly growth. A robot at a laptop with code on the screen."', ), url: z.string().nullable().optional(), status: z.nativeEnum(SLIDE_IMAGE_SLOT_STATUS).optional(), }); export type ImageSlot = z.infer; export const IconSlotSchema = z.object({ uuid: z .string() .uuid() .describe('Generate a valid uuid for icon slot, that will be used for future lookups'), prompt: z .string() .describe( 'Provide a simple ENGLISH icon retrieval query made of 4-8 generic, tag-like keywords for a simple universal pictogram. ' + 'Use broader everyday concept words and close synonyms, not a narrow phrase: e.g. "airport runway plane travel transport", "team people collaboration group office", "shopping store retail bag cart". ' + 'Prefer simple nouns over technical wording, and include 1-3 related tags that describe context or use case. ' + 'Avoid logos, brands, company names, text, mascots, and overly specific visual styles. ' + 'The query should describe the concept to retrieve, not a sentence, and not say "icon" unless needed for disambiguation.', ), url: z.string().nullable().optional(), status: z.nativeEnum(SLIDE_ICON_SLOT_STATUS).optional(), }); export type IconSlot = z.infer; export interface ICoverSlideDataStructure { contentType: SLIDE_CONTENT_TYPE.COVER; title: string; author?: { label: string; value: string }; date?: { label: string; value: string }; email?: { label: string; value: string }; version: 1; } export interface IThankYouSlideDataStructure { contentType: SLIDE_CONTENT_TYPE.THANK_YOU; title: string; author?: { label: string; value: string }; date?: { label: string; value: string }; email?: { label: string; value: string }; version: 1; } export interface ITextSlideDataStructure { contentType: SLIDE_CONTENT_TYPE.TEXT; title: string; description: string; version: 1; } export interface IStructuredListSlideDataStructure { contentType: SLIDE_CONTENT_TYPE.STRUCTURED_LIST; title: string; description: string; list: Array<{ title: string; description: string; iconSlot: IconSlot }>; version: 1; } export interface IContentsSlideDataStructure { contentType: SLIDE_CONTENT_TYPE.CONTENTS; title: string; items: Array<{ pageNumber: number; title: string }>; imageSlot: ImageSlot; version: 1; } export interface IImageSlideDataStructure { contentType: SLIDE_CONTENT_TYPE.TEXT_WITH_IMAGE; title: string; description: string; imageSlot?: ImageSlot; version: 1; } export interface ISectionBreakSlideDataStructure { contentType: SLIDE_CONTENT_TYPE.SECTION_BREAK; title: string; description: string; version: 1; } export interface ITableSlideDataStructure { contentType: SLIDE_CONTENT_TYPE.TABLE; title: string; description: string; table: { columnHeaders: string[]; rows: string[][]; hasRowHeaders: boolean; hasSummaryRow: boolean; }; version: 1; } export interface IBarChartSlideDataStructure { type: SLIDE_CHART_TYPE.BAR; categories: string[]; series: Array<{ name: string; data: number[]; type: number }>; yAxisLabel?: string; unit?: string; version: 1; } export interface IChartSlideDataStructure { contentType: SLIDE_CONTENT_TYPE.CHART; title: string; description: string; chart: IBarChartSlideDataStructure; version: 1; } export interface ITimelineSlideDataStructure { contentType: SLIDE_CONTENT_TYPE.TIMELINE; title: string; description: string; timeline: { events: Array<{ date: string; title: string; description: string; }>; }; version: 1; } export const CoverSlideDataSchema = z.object({ contentType: z.literal(SLIDE_CONTENT_TYPE.COVER), title: z.string().describe('Slide title in about 6 words').min(10).max(150), author: z .object({ label: z.string().describe('Literal "Author" in presentation\'s language'), value: z.string().describe('Author value from titlePage if provided'), }) .optional(), date: z .object({ label: z.string().describe('Literal "Date" in presentation\'s language'), value: z .string() .describe( 'Date from titlePage in the "dd month yyyy" format in presentation\'s locale', ), }) .optional(), email: z .object({ label: z.string().describe('Localized contact/email label'), value: z.string().describe('Email value from titlePage if provided'), }) .optional(), version: z.literal(1), }) satisfies z.ZodType; export type CoverSlideData = z.infer; export const ThankYouSlideDataSchema = z.object({ contentType: z.literal(SLIDE_CONTENT_TYPE.THANK_YOU), title: z .string() .describe('"Thank you for your attention" in presentation\'s language') .min(10) .max(150), author: z .object({ label: z.string().describe('Literal "Author" in presentation\'s language'), value: z.string().describe('Author value from titlePage if provided'), }) .optional(), date: z .object({ label: z.string().describe('Literal "Date" in presentation\'s language'), value: z .string() .describe( 'Date from titlePage in the "dd month yyyy" format in presentation\'s locale', ), }) .optional(), email: z .object({ label: z.string().describe('Localized contact/email label'), value: z.string().describe('Email value from titlePage if provided'), }) .optional(), version: z.literal(1), }) satisfies z.ZodType; export type ThankYouSlideData = z.infer; export const StructuredListSlideDataSchema = z.object({ contentType: z.literal(SLIDE_CONTENT_TYPE.STRUCTURED_LIST), title: z .string() .describe( 'Slide headline — 2–4 words naming the group. A noun phrase, NOT a full sentence. Displayed in very large font on the left panel.', ) .min(5) .max(40), description: z .string() .describe( 'One framing sentence answering "What are these 4 things about?". Displayed in small text below the title on the left panel.', ) .min(30) .max(80), list: z .array( z.object({ title: z .string() .describe( '2–4 concrete words. A noun phrase or short verb phrase. All 4 titles MUST be grammatically parallel. Displayed in bold next to a large number (01–04).', ) .min(3) .max(30), description: z .string() .describe( 'ONE complete sentence that expands on the title with a specific fact or benefit. NEVER just restate the title. NEVER cut short mid-word or mid-sentence. All 4 descriptions must be roughly equal in length.', ) .min(50) .max(100), iconSlot: IconSlotSchema.describe( 'Icon descriptor for this list item. Icon must semantically match the item title/description and be a generic UI pictogram query in English.', ), }), ) .length(4), version: z.literal(1), }) satisfies z.ZodType; export type StructuredListSlideData = z.infer; export const TextSlideDataSchema = z.object({ contentType: z.literal(SLIDE_CONTENT_TYPE.TEXT), title: z .string() .describe( 'Slide headline rendered in very large bold font. Keep to 3–7 words (max 70 characters). It should name the topic or thesis — not summarise all content.', ) .min(10) .max(70), description: z .string() .describe( 'Expository prose split into 2–3 paragraphs separated by \\n\\n (double newline). Total length 180–800 characters. ' + 'Use **bold** to highlight 1–3 key terms per paragraph (NOT entire sentences). ' + 'Use *italics* for definitions, counter-points, or light emphasis (1 per paragraph max). ' + 'NO bullet points, NO markdown headers, NO numbered lists. ' + 'Each paragraph covers ONE focused idea: e.g. context → main argument → implication. ' + 'Do NOT start consecutive paragraphs with the same word. Do NOT cut text short mid-sentence.', ) .min(180) .max(800), version: z.literal(1), }) satisfies z.ZodType; export type TextSlideData = z.infer; export const ContentsSlideDataSchema = z.object({ contentType: z.literal(SLIDE_CONTENT_TYPE.CONTENTS), title: z .string() .describe( 'This slide represents table of contents for the presentation and must be named "Contents" in the presentation language', ) .min(10) .max(200), items: z .array( z.object({ pageNumber: z.number().describe('Page number of the slide'), title: z.string().min(1).describe('Title of the slide on said page'), }), ) .min(1) .describe( 'List of slide titles. Must be relevant and generated after all the slides are generated', ), imageSlot: ImageSlotSchema, version: z.literal(1), }) satisfies z.ZodType; export type ContentsSlideData = z.infer; export const ImageSlideDataSchema = z .object({ contentType: z.literal(SLIDE_CONTENT_TYPE.TEXT_WITH_IMAGE), title: z .string() .describe( 'Slide headline rendered in very large bold font on the LEFT column. Keep to 3–7 words (max 70 characters). Aim for under 50 characters to avoid overflow. A noun phrase or short declarative phrase — NOT a full sentence.', ) .min(10) .max(70), description: z .string() .describe( 'Expository prose in 2 paragraphs separated by \\n\\n (double newline). Total length 150–500 characters. ' + 'Use **bold** for 1–2 key terms per paragraph (NOT entire sentences). ' + 'Use *italics* for definitions or emphasis at most once per paragraph. ' + 'NO bullet points, NO headers, NO numbered lists. ' + 'Each paragraph covers ONE focused idea. NEVER reference or describe the image in the text. ' + 'The image provides visual atmosphere — the text must stand alone as complete, readable prose.', ) .min(150) .max(500), imageSlot: ImageSlotSchema, version: z.literal(1), }) .describe( 'Slide with a large title and prose description on the left, and a full-bleed supporting image on the right.', ) satisfies z.ZodType; export type ImageSlideData = z.infer; export const SectionBreakSlideDataSchema = z.object({ contentType: z.literal(SLIDE_CONTENT_TYPE.SECTION_BREAK), title: z .string() .describe( 'The section name — a clean noun phrase of 3–6 words that names the thematic block which follows. ' + 'NEVER prefix with "Раздел:", "Section:", "Part:", "Глава:", or any structural label. ' + 'NEVER include numbers. Just the name itself. ' + 'GOOD: "Технологические достижения". BAD: "Раздел 2: Технологические достижения".', ) .min(5) .max(60), description: z .string() .describe( 'One sentence (up to 100 characters) that briefly hints at the content of the upcoming section. ' + 'Should complement the title without repeating it. No markdown, no labels.', ) .min(10) .max(120), version: z.literal(1), }) satisfies z.ZodType; export type SectionBreakSlideData = z.infer; export const TableSlideDataSchema = z.object({ contentType: z.literal(SLIDE_CONTENT_TYPE.TABLE), title: z .string() .describe( 'Table name — 2–4 words rendered as large bold heading above the table. ' + 'Doubles as both the slide title and the table heading. Keep concise.', ) .min(5) .max(55), description: z .string() .describe( 'One short sentence (max 80 characters) describing what the table shows. ' + 'Rendered in small gray text below the title. No markdown.', ) .min(10) .max(100), table: z.object({ columnHeaders: z .array( z .string() .min(1) .max(20) .describe( 'Column label. Keep SHORT — 1–3 words. Include units here (e.g., "Сумма, ₽"), not in cells. ' + 'When hasRowHeaders is true, the first header should be a single space " " or a short category label.', ), ) .min(2) .max(5) .describe( 'Column header labels. MAXIMUM 5 columns total. Recommended 3–4 data columns. ' + 'More columns will overflow the slide — NEVER exceed 5.', ), rows: z .array( z .array( z .string() .min(1) .max(40) .describe( 'Cell content. NUMERIC tables: numbers only (e.g., "1 200", "85%"). ' + 'TEXTUAL tables: 1–3 words maximum — keywords or short phrases ONLY, NEVER full sentences. ' + 'Row header cells (first cell when hasRowHeaders=true): category label, max 22 characters.', ), ) .describe('One row. Cell count MUST equal columnHeaders length exactly.'), ) .min(2) .max(4) .describe( 'Table rows. Max 4 rows total. ' + 'When hasSummaryRow is true, the LAST row is the summary row — so at most 3 regular rows + 1 summary = 4 total. ' + 'NEVER exceed 4 rows.', ), hasRowHeaders: z .boolean() .describe( 'True when the first cell of each row is a descriptive label or category (pivot table). ' + 'False for plain data tables where all cells are equivalent data.', ), hasSummaryRow: z .boolean() .describe( 'True when the last row contains totals, averages, or aggregated values. ' + 'That summary row must be the last element of the rows array.', ), }), version: z.literal(1), }) satisfies z.ZodType; export type TableSlideData = z.infer; // Charts export const BarChartSlideDataSchema = z.object({ type: z.literal(SLIDE_CHART_TYPE.BAR), categories: z .array(z.string().min(1).max(20)) .min(3) .max(10) .describe( 'Category labels (e.g., months, product names, departments). ' + 'Recommended: 4–8 items. Keep each label SHORT — 1–3 words, max 15 characters. ' + 'Categories work best when naturally ordered: time periods, rankings, or comparable groups.', ), series: z .array( z.object({ name: z .string() .max(40) .describe( 'Series name shown in the chart legend. Do NOT include units here — units belong in the unit field. ' + 'GOOD: "Рост продаж". BAD: "Рост продаж (%)" or "Выручка (₽)".', ), data: z .array(z.number()) .min(3) .describe( 'Numeric values corresponding to categories. ' + 'MUST have EXACTLY the same number of values as categories — mismatch will break the chart. ' + 'Use round numbers or simple approximations. NEVER hallucinate precise figures. ' + 'Values should show meaningful variation — avoid all bars being the same height.', ), type: z .number() .min(0) .max(11) .describe( 'Color index from the theme palette. ALWAYS use 0 (primary blue). Never change this value.', ), }), ) .min(1) .max(1) .describe('Data series. Only ONE series is supported. Never add more than one.'), yAxisLabel: z .string() .max(30) .optional() .describe( 'Y-axis label. Include ONLY when the axis needs a descriptor that is not already covered by the unit field. ' + 'GOOD use: "Количество студентов" (when there is no unit symbol). ' + 'OMIT when unit is set (e.g., "%", "₽") — the unit already contextualises the values.', ), unit: z .string() .max(10) .optional() .describe( 'Unit symbol displayed with each bar value (e.g., "%", "₽", "шт.", "млн"). ' + 'ALWAYS include when values represent percentages, currency, or any measurable quantity with a unit. ' + 'Omit only for dimensionless counts that are self-explanatory (e.g., raw ranking scores without a fixed scale).', ), version: z.literal(1), }) satisfies z.ZodType; export type BarChartSlideData = z.infer; export const ChartSlideDataSchema = z.object({ contentType: z.literal(SLIDE_CONTENT_TYPE.CHART), title: z .string() .describe( 'Chart name — 2–4 words rendered as large bold heading on the LEFT panel of the slide. ' + 'Keep short: the left panel is narrow and the title font is very large.', ) .min(5) .max(50), description: z .string() .describe( '1–2 sentences (max 120 characters) explaining what the chart shows. ' + 'Rendered in small gray text below the title on the LEFT panel. No markdown.', ) .min(10) .max(150), chart: z.discriminatedUnion('type', [BarChartSlideDataSchema]), version: z.literal(1), }) satisfies z.ZodType; export type ChartSlideData = z.infer; export const TimelineSlideDataSchema = z.object({ contentType: z.literal(SLIDE_CONTENT_TYPE.TIMELINE), title: z .string() .describe( 'Timeline name — 2–5 words, rendered as large bold heading above the timeline. ' + 'Names the subject or scope of the chronological sequence.', ) .min(5) .max(65), description: z .string() .describe( 'One sentence (max 100 characters) providing context for the timeline. ' + 'Rendered in small gray text below the title. No markdown.', ) .min(10) .max(120), timeline: z.object({ events: z .array( z.object({ date: z .string() .describe( 'Temporal label displayed inside the arrow/chevron shape at the top of each column. ' + 'MUST be SHORT — max 20 characters — to fit inside the arrow. ' + 'Use a year ("2015"), a range ("2010–2015"), a quarter ("Q1 2023"), ' + 'an approximate period ("Early 2020s", "Середина 2010-х"), ' + 'or a phase label ("Этап 1", "Запуск") for process timelines. ' + 'NEVER fabricate specific dates you are not certain about.', ) .min(2) .max(25), title: z .string() .describe( 'Event name in bold below the arrow. 2–3 words maximum. ' + 'Short and punchy — the column is narrow, long titles wrap to many lines. ' + 'GOOD: "Первый запуск", "Выход на рынок". BAD: "Успешный выход компании на международный рынок".', ) .min(3) .max(35), description: z .string() .describe( 'Brief description below the event title. 1–2 sentences, max 100 characters. ' + 'The column is narrow — text wraps to ~5 lines at this limit. ' + 'Do NOT exceed 100 characters or the description will overflow off the slide.', ) .min(20) .max(110), }), ) .length(5) .describe( 'Exactly 5 chronological events in sequential order (oldest → newest). ' + 'The template renders exactly 5 equal columns — NEVER generate fewer or more.', ), }), version: z.literal(1), }) satisfies z.ZodType; export type TimelineSlideData = z.infer; export const SlideContentSchema = z.discriminatedUnion('contentType', [ CoverSlideDataSchema, StructuredListSlideDataSchema, TextSlideDataSchema, ContentsSlideDataSchema, SectionBreakSlideDataSchema, ImageSlideDataSchema, ThankYouSlideDataSchema, TableSlideDataSchema, ChartSlideDataSchema, TimelineSlideDataSchema, ]); export type SlideContent = z.infer;