refactor: improve export filename safety and config handling, cleanup components

This commit is contained in:
shiva raj badu 2026-04-19 22:59:15 +05:45
parent 3a4aacd0dd
commit 48047cf288
No known key found for this signature in database
6 changed files with 87 additions and 135 deletions

View file

@ -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')

View file

@ -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 }) => (

View file

@ -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;

View file

@ -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"

View file

@ -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<number>(1)
const [selectedMode, setSelectedMode] = useState<string>("presenton")
const config = useSelector((state: RootState) => state.userConfig);
const [llmConfig, setLlmConfig] = useState<LLMConfig>(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<boolean>(false);
const [buttonState, setButtonState] = useState<ButtonState>({
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) {

View file

@ -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;
}