diff --git a/servers/nextjs/app/(presentation-generator)/(dashboard)/theme/components/ThemePanel/index.tsx b/servers/nextjs/app/(presentation-generator)/(dashboard)/theme/components/ThemePanel/index.tsx index 1e342628..460d2ab0 100644 --- a/servers/nextjs/app/(presentation-generator)/(dashboard)/theme/components/ThemePanel/index.tsx +++ b/servers/nextjs/app/(presentation-generator)/(dashboard)/theme/components/ThemePanel/index.tsx @@ -517,11 +517,11 @@ const ThemePanel: React.FC = () => { const handleCustomFontChange = async (fontFile: File) => { try { setIsFontUploading(true) - const { name, url } = await ThemeApi.uploadFont(fontFile) + const { font_name, font_url } = await ThemeApi.uploadFont(fontFile) setCustomFonts({ textFont: { - name: name, - url: url, + name: font_name, + url: font_url, } }) trackEvent(MixpanelEvent.Theme_Custom_Font_Uploaded, { @@ -533,17 +533,17 @@ const ThemePanel: React.FC = () => { trackEvent(MixpanelEvent.Theme_Font_Changed, { pathname, theme_id: selectedTheme.id, - font_name: name, - font_url: url, + font_name: font_name, + font_url: font_url, source: "uploaded_font", }) // Add the newly uploaded font to userFonts if not already present - if (!userFonts.fonts.find(f => f.name === name)) { + if (!userFonts.fonts.find(f => f.name === font_name)) { setUserFonts(prev => ({ - fonts: [...prev.fonts, { name, url }] + fonts: [...prev.fonts, { name: font_name, url: font_url }] })) } - toast.success(`Font "${name}" uploaded successfully`) + toast.success(`Font "${font_name}" uploaded successfully`) } catch (error: any) { console.error('Failed to upload font', error) toast.error(error?.message || 'Failed to upload font') diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationHeader.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationHeader.tsx index 8d253464..141c0a42 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationHeader.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationHeader.tsx @@ -42,6 +42,43 @@ import { Theme } from "../../services/api/types"; import MarkdownRenderer from "@/components/MarkDownRender"; import { cn } from "@/lib/utils"; +const MAX_EXPORT_TITLE_LENGTH = 40; + +const buildSafeExportFileName = ( + rawTitle: string | null | undefined, + extension: "pdf" | "pptx" +) => { + const normalizedTitle = (rawTitle || "presentation").trim(); + const titleWithoutExtension = normalizedTitle.replace( + /\.(pdf|pptx)$/i, + "" + ); + + let safeBase = titleWithoutExtension + // Replace all punctuation/special chars (including dots) with dashes + .replace(/[^a-zA-Z0-9\s_-]+/g, "-") + // Replace whitespace with single dashes + .replace(/\s+/g, "-") + // Collapse repeated separators + .replace(/[-_]{2,}/g, "-") + // Trim separators from both ends + .replace(/^[-_]+|[-_]+$/g, ""); + + if (!safeBase) { + safeBase = "presentation"; + } + + if (safeBase.length > MAX_EXPORT_TITLE_LENGTH) { + safeBase = safeBase.slice(0, MAX_EXPORT_TITLE_LENGTH).replace(/[-_]+$/g, ""); + } + + if (!safeBase) { + safeBase = "presentation"; + } + + return `${safeBase}.${extension}`; +}; + const PresentationHeader = ({ presentation_id, isPresentationSaving, @@ -169,10 +206,18 @@ const PresentationHeader = ({ if (!pptx_model) { throw new Error("Failed to get presentation PPTX model"); } - const pptx_path = await PresentationGenerationApi.exportAsPPTX(pptx_model); + const safePptxFileName = buildSafeExportFileName( + presentationData?.title, + "pptx" + ); + const safePptxTitle = safePptxFileName.replace(/\.pptx$/i, ""); + const pptx_path = await PresentationGenerationApi.exportAsPPTX({ + ...pptx_model, + name: safePptxTitle, + }); if (pptx_path) { // window.open(pptx_path, '_self'); - downloadLink(pptx_path); + downloadLink(pptx_path, safePptxFileName); } else { throw new Error("No path returned from export"); } @@ -201,18 +246,23 @@ const PresentationHeader = ({ setIsExporting(true); // Save the presentation data before exporting await PresentationGenerationApi.updatePresentationContent(presentationData); + const safePdfFileName = buildSafeExportFileName( + presentationData?.title, + "pdf" + ); + const safePdfTitle = safePdfFileName.replace(/\.pdf$/i, ""); const response = await fetch('/api/export-as-pdf', { method: 'POST', body: JSON.stringify({ id: presentation_id, - title: presentationData?.title, + title: safePdfTitle, }) }); if (response.ok) { const { path: pdfPath } = await response.json(); // window.open(pdfPath, '_blank'); - downloadLink(pdfPath); + downloadLink(pdfPath, safePdfFileName); } else { throw new Error("Failed to export PDF"); } @@ -237,17 +287,14 @@ const PresentationHeader = ({ }); router.push(`/presentation?id=${presentation_id}&stream=true`); }; - const downloadLink = (path: string) => { - // if we have popup access give direct download if not redirect to the path - if (window.opener) { - window.open(path, '_blank'); - } else { - const link = document.createElement('a'); - link.href = path; - link.download = path.split('/').pop() || 'download'; - document.body.appendChild(link); - link.click(); - } + const downloadLink = (path: string, fileName: string) => { + const link = document.createElement("a"); + link.href = path; + link.download = fileName; + link.rel = "noopener"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); }; const ExportOptions = ({ mobile }: { mobile: boolean }) => ( diff --git a/servers/nextjs/app/ConfigurationInitializer.tsx b/servers/nextjs/app/ConfigurationInitializer.tsx index e5583523..9225d699 100644 --- a/servers/nextjs/app/ConfigurationInitializer.tsx +++ b/servers/nextjs/app/ConfigurationInitializer.tsx @@ -61,11 +61,10 @@ export function ConfigurationInitializer({ children }: { children: React.ReactNo if (!llmConfig.LLM) { llmConfig.LLM = 'openai'; } - if (!llmConfig.IMAGE_PROVIDER) { - llmConfig.IMAGE_PROVIDER = 'gpt-image-1.5'; - } + dispatch(setLLMConfig(llmConfig)); const isValid = hasValidLLMConfig(llmConfig); + console.log('isValid', isValid); if (route.startsWith('/pdf-maker')) { setIsLoading(false); return; diff --git a/servers/nextjs/app/api/user-config/route.ts b/servers/nextjs/app/api/user-config/route.ts index af604f4b..6b1e8ca4 100644 --- a/servers/nextjs/app/api/user-config/route.ts +++ b/servers/nextjs/app/api/user-config/route.ts @@ -35,9 +35,11 @@ export async function POST(request: Request) { const userConfig = await request.json(); + console.log('userConfig', userConfig); let existingConfig: LLMConfig = {}; if (fs.existsSync(userConfigPath)) { const configData = fs.readFileSync(userConfigPath, "utf-8"); + existingConfig = JSON.parse(configData); } const definedIncomingEntries = Object.entries(userConfig).filter( @@ -57,6 +59,12 @@ export async function POST(request: Request) { CODEX_USERNAME: existingConfig.CODEX_USERNAME, CODEX_EMAIL: existingConfig.CODEX_EMAIL, CODEX_IS_PRO: existingConfig.CODEX_IS_PRO, + DISABLE_IMAGE_GENERATION: Object.prototype.hasOwnProperty.call( + userConfig, + "DISABLE_IMAGE_GENERATION" + ) + ? userConfig.DISABLE_IMAGE_GENERATION + : existingConfig.DISABLE_IMAGE_GENERATION, DISABLE_ANONYMOUS_TRACKING: Object.prototype.hasOwnProperty.call( userConfig, "DISABLE_ANONYMOUS_TRACKING" diff --git a/servers/nextjs/components/Home.tsx b/servers/nextjs/components/Home.tsx index d6960da6..5acd2ea8 100644 --- a/servers/nextjs/components/Home.tsx +++ b/servers/nextjs/components/Home.tsx @@ -2,11 +2,9 @@ import { useState, useEffect, useMemo } from "react"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; -import { Loader2, Download, CheckCircle } from "lucide-react"; import { useSelector } from "react-redux"; import { RootState } from "@/store/store"; import { handleSaveLLMConfig } from "@/utils/storeHelpers"; -import LLMProviderSelection from "./LLMSelection"; import { checkIfSelectedOllamaModelIsPulled, pullOllamaModel, @@ -32,120 +30,15 @@ interface ButtonState { } -const getTaperedSideOffset = (offset: number, top: number) => { - const taperMultiplier = Math.max(0.72, 1.85 - top * 0.012); - return Math.min(29, Number((offset * taperMultiplier).toFixed(2))); -}; export default function Home() { const router = useRouter(); - const pathname = usePathname(); const [step, setStep] = useState(1) const [selectedMode, setSelectedMode] = useState("presenton") const config = useSelector((state: RootState) => state.userConfig); - const [llmConfig, setLlmConfig] = useState(config.llm_config); - - const [downloadingModel, setDownloadingModel] = useState<{ - name: string; - size: number | null; - downloaded: number | null; - status: string; - done: boolean; - } | null>(null); - const [showDownloadModal, setShowDownloadModal] = useState(false); - const [buttonState, setButtonState] = useState({ - isLoading: false, - isDisabled: false, - text: "Save Configuration", - showProgress: false - }); const canChangeKeys = config.can_change_keys; - const downloadProgress = useMemo(() => { - if (downloadingModel && downloadingModel.downloaded !== null && downloadingModel.size !== null) { - return Math.round((downloadingModel.downloaded / downloadingModel.size) * 100); - } - return 0; - }, [downloadingModel?.downloaded, downloadingModel?.size]); - const handleSaveConfig = async () => { - trackEvent(MixpanelEvent.Home_SaveConfiguration_Button_Clicked, { pathname }); - try { - setButtonState(prev => ({ - ...prev, - isLoading: true, - isDisabled: true, - text: "Saving Configuration..." - })); - // API: save config - trackEvent(MixpanelEvent.Home_SaveConfiguration_API_Call); - // API CALL: save config - await handleSaveLLMConfig(llmConfig); - - if (llmConfig.LLM === "ollama" && llmConfig.OLLAMA_MODEL) { - // API: check model pulled - trackEvent(MixpanelEvent.Home_CheckOllamaModelPulled_API_Call); - const isPulled = await checkIfSelectedOllamaModelIsPulled(llmConfig.OLLAMA_MODEL); - if (!isPulled) { - setShowDownloadModal(true); - // API: download model - trackEvent(MixpanelEvent.Home_DownloadOllamaModel_API_Call); - await handleModelDownload(); - } - } - toast.info("Configuration saved successfully"); - setButtonState(prev => ({ - ...prev, - isLoading: false, - isDisabled: false, - text: "Save Configuration" - })); - // Track navigation from -> to - trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/upload" }); - router.push("/upload"); - } catch (error) { - toast.info(error instanceof Error ? error.message : "Failed to save configuration"); - setButtonState(prev => ({ - ...prev, - isLoading: false, - isDisabled: false, - text: "Save Configuration" - })); - } - }; - - const handleModelDownload = async () => { - try { - await pullOllamaModel(llmConfig.OLLAMA_MODEL!, setDownloadingModel); - } - finally { - setDownloadingModel(null); - setShowDownloadModal(false); - } - }; - - - useEffect(() => { - if (downloadingModel && downloadingModel.downloaded !== null && downloadingModel.size !== null) { - const percentage = Math.round(((downloadingModel.downloaded / downloadingModel.size) * 100)); - setButtonState({ - isLoading: true, - isDisabled: true, - text: `Downloading Model (${percentage}%)`, - showProgress: true, - progressPercentage: percentage, - status: downloadingModel.status - }); - } - - if (downloadingModel && downloadingModel.done) { - setTimeout(() => { - setShowDownloadModal(false); - setDownloadingModel(null); - toast.info("Model downloaded successfully!"); - }, 2000); - } - }, [downloadingModel]); useEffect(() => { if (!canChangeKeys) { diff --git a/servers/nextjs/utils/storeHelpers.ts b/servers/nextjs/utils/storeHelpers.ts index c9cec03c..ef8710a5 100644 --- a/servers/nextjs/utils/storeHelpers.ts +++ b/servers/nextjs/utils/storeHelpers.ts @@ -123,5 +123,10 @@ export const handleSaveLLMConfig = async (llmConfig: LLMConfig) => { store.dispatch(setLLMConfig(llmConfig)); }; -export const hasValidLLMConfig = (llmConfig: LLMConfig) => - getLLMConfigValidationError(llmConfig) === null; +export const hasValidLLMConfig = (llmConfig: LLMConfig) => { + console.log('llmConfig', llmConfig); + + const validationError = getLLMConfigValidationError(llmConfig); + console.log('validationError', validationError); + return validationError === null; +}