diff --git a/src/lib/types/generateTypes.ts b/src/lib/types/generateTypes.ts index 7c64cb97a..7bd1e06c6 100644 --- a/src/lib/types/generateTypes.ts +++ b/src/lib/types/generateTypes.ts @@ -12,6 +12,7 @@ import type { MiddlewareFactoryOptions } from "./middlewareTypes.js"; import type { JsonValue } from "./common.js"; import type { Content, ImageWithAltText } from "./content.js"; import type { TTSOptions, TTSResult } from "./ttsTypes.js"; +import type { PPTOutputOptions, PPTGenerationResult } from "./pptTypes.js"; import type { VideoOutputOptions, VideoGenerationResult, @@ -76,13 +77,19 @@ export type GenerateOptions = { * Output mode - determines the type of content generated * - "text": Standard text generation (default) * - "video": Video generation using models like Veo 3.1 + * - "ppt": PowerPoint presentation generation */ - mode?: "text" | "video"; + mode?: "text" | "video" | "ppt"; /** * Video generation configuration (used when mode is "video") * Requires an input image and text prompt */ video?: VideoOutputOptions; + /** + * PowerPoint generation configuration (used when mode is "ppt") + * Generates slides based on text prompt + */ + ppt?: PPTOutputOptions; }; // CSV processing options @@ -339,6 +346,24 @@ export type GenerateResult = { * ``` */ video?: VideoGenerationResult; + /** + * PowerPoint generation result (present when output.mode is "ppt") + * + * @example + * ```typescript + * const result = await neurolink.generate({ + * input: { text: "Introducing Our New Product" }, + * model: "gemini-pro", + * output: { mode: "ppt", ppt: { pages: 10, theme: "modern" } } + * }); + * + * if (result.ppt) { + * console.log(`Generated ${result.ppt.slides.length} slides`); + * console.log(`Title: ${result.ppt.slides[0].title}`); + * } + * ``` + */ + ppt?: PPTGenerationResult; imageOutput?: { base64: string } | null; // Standard format for image generation // Provider information @@ -478,12 +503,17 @@ export type TextGenerationOptions = { * Output mode - determines the type of content generated * - "text": Standard text generation (default) * - "video": Video generation using models like Veo 3.1 + * - "ppt": PowerPoint presentation generation */ - mode?: "text" | "video"; + mode?: "text" | "video" | "ppt"; /** * Video generation configuration (used when mode is "video") */ video?: VideoOutputOptions; + /** + * PowerPoint generation configuration (used when mode is "ppt") + */ + ppt?: PPTOutputOptions; }; tools?: Record; // Enable MCP tools integration timeout?: number | string; // Optional timeout (e.g., 30000, '30s', '2m', '1h') @@ -676,6 +706,8 @@ export type TextGenerationResult = { audio?: TTSResult; /** Video generation result */ video?: VideoGenerationResult; + /** PowerPoint generation result */ + ppt?: PPTGenerationResult; /** Image generation output */ imageOutput?: { base64: string } | null; }; diff --git a/src/lib/types/pptTypes.ts b/src/lib/types/pptTypes.ts new file mode 100644 index 000000000..d2ffab059 --- /dev/null +++ b/src/lib/types/pptTypes.ts @@ -0,0 +1,74 @@ +import type { ImageWithAltText } from "./content.js"; + +type ThemeOption = "modern" | "corporate" | "creative" | "minimal" | "dark"; + +type AudienceOption = "business" | "students" | "technical" | "general"; + +type ToneOption = "professional" | "casual" | "educational" | "persuasive"; + +type OutputFormatOption = "pptx"; + +type AspectRatioOption = "16:9" | "4:3"; + +export type PPTOutputOptions = { + /** Number of slides to generate (required, range: 5-50) */ + pages: number; + /** Output format - only PPTX supported currently (default: "pptx") */ + format?: OutputFormatOption; + /** Presentation theme/style (default: "modern") */ + theme?: ThemeOption; + /** Target audience for content customization */ + audience?: AudienceOption; + /** Presentation tone/style */ + tone?: ToneOption; + /** Whether to generate AI images for slides (default: true) */ + includeImages?: boolean; + /** Custom output file path (default: auto-generated in ./output/) */ + outputPath?: string; + /** Aspect ratio for slides (default: "16:9") */ + aspectRatio?: AspectRatioOption; + /** Path to logo image to include in slides */ + logoPath?: Buffer | string | ImageWithAltText; +}; +/** + * Result type for generated presentation content + * + * Returned in `GenerateResult.ppt` when presentation generation is successful. + * Contains the file path and metadata about the generated presentation. + * + * @example + * ```typescript + * const result = await neurolink.generate({ + * input: { text: "Introducing Our New Product" }, + * provider: "vertex", + * output: { mode: "ppt", ppt: { pages: 10, theme: "modern" } } + * }); + * + * if (result.ppt) { + * console.log(`Presentation saved: ${result.ppt.filePath}`); + * console.log(`Total slides: ${result.ppt.totalSlides}`); + * console.log(`Theme: ${result.ppt.metadata?.theme}`); + * } + * ``` + */ +export type PPTGenerationResult = { + /** Path to the generated PPTX file */ + filePath: string; + /** Total number of slides in the presentation */ + totalSlides: number; + /** Output format (always "pptx" currently) */ + format: OutputFormatOption; + /** Presentation metadata */ + metadata?: { + /** Theme/style used */ + theme?: string; + /** Target audience */ + audience?: string; + /** Presentation tone */ + tone?: string; + /** Model used for image generation */ + imageModel?: string; + /** File size in bytes */ + fileSize?: number; + }; +}; diff --git a/src/lib/utils/errorHandling.ts b/src/lib/utils/errorHandling.ts index 530ca5538..7111529cd 100644 --- a/src/lib/utils/errorHandling.ts +++ b/src/lib/utils/errorHandling.ts @@ -2,7 +2,6 @@ * Robust Error Handling Utilities for NeuroLink * Provides structured error management for tool execution and system operations */ - import { ErrorCategory, ErrorSeverity } from "../constants/enums.js"; import type { StructuredError } from "../types/utilities.js"; import { logger } from "./logger.js"; @@ -49,6 +48,17 @@ export const ERROR_CODES = { IMAGE_TOO_LARGE: "IMAGE_TOO_LARGE", IMAGE_TOO_SMALL: "IMAGE_TOO_SMALL", INVALID_IMAGE_FORMAT: "INVALID_IMAGE_FORMAT", + + // PPT validation errors + MISSING_PPT_PROPERTIES: "MISSING_PPT_PROPERTIES", + INVALID_PPT_PAGES: "INVALID_PPT_PAGES", + INVALID_PPT_FORMAT: "INVALID_PPT_FORMAT", + INVALID_PPT_PROVIDER: "INVALID_PPT_PROVIDER", + INVALID_PPT_OUTPUT_OPTIONS: "INVALID_PPT_OUTPUT_OPTIONS", + INVALID_PPT_OUTPUT_PATH: "INVALID_PPT_OUTPUT_PATH", + INVALID_PPT_LOGO_PATH: "INVALID_PPT_LOGO_PATH", + INVALID_PPT_MODE: "INVALID_PPT_MODE", + INVALID_PPT_PROMPT: "INVALID_PPT_PROMPT", } as const; /** @@ -537,6 +547,206 @@ export class ErrorFactory { }, }); } + + // ============================================================================ + // PPT VALIDATION ERRORS + // ============================================================================ + + /** + * Create a generic missing PPT property error + */ + static missingPPTProperty( + field: string, + suggestions?: string[], + ): NeuroLinkError { + const defaultSuggestions = [`Provide the required '${field}' field`]; + + return new NeuroLinkError({ + code: ERROR_CODES.MISSING_PPT_PROPERTIES, + message: `PPT generation requires '${field}' field`, + category: ErrorCategory.VALIDATION, + severity: ErrorSeverity.MEDIUM, + retriable: false, + context: { + field, + suggestions: suggestions || defaultSuggestions, + }, + }); + } + + /** + * Create an invalid PPT pages error + */ + static invalidPPTPages(pages: unknown, reason: string): NeuroLinkError { + return new NeuroLinkError({ + code: ERROR_CODES.INVALID_PPT_PAGES, + message: `Invalid pages value '${pages}': ${reason}`, + category: ErrorCategory.VALIDATION, + severity: ErrorSeverity.MEDIUM, + retriable: false, + context: { + field: "output.ppt.pages", + providedValue: pages, + suggestions: [ + "Use a number between 5 and 50", + "For longer presentations, consider breaking into multiple decks", + ], + }, + }); + } + + /** + * Create an invalid PPT format error + */ + static invalidPPTFormat(format: string): NeuroLinkError { + return new NeuroLinkError({ + code: ERROR_CODES.INVALID_PPT_FORMAT, + message: `Invalid format '${format}'. Only 'pptx' is supported`, + category: ErrorCategory.VALIDATION, + severity: ErrorSeverity.MEDIUM, + retriable: false, + context: { + field: "output.ppt.format", + providedValue: format, + suggestions: ["Use format: 'pptx' or omit (defaults to 'pptx')"], + }, + }); + } + + /** + * Create a generic invalid PPT output options error + */ + static invalidPPTOutputOptions( + field: string, + value: unknown, + validOptions?: string[], + ): NeuroLinkError { + const suggestions = validOptions + ? validOptions.map((opt) => `Use '${opt}'`) + : ["Check the documentation for valid options"]; + + return new NeuroLinkError({ + code: ERROR_CODES.INVALID_PPT_OUTPUT_OPTIONS, + message: `Invalid ${field} value '${value}'`, + category: ErrorCategory.VALIDATION, + severity: ErrorSeverity.MEDIUM, + retriable: false, + context: { + field: `output.ppt.${field}`, + providedValue: value, + suggestions, + }, + }); + } + + /** + * Create an invalid PPT output path error + */ + static invalidPPTOutputPath(path: unknown, reason: string): NeuroLinkError { + return new NeuroLinkError({ + code: ERROR_CODES.INVALID_PPT_OUTPUT_PATH, + message: `Invalid outputPath '${path}': ${reason}`, + category: ErrorCategory.VALIDATION, + severity: ErrorSeverity.MEDIUM, + retriable: false, + context: { + field: "output.ppt.outputPath", + providedValue: path, + suggestions: [ + "Provide a valid file path string", + "Example: './presentations/my-deck.pptx'", + "Omit to use auto-generated path", + ], + }, + }); + } + + /** + * Create an invalid PPT mode error + */ + static invalidPPTMode(): NeuroLinkError { + return new NeuroLinkError({ + code: ERROR_CODES.INVALID_PPT_MODE, + message: "Presentation generation requires output.mode to be 'ppt'", + category: ErrorCategory.VALIDATION, + severity: ErrorSeverity.MEDIUM, + retriable: false, + context: { + field: "output.mode", + suggestions: [ + "Set output: { mode: 'ppt' } for presentation generation", + ], + }, + }); + } + + /** + * Create an invalid PPT prompt error + */ + static invalidPPTPrompt(reason: string): NeuroLinkError { + return new NeuroLinkError({ + code: ERROR_CODES.INVALID_PPT_PROMPT, + message: `Invalid PPT prompt: ${reason}`, + category: ErrorCategory.VALIDATION, + severity: ErrorSeverity.MEDIUM, + retriable: false, + context: { + field: "input.text", + suggestions: [ + "Provide a non-empty text prompt", + "Keep the prompt under 1000 characters", + "Focus on key topics and structure for the presentation", + ], + }, + }); + } + + /** + * Create an invalid PPT logo path error + */ + static invalidPPTLogoPath(path: unknown, reason: string): NeuroLinkError { + return new NeuroLinkError({ + code: ERROR_CODES.INVALID_PPT_LOGO_PATH, + message: `Invalid logoPath '${path}': ${reason}`, + category: ErrorCategory.VALIDATION, + severity: ErrorSeverity.MEDIUM, + retriable: false, + context: { + field: "output.ppt.logoPath", + providedValue: path, + suggestions: [ + "Provide a valid file path string", + "Example: './assets/logo.png'", + "Omit to skip logo inclusion", + ], + }, + }); + } + + /** + * Create an invalid PPT provider error + */ + static invalidPPTProvider(provider: unknown): NeuroLinkError { + return new NeuroLinkError({ + code: ERROR_CODES.INVALID_PPT_PROVIDER, + message: `Invalid provider '${provider}' for PPT generation. Supported providers: vertex, openai, azure, anthropic, google-ai, bedrock`, + category: ErrorCategory.VALIDATION, + severity: ErrorSeverity.MEDIUM, + retriable: false, + context: { + field: "provider", + providedValue: provider, + suggestions: [ + "Use 'vertex' for Google Vertex AI (Gemini)", + "Use 'openai' for OpenAI GPT models", + "Use 'azure' for Azure OpenAI", + "Use 'anthropic' for Anthropic Claude models", + "Use 'google-ai' for Google AI Studio (Gemini)", + "Use 'bedrock' for AWS Bedrock (Claude, Llama, Nova, etc.)", + ], + }, + }); + } } /** diff --git a/src/lib/utils/parameterValidation.ts b/src/lib/utils/parameterValidation.ts index 73f168fd9..5e0aadf83 100644 --- a/src/lib/utils/parameterValidation.ts +++ b/src/lib/utils/parameterValidation.ts @@ -3,6 +3,7 @@ * Provides consistent parameter validation across all tool interfaces */ +import type { AIProviderName } from "../constants/enums.js"; import { SYSTEM_LIMITS } from "../core/constants.js"; import type { GenerateOptions, @@ -10,6 +11,7 @@ import type { } from "../types/generateTypes.js"; import type { NeuroLinkMCPTool } from "../types/mcpTypes.js"; import type { VideoOutputOptions } from "../types/multimodal.js"; +import type { PPTOutputOptions } from "../types/pptTypes.js"; import type { StreamOptions } from "../types/streamTypes.js"; import type { EnhancedValidationResult } from "../types/tools.js"; import type { @@ -966,6 +968,340 @@ export function validateVideoGenerationInput( return { isValid: errors.length === 0, errors, warnings, suggestions }; } +// ============================================================================ +// PPT VALIDATION (Presentation Generation) +// ============================================================================ + +/** + * Valid PPT generation options + */ +const VALID_PPT_THEMES = [ + "modern", + "corporate", + "creative", + "minimal", + "dark", +] as const; +const VALID_PPT_AUDIENCES = [ + "business", + "students", + "technical", + "general", +] as const; +const VALID_PPT_TONES = [ + "professional", + "casual", + "educational", + "persuasive", +] as const; +const VALID_PPT_ASPECT_RATIOS = ["16:9", "4:3"] as const; +const VALID_PPT_FORMATS = ["pptx"] as const; +export const MIN_PPT_PAGES = 5; +export const MAX_PPT_PAGES = 50; +export const MIN_PPT_PROMPT_LENGTH = 10; +export const MAX_PPT_PROMPT_LENGTH = 1000; + +/** + * Validate PPT output options (pages, theme, audience, tone, etc.) + * + * @param options - PPTOutputOptions to validate + * @returns NeuroLinkError if invalid, null if valid + * + * @example + * ```typescript + * const error = validatePPTOutputOptions({ pages: 100, theme: "invalid" }); + * // error.code === "INVALID_PPT_PAGES" + * ``` + */ +export function validatePPTOutputOptions( + options: PPTOutputOptions, +): NeuroLinkError | null { + // Validate pages (slide count) - REQUIRED FIELD + if (options.pages === undefined || options.pages === null) { + return ErrorFactory.missingPPTProperty("output.ppt.pages", [ + "Provide the number of slides: output.ppt.pages = 10", + "Valid range: 5 to 50 slides", + "Recommended: 10 slides for most presentations", + ]); + } + + if (typeof options.pages !== "number") { + return ErrorFactory.invalidPPTPages(options.pages, "not a number"); + } + + if (!Number.isInteger(options.pages)) { + return ErrorFactory.invalidPPTPages(options.pages, "not an integer"); + } + + if (options.pages < MIN_PPT_PAGES || options.pages > MAX_PPT_PAGES) { + return ErrorFactory.invalidPPTPages( + options.pages, + `out of range (${MIN_PPT_PAGES}-${MAX_PPT_PAGES})`, + ); + } + + // Validate format + if ( + options.format !== undefined && + !VALID_PPT_FORMATS.includes(options.format) + ) { + return ErrorFactory.invalidPPTFormat(options.format); + } + + // Validate theme + if ( + options.theme !== undefined && + !VALID_PPT_THEMES.includes(options.theme) + ) { + return ErrorFactory.invalidPPTOutputOptions( + "theme", + options.theme, + Array.from(VALID_PPT_THEMES), + ); + } + + // Validate audience + if ( + options.audience !== undefined && + !VALID_PPT_AUDIENCES.includes(options.audience) + ) { + return ErrorFactory.invalidPPTOutputOptions( + "audience", + options.audience, + Array.from(VALID_PPT_AUDIENCES), + ); + } + + // Validate tone + if (options.tone !== undefined && !VALID_PPT_TONES.includes(options.tone)) { + return ErrorFactory.invalidPPTOutputOptions( + "tone", + options.tone, + Array.from(VALID_PPT_TONES), + ); + } + + // Validate aspectRatio + if ( + options.aspectRatio !== undefined && + !VALID_PPT_ASPECT_RATIOS.includes(options.aspectRatio) + ) { + return ErrorFactory.invalidPPTOutputOptions( + "aspectRatio", + options.aspectRatio, + Array.from(VALID_PPT_ASPECT_RATIOS), + ); + } + + // Validate includeImages (must be boolean if provided) + if ( + options.includeImages !== undefined && + typeof options.includeImages !== "boolean" + ) { + return ErrorFactory.invalidPPTOutputOptions( + "includeImages", + options.includeImages, + ["true", "false"], + ); + } + + // Validate logoPath (string path, Buffer, or ImageWithAltText) + if (options.logoPath !== undefined) { + if (typeof options.logoPath === "string") { + if (options.logoPath.trim().length === 0) { + return ErrorFactory.invalidPPTLogoPath( + options.logoPath, + "empty string", + ); + } + } else if (Buffer.isBuffer(options.logoPath)) { + // ok + } else if ( + typeof options.logoPath === "object" && + "data" in options.logoPath + ) { + const data = (options.logoPath as { data: unknown }).data; + if (typeof data === "string") { + if (data.trim().length === 0) { + return ErrorFactory.invalidPPTLogoPath( + options.logoPath, + "empty string", + ); + } + } else if (!Buffer.isBuffer(data)) { + return ErrorFactory.invalidPPTLogoPath( + options.logoPath, + "invalid data type", + ); + } + } else { + return ErrorFactory.invalidPPTLogoPath(options.logoPath, "invalid type"); + } + } + + // Validate outputPath (must be non-empty string if provided) + if (options.outputPath !== undefined) { + if (typeof options.outputPath !== "string") { + return ErrorFactory.invalidPPTOutputPath( + options.outputPath, + "not a string", + ); + } + if (options.outputPath.trim().length === 0) { + return ErrorFactory.invalidPPTOutputPath( + options.outputPath, + "empty string", + ); + } + } + + return null; +} + +function validatePPTProvider( + provider: AIProviderName | string, +): NeuroLinkError | null { + // PPT generation supported providers (subset of all AIProviderName values) + // Supports major LLM providers with structured output capabilities + const validProviders = [ + "vertex", + "openai", + "azure", + "anthropic", + "google-ai", + "bedrock", + ]; + + // Convert enum or string to lowercase string for comparison + const providerString = String(provider).toLowerCase(); + + if (!validProviders.includes(providerString)) { + return ErrorFactory.invalidPPTProvider(provider); + } + + return null; +} + +/** + * Validate complete PPT generation input + * + * Validates all requirements for presentation generation: + * - output.mode must be "ppt" + * - Prompt must be within length limits + * - PPT output options must be valid + * + * @param options - GenerateOptions to validate for PPT generation + * @returns EnhancedValidationResult with errors, warnings, and suggestions + * + * @example + * ```typescript + * const validation = validatePPTGenerationInput({ + * input: { text: "Introducing Our New Product" }, + * output: { mode: "ppt", ppt: { pages: 10, theme: "modern" } } + * }); + * if (!validation.isValid) { + * console.error(validation.errors); + * } + * ``` + */ +export function validatePPTGenerationInput( + options: GenerateOptions, +): EnhancedValidationResult { + const errors: ValidationError[] = []; + const warnings: string[] = []; + const suggestions: StringArray = []; + + // Validate prompt/text - trim once for consistency + const trimmedPrompt = options.input.text.trim(); + if (trimmedPrompt === "") { + errors.push( + toValidationError(ErrorFactory.invalidPPTPrompt("empty prompt")), + ); + } else if (trimmedPrompt.length < MIN_PPT_PROMPT_LENGTH) { + errors.push( + toValidationError( + ErrorFactory.invalidPPTPrompt( + `prompt too short (${trimmedPrompt.length} characters, min ${MIN_PPT_PROMPT_LENGTH})`, + ), + ), + ); + } else if (trimmedPrompt.length > MAX_PPT_PROMPT_LENGTH) { + errors.push( + toValidationError( + ErrorFactory.invalidPPTPrompt( + `prompt too long (${trimmedPrompt.length} characters, max ${MAX_PPT_PROMPT_LENGTH})`, + ), + ), + ); + } + + // image PPT options if provided + if (options.input.images && options.input.images.length > 0) { + warnings.push( + "Images can be unused in PPT generation due to fail in quality standards and can lead to longer generation times.", + ); + suggestions.push( + "Only provide high-quality, relevant images for PPT generation.", + ); + } + + // Validate provider (optional - only validate if explicitly provided) + if (options.provider !== undefined) { + const providerError = validatePPTProvider(options.provider); + if (providerError) { + errors.push(toValidationError(providerError)); + } + } + + // Mode is optional, but if provided must be "ppt" + if (options.output?.mode !== undefined && options.output.mode !== "ppt") { + errors.push(toValidationError(ErrorFactory.invalidPPTMode())); + } + + // Validate PPT output options + if (options.output?.ppt) { + const pptError = validatePPTOutputOptions(options.output.ppt); + if (pptError) { + errors.push(toValidationError(pptError)); + } + + // Add specific warnings + const pages = options.output.ppt.pages; + if (pages > 30) { + warnings.push( + `Generating ${pages} slides may take significant time (estimated: ${Math.ceil(pages * 3)}-${Math.ceil(pages * 5)} seconds)`, + ); + } + + if ( + options.output.ppt.includeImages === undefined || + options.output.ppt.includeImages === true + ) { + suggestions.push( + "Image generation is enabled. Each slide with images will take additional time (~2-5 seconds per image).", + ); + } + } else { + errors.push( + toValidationError( + ErrorFactory.missingPPTProperty("output.ppt", [ + "Provide PPT generation options under output.ppt", + "Specify number of slides, theme, and other preferences", + ]), + ), + ); + } + + // Add helpful suggestions + if (errors.length === 0 && warnings.length === 0) { + suggestions.push( + "PPT generation typically takes 30-120 seconds depending on slide count and image generation.", + ); + } + + return { isValid: errors.length === 0, errors, warnings, suggestions }; +} + // ============================================================================ // HELPER FUNCTIONS // ============================================================================ diff --git a/test/unit/ppt-generation.test.ts b/test/unit/ppt-generation.test.ts new file mode 100644 index 000000000..e96c886f9 --- /dev/null +++ b/test/unit/ppt-generation.test.ts @@ -0,0 +1,350 @@ +import { describe, it, expect } from "vitest"; +import { + validatePPTOutputOptions, + validatePPTGenerationInput, +} from "../../src/lib/utils/parameterValidation.js"; +import type { GenerateOptions } from "../../src/lib/types/index.js"; +import { PPTOutputOptions } from "../../src/lib/types/pptTypes.js"; +import { AIProviderName } from "../../src/lib/constants/enums.js"; + +// ----------------------------------------------------------------------- +// PPT Gen Validation Tests +// ----------------------------------------------------------------------- + +describe("PPT Validation", () => { + describe("validatePPTOutputOptions", () => { + it("should accept valid PPT options", () => { + const validOptions: PPTOutputOptions = { + pages: 10, + format: "pptx", + theme: "modern", + audience: "business", + tone: "professional", + includeImages: true, + outputPath: "./output/presentation.pptx", + }; + + const error = validatePPTOutputOptions(validOptions); + expect(error).toBeNull(); + }); + + it("should reject missing pages field", () => { + const options = {} as PPTOutputOptions; + const error = validatePPTOutputOptions(options); + + expect(error).not.toBeNull(); + expect(error?.code).toBe("MISSING_PPT_PROPERTIES"); + }); + + it("should accept minimal valid options", () => { + const minimalOptions: PPTOutputOptions = { + pages: 5, + }; + + const error = validatePPTOutputOptions(minimalOptions); + expect(error).toBeNull(); + }); + + it("should reject pages < 1", () => { + const options: PPTOutputOptions = { pages: 0 }; + const error = validatePPTOutputOptions(options); + + expect(error).not.toBeNull(); + expect(error?.code).toBe("INVALID_PPT_PAGES"); + }); + + it("should reject pages > 50", () => { + const options: PPTOutputOptions = { pages: 51 }; + const error = validatePPTOutputOptions(options); + + expect(error).not.toBeNull(); + expect(error?.code).toBe("INVALID_PPT_PAGES"); + }); + + it("should reject non-integer pages", () => { + const options: PPTOutputOptions = { pages: 10.5 }; + const error = validatePPTOutputOptions(options); + + expect(error).not.toBeNull(); + expect(error?.code).toBe("INVALID_PPT_PAGES"); + }); + + it("should reject invalid theme", () => { + const options = { + pages: 10, + theme: "invalid-theme", + } as unknown as PPTOutputOptions; + const error = validatePPTOutputOptions(options); + + expect(error).not.toBeNull(); + expect(error?.code).toBe("INVALID_PPT_OUTPUT_OPTIONS"); + }); + + it("should reject invalid audience", () => { + const options = { + pages: 10, + audience: "invalid-audience", + } as unknown as PPTOutputOptions; + const error = validatePPTOutputOptions(options); + + expect(error).not.toBeNull(); + expect(error?.code).toBe("INVALID_PPT_OUTPUT_OPTIONS"); + }); + + it("should reject invalid tone", () => { + const options = { + pages: 10, + tone: "invalid-tone", + } as unknown as PPTOutputOptions; + const error = validatePPTOutputOptions(options); + + expect(error).not.toBeNull(); + expect(error?.code).toBe("INVALID_PPT_OUTPUT_OPTIONS"); + }); + + it("should reject invalid format", () => { + const options = { + pages: 10, + format: "pdf", + } as unknown as PPTOutputOptions; + const error = validatePPTOutputOptions(options); + + expect(error).not.toBeNull(); + expect(error?.code).toBe("INVALID_PPT_FORMAT"); + }); + + it("should reject non-boolean includeImages", () => { + const options = { + pages: 10, + includeImages: "yes", + } as unknown as PPTOutputOptions; + const error = validatePPTOutputOptions(options); + + expect(error).not.toBeNull(); + expect(error?.code).toBe("INVALID_PPT_OUTPUT_OPTIONS"); + }); + + it("should reject empty outputPath", () => { + const options: PPTOutputOptions = { + pages: 10, + outputPath: " ", + }; + const error = validatePPTOutputOptions(options); + + expect(error).not.toBeNull(); + expect(error?.code).toBe("INVALID_PPT_OUTPUT_PATH"); + }); + + it("should reject invalid aspectRatio", () => { + const options = { + pages: 10, + aspectRatio: "21:9", + } as unknown as PPTOutputOptions; + const error = validatePPTOutputOptions(options); + + expect(error).not.toBeNull(); + expect(error?.code).toBe("INVALID_PPT_OUTPUT_OPTIONS"); + }); + + it("should reject empty logoPath", () => { + const options: PPTOutputOptions = { + pages: 10, + logoPath: "", + }; + const error = validatePPTOutputOptions(options); + + expect(error).not.toBeNull(); + expect(error?.code).toBe("INVALID_PPT_LOGO_PATH"); + }); + + it("should reject non-string logoPath", () => { + const options = { + pages: 10, + logoPath: 123, + } as unknown as PPTOutputOptions; + const error = validatePPTOutputOptions(options); + + expect(error).not.toBeNull(); + expect(error?.code).toBe("INVALID_PPT_LOGO_PATH"); + }); + }); + + describe("validatePPTGenerationInput", () => { + it("should accept valid PPT generation input", () => { + const options: GenerateOptions = { + input: { text: "Introducing Our New Product" }, + output: { + mode: "ppt", + ppt: { + pages: 10, + theme: "modern", + }, + }, + }; + + const result = validatePPTGenerationInput(options); + expect(result.isValid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it("should accept missing mode (mode is optional)", () => { + const options: GenerateOptions = { + input: { text: "Test presentation content" }, + output: { + ppt: { pages: 10 }, + }, + }; + + const result = validatePPTGenerationInput(options); + expect(result.isValid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it("should reject invalid mode (if mode provided, must be ppt)", () => { + const options: GenerateOptions = { + input: { text: "Test presentation content" }, + output: { + mode: "video", + ppt: { pages: 10 }, + }, + }; + + const result = validatePPTGenerationInput(options); + expect(result.isValid).toBe(false); + expect(result.errors.some((e) => e.code === "INVALID_PPT_MODE")).toBe( + true, + ); + }); + + it("should reject empty prompt", () => { + const options: GenerateOptions = { + input: { text: " " }, + output: { + mode: "ppt", + ppt: { pages: 10 }, + }, + }; + + const result = validatePPTGenerationInput(options); + expect(result.isValid).toBe(false); + expect(result.errors.some((e) => e.code === "INVALID_PPT_PROMPT")).toBe( + true, + ); + }); + + it("should reject prompt that is too short", () => { + const options: GenerateOptions = { + input: { text: "Short" }, // Only 5 characters + output: { + mode: "ppt", + ppt: { pages: 10 }, + }, + }; + + const result = validatePPTGenerationInput(options); + expect(result.isValid).toBe(false); + expect(result.errors.some((e) => e.code === "INVALID_PPT_PROMPT")).toBe( + true, + ); + }); + + it("should reject prompt that is too long", () => { + const longPrompt = "a".repeat(1001); + const options: GenerateOptions = { + input: { text: longPrompt }, + output: { + mode: "ppt", + ppt: { pages: 10 }, + }, + }; + + const result = validatePPTGenerationInput(options); + expect(result.isValid).toBe(false); + expect(result.errors.some((e) => e.code === "INVALID_PPT_PROMPT")).toBe( + true, + ); + }); + + it("should add warning for large slide counts", () => { + const options: GenerateOptions = { + input: { text: "Test presentation with many slides" }, + output: { + mode: "ppt", + ppt: { pages: 35 }, + }, + }; + + const result = validatePPTGenerationInput(options); + expect(result.warnings.length).toBeGreaterThan(0); + expect(result.warnings[0]).toContain("may take significant time"); + }); + + it("should provide helpful suggestions for valid input", () => { + const options: GenerateOptions = { + input: { text: "Test presentation for validation" }, + output: { + mode: "ppt", + ppt: { pages: 10 }, + }, + }; + + const result = validatePPTGenerationInput(options); + expect(result.isValid).toBe(true); + expect(result.suggestions.length).toBeGreaterThan(0); + }); + + it("should accept valid AIProviderName enum values", () => { + const validProviders = [ + AIProviderName.VERTEX, + AIProviderName.OPENAI, + AIProviderName.AZURE, + AIProviderName.ANTHROPIC, + AIProviderName.GOOGLE_AI, + AIProviderName.BEDROCK, + ]; + + for (const provider of validProviders) { + const options: GenerateOptions = { + input: { text: "Test presentation" }, + provider: provider, + output: { + mode: "ppt", + ppt: { pages: 10 }, + }, + }; + + const result = validatePPTGenerationInput(options); + expect(result.isValid).toBe(true); + expect(result.errors).toHaveLength(0); + } + }); + + it("should reject unsupported AIProviderName enum values", () => { + const unsupportedProviders = [ + AIProviderName.HUGGINGFACE, + AIProviderName.OLLAMA, + AIProviderName.SAGEMAKER, + AIProviderName.OPENAI_COMPATIBLE, + AIProviderName.OPENROUTER, + AIProviderName.MISTRAL, + AIProviderName.LITELLM, + ]; + + for (const provider of unsupportedProviders) { + const options: GenerateOptions = { + input: { text: "Test presentation" }, + provider: provider, + output: { + mode: "ppt", + ppt: { pages: 10 }, + }, + }; + + const result = validatePPTGenerationInput(options); + expect(result.isValid).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + expect(result.errors[0].code).toBe("INVALID_PPT_PROVIDER"); + } + }); + }); +});