diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx
index d694c594..6030e48f 100644
--- a/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx
+++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx
@@ -1,5 +1,5 @@
-import React, { useEffect, useState, useMemo } from "react";
-import { Loader2, PlusIcon, Trash2, WandSparkles, StickyNote } from "lucide-react";
+import React, { useEffect, useState } from "react";
+import { Loader2, PlusIcon, Trash2, Pencil, Trash } from "lucide-react";
import {
Popover,
PopoverContent,
@@ -32,6 +32,9 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
const dispatch = useDispatch();
const [isUpdating, setIsUpdating] = useState(false);
const [showNewSlideSelection, setShowNewSlideSelection] = useState(false);
+ const [isEditPopoverOpen, setIsEditPopoverOpen] = useState(false);
+ const [isSpeakerPopoverOpen, setIsSpeakerPopoverOpen] = useState(false);
+ const [editPrompt, setEditPrompt] = useState("");
const { presentationData, isStreaming } = useSelector(
(state: RootState) => state.presentationGeneration
);
@@ -41,26 +44,24 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
const pathname = usePathname();
const handleSubmit = async () => {
- const element = document.getElementById(
- `slide-${slide.index}-prompt`
- ) as HTMLInputElement;
- const value = element?.value;
- if (!value?.trim()) {
+ if (!editPrompt.trim()) {
toast.error("Please enter a prompt before submitting");
return;
}
setIsUpdating(true);
try {
+ trackEvent(MixpanelEvent.Slide_Update_From_Prompt_Button_Clicked, { pathname });
trackEvent(MixpanelEvent.Slide_Edit_API_Call);
const response = await PresentationGenerationApi.editSlide(
slide.id,
- value
+ editPrompt
);
if (response) {
dispatch(updateSlide({ index: slide.index, slide: response }));
toast.success("Slide updated successfully");
+ setEditPrompt("");
}
} catch (error: any) {
console.error("Error in slide editing:", error);
@@ -71,8 +72,10 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
setIsUpdating(false);
}
};
+
const onDeleteSlide = async () => {
try {
+ trackEvent(MixpanelEvent.Slide_Delete_Slide_Button_Clicked, { pathname });
trackEvent(MixpanelEvent.Slide_Delete_API_Call);
// Add current state to past
dispatch(addToHistory({
@@ -170,96 +173,116 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
)}
{!isStreaming && (
-
- {
- trackEvent(MixpanelEvent.Slide_Delete_Slide_Button_Clicked, { pathname });
- onDeleteSlide();
- }}
- className="absolute top-2 z-20 sm:top-4 right-2 sm:right-4 hidden md:block transition-transform"
- >
-
-
-
- )}
- {!isStreaming && (
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
+
+
Update slide
+
+ Describe how this slide should be improved.
+
+
{
+ e.preventDefault();
+ handleSubmit();
+ }}
+ >
+ setEditPrompt(e.target.value)}
+ rows={5}
+ wrap="soft"
+ />
+
+ {isUpdating ? "Updating..." : "Update"}
+
+
+
-
- )}
- {/* Speaker Notes */}
- {!isStreaming && slide?.speaker_note && (
-
-
+
+
-
+
-
-
-
Speaker notes
-
- {slide.speaker_note}
+
+
+
+
+ {slide?.speaker_note?.trim() || "No speaker notes for this slide."}
+
+
+
+
+
+
)}
diff --git a/servers/nextjs/app/(presentation-generator)/upload/components/ConfigurationSelects.tsx b/servers/nextjs/app/(presentation-generator)/upload/components/ConfigurationSelects.tsx
new file mode 100644
index 00000000..64a4f768
--- /dev/null
+++ b/servers/nextjs/app/(presentation-generator)/upload/components/ConfigurationSelects.tsx
@@ -0,0 +1,370 @@
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { LanguageType, PresentationConfig, ToneType, VerbosityType } from "../type";
+import { useState } from "react";
+import { Check, ChevronsUpDown, GalleryVertical, Languages, SlidersHorizontal } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { cn } from "@/lib/utils";
+import { Input } from "@/components/ui/input";
+import { Switch } from "@/components/ui/switch";
+import { Textarea } from "@/components/ui/textarea";
+import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import ToolTip from "@/components/ToolTip";
+
+// Types
+interface ConfigurationSelectsProps {
+ config: PresentationConfig;
+ onConfigChange: (key: keyof PresentationConfig, value: any) => void;
+}
+
+type SlideOption = "5" | "8" | "9" | "10" | "11" | "12" | "13" | "14" | "15" | "16" | "17" | "18" | "19" | "20";
+
+// Constants
+const SLIDE_OPTIONS: SlideOption[] = ["5", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20"];
+
+/**
+ * Renders a select component for slide count
+ */
+const SlideCountSelect: React.FC<{
+ value: string | null;
+ onValueChange: (value: string) => void;
+}> = ({ value, onValueChange }) => {
+ const [customInput, setCustomInput] = useState(
+ value && !SLIDE_OPTIONS.includes(value as SlideOption) ? value : ""
+ );
+
+ const sanitizeToPositiveInteger = (raw: string): string => {
+ const digitsOnly = raw.replace(/\D+/g, "");
+ if (!digitsOnly) return "";
+ // Remove leading zeros
+ const noLeadingZeros = digitsOnly.replace(/^0+/, "");
+ return noLeadingZeros;
+ };
+
+ const applyCustomValue = () => {
+ const sanitized = sanitizeToPositiveInteger(customInput);
+ if (sanitized && Number(sanitized) > 0) {
+ onValueChange(sanitized);
+ }
+ };
+
+ return (
+
+
+
+
+
+ {/* Sticky custom input at the top */}
+ e.stopPropagation()}
+ onPointerDown={(e) => e.stopPropagation()}
+ onClick={(e) => e.stopPropagation()}
+ >
+
+ e.stopPropagation()}
+ onPointerDown={(e) => e.stopPropagation()}
+ onClick={(e) => e.stopPropagation()}
+ onChange={(e) => {
+ const next = sanitizeToPositiveInteger(e.target.value);
+ setCustomInput(next);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ applyCustomValue();
+ }
+ }}
+ onBlur={applyCustomValue}
+ placeholder="--"
+ className="h-8 w-16 px-2 text-sm"
+ />
+ slides
+
+
+
+ {/* Hidden item to allow SelectValue to render custom selection */}
+ {value && !SLIDE_OPTIONS.includes(value as SlideOption) && (
+
+ {value} slides
+
+ )}
+
+ {SLIDE_OPTIONS.map((option) => (
+
+ {option} slides
+
+ ))}
+
+
+ );
+};
+
+/**
+ * Renders a language selection component with search functionality
+ */
+const LanguageSelect: React.FC<{
+ value: string | null;
+ onValueChange: (value: string) => void;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}> = ({ value, onValueChange, open, onOpenChange }) => (
+
+
+
+
+
+
+
+
+ {value || "Select language"}
+
+
+
+
+
+
+
+
+
+ No language found.
+
+ {Object.values(LanguageType).map((language) => (
+ {
+ onValueChange(currentValue);
+ onOpenChange(false);
+ }}
+ className="font-instrument_sans"
+ >
+
+ {language}
+
+ ))}
+
+
+
+
+
+);
+
+export function ConfigurationSelects({
+ config,
+ onConfigChange,
+}: ConfigurationSelectsProps) {
+ const [openLanguage, setOpenLanguage] = useState(false);
+ const [openAdvanced, setOpenAdvanced] = useState(false);
+
+ const [advancedDraft, setAdvancedDraft] = useState({
+ tone: config.tone,
+ verbosity: config.verbosity,
+ instructions: config.instructions,
+ includeTableOfContents: config.includeTableOfContents,
+ includeTitleSlide: config.includeTitleSlide,
+ webSearch: config.webSearch,
+ });
+
+ const handleOpenAdvancedChange = (open: boolean) => {
+ if (open) {
+ setAdvancedDraft({
+ tone: config.tone,
+ verbosity: config.verbosity,
+ instructions: config.instructions,
+ includeTableOfContents: config.includeTableOfContents,
+ includeTitleSlide: config.includeTitleSlide,
+ webSearch: config.webSearch,
+ });
+ }
+ setOpenAdvanced(open);
+ };
+
+ const handleSaveAdvanced = () => {
+ onConfigChange("tone", advancedDraft.tone);
+ onConfigChange("verbosity", advancedDraft.verbosity);
+ onConfigChange("instructions", advancedDraft.instructions);
+ onConfigChange("includeTableOfContents", advancedDraft.includeTableOfContents);
+ onConfigChange("includeTitleSlide", advancedDraft.includeTitleSlide);
+ onConfigChange("webSearch", advancedDraft.webSearch);
+ setOpenAdvanced(false);
+ };
+
+ return (
+
+
onConfigChange("slides", value)}
+ />
+ onConfigChange("language", value)}
+ open={openLanguage}
+ onOpenChange={setOpenLanguage}
+ />
+
+
+ handleOpenAdvancedChange(true)}
+ className="ml-auto flex items-center gap-2 text-sm bg-white text-slate-700 hover:bg-slate-50 focus-visible:ring-[#5146E5]/30 h-10 rounded-xl px-3 ring-1 ring-inset ring-slate-200 shadow-sm font-instrument_sans font-medium"
+ data-testid="advanced-settings-button"
+ >
+
+
+
+
+
+
+
+ Advanced settings
+
+
+
+ {/* Tone */}
+
+
Tone
+
Controls the writing style (e.g., casual, professional, funny).
+
setAdvancedDraft((prev) => ({ ...prev, tone: value as ToneType }))}
+ >
+
+
+
+
+ {Object.values(ToneType).map((tone) => (
+
+ {tone}
+
+ ))}
+
+
+
+
+ {/* Verbosity */}
+
+
Verbosity
+
Controls how detailed slide descriptions are: concise, standard, or text-heavy.
+
setAdvancedDraft((prev) => ({ ...prev, verbosity: value as VerbosityType }))}
+ >
+
+
+
+
+ {Object.values(VerbosityType).map((verbosity) => (
+
+ {verbosity}
+
+ ))}
+
+
+
+
+
+
+ {/* Toggles */}
+
+
+ Include table of contents
+ setAdvancedDraft((prev) => ({ ...prev, includeTableOfContents: checked }))}
+ />
+
+
Add an index slide summarizing sections (requires 3+ slides).
+
+
+
+ Title slide
+ setAdvancedDraft((prev) => ({ ...prev, includeTitleSlide: checked }))}
+ />
+
+
Include a title slide as the first slide.
+
+
+
+ Web search
+ setAdvancedDraft((prev) => ({ ...prev, webSearch: checked }))}
+ />
+
+
Allow the model to consult the web for fresher facts.
+
+
+ {/* Instructions */}
+
+
Instructions
+
Optional guidance for the AI. These override defaults except format constraints.
+
setAdvancedDraft((prev) => ({ ...prev, instructions: e.target.value }))}
+ placeholder="Example: Focus on enterprise buyers, emphasize ROI and security compliance. Keep slides data-driven, avoid jargon, and include a short call-to-action on the final slide."
+ className="py-2 px-3 border-2 font-medium text-sm min-h-[100px] max-h-[200px] border-blue-200 focus-visible:ring-offset-0 focus-visible:ring-blue-300"
+ />
+
+
+
+
+ handleOpenAdvancedChange(false)}>Cancel
+ Save
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/servers/nextjs/app/(presentation-generator)/upload/components/PromptInput.tsx b/servers/nextjs/app/(presentation-generator)/upload/components/PromptInput.tsx
index ad28e4ce..5e1baa7f 100644
--- a/servers/nextjs/app/(presentation-generator)/upload/components/PromptInput.tsx
+++ b/servers/nextjs/app/(presentation-generator)/upload/components/PromptInput.tsx
@@ -1,42 +1,32 @@
import { Textarea } from "@/components/ui/textarea";
import { useState } from "react";
-
interface PromptInputProps {
value: string;
onChange: (value: string) => void;
-
}
-export function PromptInput({
- value,
- onChange,
+export function PromptInput({ value, onChange }: PromptInputProps) {
+ const [showHint, setShowHint] = useState(false);
-}: PromptInputProps) {
+ const handleChange = (val: string) => {
+ setShowHint(val.length > 0);
+ onChange(val);
+ };
return (
+
+
+ handleChange(e.target.value)}
+ placeholder="Tell us about your presentation"
+ data-testid="prompt-input"
+ className={`py-4 px-5 border-2 font-medium font-instrument_sans text-base min-h-[150px] max-h-[300px] border-[#5146E5] focus-visible:ring-offset-0 focus-visible:ring-[#5146E5] overflow-y-auto custom_scrollbar `}
+ />
+
-
-
onChange(e.target.value)}
- placeholder="Tell us about your presentation"
- data-testid="prompt-input"
- className={`py-3.5 px-2.5 rounded-[10px] border-none bg-[#F6F6F9] placeholder:text-[#B3B3B3] font-medium font-instrument_sans text-base max-h-[300px] focus-visible:ring-offset-0 focus-visible:ring-0 overflow-y-auto custom_scrollbar `}
- />
-
-
+
);
-}
+}
\ No newline at end of file
diff --git a/servers/nextjs/app/(presentation-generator)/upload/components/SupportingDoc.tsx b/servers/nextjs/app/(presentation-generator)/upload/components/SupportingDoc.tsx
index 970bdaf2..3a7b6826 100644
--- a/servers/nextjs/app/(presentation-generator)/upload/components/SupportingDoc.tsx
+++ b/servers/nextjs/app/(presentation-generator)/upload/components/SupportingDoc.tsx
@@ -1,233 +1,240 @@
'use client'
-import React, { useRef, useState } from 'react'
-import { File, X, Upload, Plus } from 'lucide-react'
+import React, { ChangeEvent, useEffect, useMemo, useState } from 'react'
+import { File, Paperclip, X } from 'lucide-react'
import { toast } from 'sonner'
-import { cn } from '@/lib/utils'
-
-interface FileWithId extends File {
- id: string;
-}
interface SupportingDocProps {
- files: File[];
- onFilesChange: (files: File[]) => void;
+ files: File[]
+ onFilesChange: (files: File[]) => void
+ accept?: string
+ multiple?: boolean
}
-const SupportingDoc = ({ files, onFilesChange }: SupportingDocProps) => {
+const PDF_TYPES = ['.pdf']
+const TEXT_TYPES = ['.txt']
+const POWERPOINT_TYPES = ['.pptx']
+const WORD_TYPES = ['.docx']
+
+const ACCEPT_DEFAULT = [
+ 'application/pdf',
+ 'text/plain',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ ...PDF_TYPES,
+ ...TEXT_TYPES,
+ ...POWERPOINT_TYPES,
+ ...WORD_TYPES,
+].join(',')
+const ALLOWED_MIME_PREFIXES: string[] = []
+const ALLOWED_MIME_TYPES = [
+ 'application/pdf',
+ 'application/x-pdf',
+ 'application/acrobat',
+ 'applications/pdf',
+ 'text/pdf',
+ 'application/vnd.pdf',
+ 'text/plain',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+]
+const ALLOWED_EXTENSIONS = [
+ ...PDF_TYPES,
+ ...TEXT_TYPES,
+ ...POWERPOINT_TYPES,
+ ...WORD_TYPES,
+]
+
+const SupportingDoc = ({
+ files,
+ onFilesChange,
+ accept = ACCEPT_DEFAULT,
+ multiple = true,
+}: SupportingDocProps) => {
const [isDragging, setIsDragging] = useState(false)
- const fileInputRef = useRef(null)
+ const [previewUrls, setPreviewUrls] = useState<(string | null)[]>([])
- // Convert Files to FileWithId with proper type checking
- const filesWithIds: FileWithId[] = files.map(file => {
- const fileWithId = file as FileWithId
- fileWithId.id = `${file.name || 'unnamed'}-${file.lastModified || Date.now()}-${file.size || 0}`
- return fileWithId
- })
+ const hasFiles = files.length > 0
- const formatFileSize = (bytes: number): string => {
- if (!bytes || bytes === 0) return '0 Bytes'
- const k = 1024
- const sizes = ['Bytes', 'KB', 'MB', 'GB']
- const i = Math.floor(Math.log(bytes) / Math.log(k))
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
+ const filteredFiles = useMemo(() => {
+ return files.filter(isAllowedFile)
+ }, [files])
+
+ useEffect(() => {
+ const urls = filteredFiles.map((file) => (file.type.startsWith('image/') ? URL.createObjectURL(file) : null))
+ setPreviewUrls(urls)
+
+ return () => {
+ urls.forEach((url) => {
+ if (url) URL.revokeObjectURL(url)
+ })
+ }
+ }, [filteredFiles])
+
+ const handleValidate = (filesToReview: File[]) => {
+ const disallowed = filesToReview.filter((file) => !isAllowedFile(file))
+ if (disallowed.length > 0) {
+ toast.error('Some files are not supported', {
+ description: 'Only PDF, TXT, PPTX, and DOCX files are allowed.',
+ })
+ }
}
- const handleDragEvents = (e: React.DragEvent, isDragging: boolean) => {
- e.preventDefault()
- e.stopPropagation()
- setIsDragging(isDragging)
+ const handleFilesSelected = (e: ChangeEvent) => {
+ const selectedFiles = Array.from(e.target.files ?? [])
+ if (selectedFiles.length === 0) return
+
+ const nextFiles = multiple ? [...files, ...selectedFiles] : [selectedFiles[0]]
+ const allowedFiles = nextFiles.filter(isAllowedFile)
+
+ onFilesChange(allowedFiles)
+ handleValidate(nextFiles)
+ if (allowedFiles.length > files.length) {
+ toast.success('Files selected', {
+ description: `${allowedFiles.length - files.length} file(s) have been added`,
+ })
+ }
+ e.currentTarget.value = ''
}
- const handleDrop = (e: React.DragEvent) => {
+ const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
- e.stopPropagation()
setIsDragging(false)
- const droppedFiles = Array.from(e.dataTransfer.files);
- const hasPdf = files.some(file => file.type === 'application/pdf');
+ const droppedFiles = Array.from(e.dataTransfer.files ?? [])
+ if (droppedFiles.length === 0) return
- const validTypes = [
- 'application/pdf',
- 'text/plain',
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
- ];
-
- const invalidFiles = droppedFiles.filter(file => !validTypes.includes(file.type));
- if (invalidFiles.length > 0) {
- toast.error('Invalid file type', {
- description: 'Please upload only PDF, TXT, PPTX, or DOCX files',
- });
- return;
- }
-
- if (hasPdf && droppedFiles.some(file => file.type === 'application/pdf')) {
- toast.error('Multiple PDF files are not allowed', {
- description: 'Please select only one PDF file',
- });
- return;
- }
-
- const validFiles = droppedFiles.filter(file => {
- return !(hasPdf && file.type === 'application/pdf');
- });
-
- if (validFiles.length > 0) {
- const updatedFiles = [...files, ...validFiles]
- onFilesChange(updatedFiles)
+ const nextFiles = multiple ? [...files, ...droppedFiles] : [droppedFiles[0]]
+ const allowedFiles = nextFiles.filter(isAllowedFile)
+ onFilesChange(allowedFiles)
+ handleValidate(nextFiles)
+ if (allowedFiles.length > files.length) {
toast.success('Files selected', {
- description: `${validFiles.length} file(s) have been added`,
+ description: `${allowedFiles.length - files.length} file(s) have been added`,
})
}
}
- const handleFileInput = (e: React.ChangeEvent) => {
- const selectedFiles = Array.from(e.target.files || []);
-
- const hasPdf = files.some(file => file.type === 'application/pdf');
-
- const validFiles = selectedFiles.filter(file => {
- return !(hasPdf && file.type === 'application/pdf');
- });
-
- if (validFiles.length > 0) {
- const updatedFiles = [...files, ...validFiles]
- onFilesChange(updatedFiles)
-
- toast.success('Files selected', {
- description: `${validFiles.length} file(s) have been added`,
- })
- }
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault()
+ setIsDragging(true)
}
- const removeFile = (fileId: string) => {
- const updatedFiles = files.filter(file => {
- const currentFileId = `${file.name || 'unnamed'}-${file.lastModified || Date.now()}-${file.size || 0}`
- return currentFileId !== fileId
- })
- onFilesChange(updatedFiles)
+ const handleDragLeave = (e: React.DragEvent) => {
+ e.preventDefault()
+ setIsDragging(false)
}
+ const handleRemoveFileAt = (index: number) => {
+ const nextFiles = filteredFiles.filter((_, i) => i !== index)
+ onFilesChange(nextFiles)
+ }
+
+ const handleClearFiles = () => {
+ if (!hasFiles) return
+ onFilesChange([])
+ }
return (
-
+
+
+
+ {hasFiles ? `${filteredFiles.length} attachment${filteredFiles.length > 1 ? 's' : ''}` : 'No attachments yet'}
+
+
+ Clear all
+
+
-
fileInputRef.current?.click()}
- className={cn(
- "w-full border cursor-pointer border-dashed border-[#B8B8C1] rounded-lg",
- "transition-all duration-300 ease-in-out ",
- " flex flex-col ",
- isDragging && "border-purple-400 bg-purple-50"
- )}
- onDragOver={(e) => handleDragEvents(e, true)}
- onDragLeave={(e) => handleDragEvents(e, false)}
+
-
-
-
-
-
- {isDragging
- ? Drop your file here
- : Click to Upload or drag & drop.
- }
-
-
- Supports PDFs, Text files, PPTX, DOCX
-
-
-
-
-
- {/*
{
- e.stopPropagation()
- fileInputRef.current?.click()
- }}
- className="px-6 py-2 bg-purple-600 text-white rounded-full
- hover:bg-purple-700 transition-colors duration-200
- font-medium text-sm"
- >
- Choose Files
- */}
+
+
+
+
+ Drag and drop PDF, TXT, PPTX, DOCX, or click to browse
+
+
- {files.length > 0 && (
-
-
-
-
- Selected Files ({files.length})
-
-
-
- {filesWithIds.map((file) => {
+ {hasFiles && (
+
+
+ {filteredFiles.map((file, idx) => (
+
+ {previewUrls[idx] ? (
+
+ ) : (
+
+
+
+ )}
- return (
- (
-
-
+
+
+ {file.name}
+
+
{formatFileSize(file.size)}
+
-
-
-
{
- e.stopPropagation()
- removeFile(file.id)
- }}
- className="absolute top-1 right-2 p-1.5
- bg-white/80 backdrop-blur-sm rounded-full
- text-gray-500 hover:text-red-500
- shadow-sm hover:shadow-md
- transition-all duration-200"
- aria-label="Remove file"
- >
-
-
-
-
-
-
- {file.name || 'Unnamed File'}
-
-
- {formatFileSize(file.size)}
-
-
-
- )
- )
- })}
-
-
-
- )}
-
-
+
handleRemoveFileAt(idx)}
+ className="ml-2 inline-flex h-8 w-8 items-center justify-center rounded text-red-600 hover:bg-red-50 hover:text-red-700"
+ aria-label={`Remove ${file.name}`}
+ data-testid="remove-file-button"
+ >
+
+
+
+ ))}
+
+ {filteredFiles.length !== files.length && (
+
+ Some files were skipped. Only PDF, TXT, PPTX, and DOCX files are supported.
+
+ )}
+
+ )}
)
}
+const formatFileSize = (bytes: number): string => {
+ if (!bytes || bytes <= 0) return '0 KB'
+ return `${(bytes / 1024).toFixed(1)} KB`
+}
+
+function isAllowedFile(file: File): boolean {
+ const type = (file.type || '').toLowerCase()
+ const name = (file.name || '').toLowerCase()
+ const typeAllowed = ALLOWED_MIME_TYPES.includes(type) || ALLOWED_MIME_PREFIXES.some((prefix) => type.startsWith(prefix))
+
+ if (typeAllowed) return true
+ return ALLOWED_EXTENSIONS.some((ext) => name.endsWith(ext))
+}
+
export default SupportingDoc
diff --git a/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx
index c90087a6..5c777d1e 100644
--- a/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx
+++ b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx
@@ -18,16 +18,14 @@ import { PromptInput } from "./PromptInput";
import { LanguageType, PresentationConfig, ToneType, VerbosityType } from "../type";
import SupportingDoc from "./SupportingDoc";
import { Button } from "@/components/ui/button";
-import { ChevronRight, GitPullRequestCreate, UploadIcon } from "lucide-react";
+import { ChevronRight } from "lucide-react";
import { toast } from "sonner";
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
import { OverlayLoader } from "@/components/ui/overlay-loader";
import Wrapper from "@/components/Wrapper";
import { setPptGenUploadState } from "@/store/slices/presentationGenUpload";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
-import { LanguageSelector } from "./LanguageSelector";
-import AdvanceSettings from "./AdvanceSettings";
-import NumberOfSlide from "./NumberOfSlide";
+import { ConfigurationSelects } from "./ConfigurationSelects";
// Types for loading state
interface LoadingState {
@@ -196,15 +194,7 @@ const UploadPage = () => {
};
return (
-
-
+
{
duration={loadingState.duration}
extra_info={loadingState.extra_info}
/>
- {/* */}
-
+
+
+
+
Configuration
+
Choose slides, tone, and language preferences.
+
+
+
+
-
+
+
Content
+
+
handleConfigChange("prompt", value)}
+ data-testid="prompt-input"
+ />
+
+
+
+
+
Attachments (optional)
-
+
+
+
-
+
-
- Create Presentation
-
+
Generate Presentation
+
+
-
-
-
-
handleConfigChange("prompt", value)}
- data-testid="prompt-input"
- />
-
-
-
-
-
-
-
-
handleConfigChange("language", value)}
-
- />
-
-
-
-
- Next
-
-
-
-
-
-
-
-
-
-
);
};
-export default UploadPage;
+export default UploadPage;
\ No newline at end of file
diff --git a/servers/nextjs/app/(presentation-generator)/upload/page.tsx b/servers/nextjs/app/(presentation-generator)/upload/page.tsx
index ffb7ac27..6ff417d8 100644
--- a/servers/nextjs/app/(presentation-generator)/upload/page.tsx
+++ b/servers/nextjs/app/(presentation-generator)/upload/page.tsx
@@ -45,8 +45,8 @@ const page = () => {
return (
-
-
+
+
AI Presentation
Choose a design, set preferences, and generate polished slides.
diff --git a/servers/nextjs/utils/providerConstants.ts b/servers/nextjs/utils/providerConstants.ts
index ecccbfa6..a936c6a5 100644
--- a/servers/nextjs/utils/providerConstants.ts
+++ b/servers/nextjs/utils/providerConstants.ts
@@ -22,6 +22,7 @@ export interface LLMProviderOption {
description?: string;
model_value?: string;
model_label?: string;
+ url?: string;
}
export const IMAGE_PROVIDERS: Record
= {
@@ -95,16 +96,19 @@ export const LLM_PROVIDERS: Record = {
value: "openai",
label: "OpenAI",
description: "OpenAI's latest text generation model",
+ url: "https://api.openai.com/v1",
},
google: {
value: "google",
label: "Google",
description: "Google's primary text generation model",
+ url: "https://api.google.com/v1",
},
anthropic: {
value: "anthropic",
label: "Anthropic",
description: "Anthropic's Claude models",
+ url: "https://api.anthropic.com/v1",
},
ollama: {
value: "ollama",