refactor: improve export filename safety and config handling, cleanup components
This commit is contained in:
parent
3a4aacd0dd
commit
48047cf288
6 changed files with 87 additions and 135 deletions
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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 }) => (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue