diff --git a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/dashboard/components/Header.tsx b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/dashboard/components/Header.tsx
index 3085a10e..b971755e 100644
--- a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/dashboard/components/Header.tsx
+++ b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/dashboard/components/Header.tsx
@@ -28,10 +28,10 @@ const Header = () => {
const backHref = backToUpload ? "/upload" : backToTemplates ? "/templates" : "/dashboard";
const backLabel = backToUpload
- ? "Back"
+ ? "BACK"
: backToTemplates
- ? "Back"
- : "Back";
+ ? "BACK"
+ : "BACK";
return (
{
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: backHref })
}
>
-
+
{backLabel}
diff --git a/electron/servers/nextjs/components/SettingCodex.tsx b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingCodex.tsx
similarity index 98%
rename from electron/servers/nextjs/components/SettingCodex.tsx
rename to electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingCodex.tsx
index b0e7b705..8b4e8b9a 100644
--- a/electron/servers/nextjs/components/SettingCodex.tsx
+++ b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingCodex.tsx
@@ -10,7 +10,6 @@ import {
User,
UserCheck,
} from "lucide-react";
-import { Button } from "./ui/button";
import {
Command,
CommandEmpty,
@@ -18,11 +17,12 @@ import {
CommandInput,
CommandItem,
CommandList,
-} from "./ui/command";
-import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
+} from "@/components/ui/command";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import { getApiUrl } from "@/utils/api";
+import { Button } from "@/components/ui/button";
interface CodexConfigProps {
codexModel: string;
@@ -205,7 +205,8 @@ export default function CodexConfig({
await fetch(getApiUrl("/api/v1/ppt/codex/auth/logout"), { method: "POST" });
setAuthStatus("unauthenticated");
applyProfile({});
- onInputChange("", "codex_model");
+ onInputChange("codex", "LLM");
+ onInputChange('', "codex_model");
toast.success("Signed out from ChatGPT");
} catch {
toast.error("Sign out failed");
diff --git a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingPage.tsx b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingPage.tsx
index bf02088a..956cbb77 100644
--- a/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingPage.tsx
+++ b/electron/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingPage.tsx
@@ -251,7 +251,6 @@ const SettingsPage = () => {
return null;
}
-
const textProviderKey = llmConfig.LLM || "openai";
const textProviderLabel =
LLM_PROVIDERS[textProviderKey]?.label || textProviderKey;
@@ -279,6 +278,67 @@ const SettingsPage = () => {
? IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER]?.label || llmConfig.IMAGE_PROVIDER
: "No image provider";
+
+ useEffect(() => {
+
+ if (llmConfig.LLM === "codex" && !llmConfig.CODEX_MODEL || llmConfig.LLM === "openai" && !llmConfig.OPENAI_MODEL || llmConfig.LLM === "google" && !llmConfig.GOOGLE_MODEL || llmConfig.LLM === "anthropic" && !llmConfig.ANTHROPIC_MODEL || llmConfig.LLM === "ollama" && !llmConfig.OLLAMA_MODEL || llmConfig.LLM === "custom" && !llmConfig.CUSTOM_MODEL) {
+ notify.error("Cannot save settings", "Please select a model for the selected provider");
+
+ const currentUrl = window.location.href;
+
+ const handleBeforeUnload = (e: BeforeUnloadEvent) => {
+ console.log("beforeunload");
+ e.preventDefault();
+ e.returnValue = "";
+ };
+
+ const handleClick = (e: MouseEvent) => {
+
+
+ const target = e.target as HTMLElement | null;
+ const link = target?.closest("a");
+
+ if (!link) return;
+
+ const href = link.getAttribute("href");
+ const targetAttr = link.getAttribute("target");
+
+ if (
+ href &&
+ href !== "#" &&
+ !href.startsWith("javascript:") &&
+ targetAttr !== "_blank"
+ ) {
+
+ // notify.error("Cannot save settings", "Please select a model for the selected provider");
+ e.preventDefault();
+ window.history.pushState(null, "", pathname);
+ }
+ };
+
+ const handlePopState = () => {
+ console.log("popstate");
+ window.history.pushState(null, "", pathname);
+ };
+
+ window.addEventListener("beforeunload", handleBeforeUnload);
+ window.addEventListener("popstate", handlePopState);
+ document.addEventListener("click", handleClick, true);
+
+ // keep current page in history
+ window.history.pushState(null, "", currentUrl);
+
+ return () => {
+ window.removeEventListener("beforeunload", handleBeforeUnload);
+ window.removeEventListener("popstate", handlePopState);
+ document.removeEventListener("click", handleClick, true);
+ };
+ }
+
+ }, [llmConfig, pathname]);
+
+
+
return (
{
if (!presentation_id) {
return
;
}
+ const handleTabChange = (tab: string) => {
+ if (streamState.isStreaming) {
+ return;
+ }
+ setActiveTab(tab);
+
+ };
return (
@@ -51,10 +58,10 @@ const OutlinePage: React.FC = () => {
-
+
{/* Reserves vertical space so content does not sit under the fixed tab bar */}
-
+
{
-
-
-
Creating Your Presentation
+
+
+
+ Creating Your Presentation
+
@@ -42,6 +48,29 @@ const LoadingState = () => {
+
);
};
diff --git a/electron/servers/nextjs/app/(presentation-generator)/upload/components/ConfigurationSelects.tsx b/electron/servers/nextjs/app/(presentation-generator)/upload/components/ConfigurationSelects.tsx
index 6cb76f7d..e2040136 100644
--- a/electron/servers/nextjs/app/(presentation-generator)/upload/components/ConfigurationSelects.tsx
+++ b/electron/servers/nextjs/app/(presentation-generator)/upload/components/ConfigurationSelects.tsx
@@ -81,19 +81,22 @@ const SlideCountSelect: React.FC<{
return (
-
+
= ({ value, onValueChange, open, onOpenChange }) => (
-
+
+
@@ -284,7 +288,7 @@ export function ConfigurationSelects({
title="Advanced settings"
type="button"
onClick={() => handleOpenAdvancedChange(true)}
- className="ml-auto flex items-center gap-2 text-sm bg-[#F6F6F9] 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"
+ className="ml-auto flex items-center gap-2 text-sm 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"
>
diff --git a/electron/servers/nextjs/app/(presentation-generator)/upload/components/CurrentConfig.tsx b/electron/servers/nextjs/app/(presentation-generator)/upload/components/CurrentConfig.tsx
new file mode 100644
index 00000000..6ea34c2d
--- /dev/null
+++ b/electron/servers/nextjs/app/(presentation-generator)/upload/components/CurrentConfig.tsx
@@ -0,0 +1,43 @@
+import { RootState } from '@/store/store';
+import { IMAGE_PROVIDERS, LLM_PROVIDERS } from '@/utils/providerConstants';
+import React from 'react'
+import { useSelector } from 'react-redux';
+
+const CurrentConfig = () => {
+ const userConfigState = useSelector((state: RootState) => state.userConfig);
+ const llmConfig = userConfigState.llm_config;
+ const textProviderKey = llmConfig.LLM || "openai";
+ const textProviderLabel =
+ LLM_PROVIDERS[textProviderKey]?.label || textProviderKey;
+ const selectedTextModel =
+ textProviderKey === "openai"
+ ? llmConfig.OPENAI_MODEL
+ : textProviderKey === "google"
+ ? llmConfig.GOOGLE_MODEL
+ : textProviderKey === "anthropic"
+ ? llmConfig.ANTHROPIC_MODEL
+ : textProviderKey === "ollama"
+ ? llmConfig.OLLAMA_MODEL
+ : textProviderKey === "custom"
+ ? llmConfig.CUSTOM_MODEL
+ : textProviderKey === "codex"
+ ? llmConfig.CODEX_MODEL
+ : "";
+ const textSummary = selectedTextModel
+ ? `${textProviderLabel} (${selectedTextModel})`
+ : textProviderLabel;
+
+ const imageSummary = llmConfig.DISABLE_IMAGE_GENERATION
+ ? "Image generation disabled"
+ : llmConfig.IMAGE_PROVIDER
+ ? IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER]?.label || llmConfig.IMAGE_PROVIDER
+ : "No image provider";
+ return (
+
+ {textSummary} · {imageSummary}
+
+
+ )
+}
+
+export default CurrentConfig
diff --git a/electron/servers/nextjs/app/(presentation-generator)/upload/components/PromptInput.tsx b/electron/servers/nextjs/app/(presentation-generator)/upload/components/PromptInput.tsx
index 42241616..99ef83fa 100644
--- a/electron/servers/nextjs/app/(presentation-generator)/upload/components/PromptInput.tsx
+++ b/electron/servers/nextjs/app/(presentation-generator)/upload/components/PromptInput.tsx
@@ -1,4 +1,5 @@
import { Textarea } from "@/components/ui/textarea";
+import { PencilIcon } from "lucide-react";
import { useState } from "react";
interface PromptInputProps {
@@ -16,14 +17,24 @@ export function PromptInput({ value, onChange }: PromptInputProps) {
return (
-
+
diff --git a/electron/servers/nextjs/app/(presentation-generator)/upload/components/SupportingDoc.tsx b/electron/servers/nextjs/app/(presentation-generator)/upload/components/SupportingDoc.tsx
index 6f013ec0..eaf7066e 100644
--- a/electron/servers/nextjs/app/(presentation-generator)/upload/components/SupportingDoc.tsx
+++ b/electron/servers/nextjs/app/(presentation-generator)/upload/components/SupportingDoc.tsx
@@ -1,7 +1,7 @@
'use client'
import React, { ChangeEvent, useEffect, useMemo, useState } from 'react'
-import { File, Paperclip, X } from 'lucide-react'
+import { File, Paperclip, Plus, X } from 'lucide-react'
import { toast } from 'sonner'
interface SupportingDocProps {
@@ -196,10 +196,12 @@ const SupportingDoc = ({
data-testid="file-upload-input"
/>
-
-
- Drag and drop Office docs, spreadsheets, images, PDF/TXT, or click to browse
-
+
+
(Office docs, spreadsheets, images, PDF/TXT)
diff --git a/electron/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx b/electron/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx
index a6f3d9a1..4664fca5 100644
--- a/electron/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx
+++ b/electron/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx
@@ -28,6 +28,7 @@ import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { ConfigurationSelects } from "./ConfigurationSelects";
import { RootState } from "@/store/store";
import { ImagesApi } from "../../services/api/images";
+import CurrentConfig from "./CurrentConfig";
const STOCK_IMAGE_PROVIDERS = new Set(["pexels", "pixabay"]);
@@ -248,20 +249,17 @@ const UploadPage = () => {
duration={loadingState.duration}
extra_info={loadingState.extra_info}
/>
-
-
-
-
Configuration
-
+
+
+
-
-
-
Content
+
+
{
-
- Generate Presentation
+
+ Generate
+
+
+
+
+
-
Choose a design, set preferences, and generate polished slides.
+
Turn prompts or documents into presentations with AI
+ {/* stars */}
+
diff --git a/electron/servers/nextjs/components/CodexConfig.tsx b/electron/servers/nextjs/components/CodexConfig.tsx
index 2073e06e..3f6453d2 100644
--- a/electron/servers/nextjs/components/CodexConfig.tsx
+++ b/electron/servers/nextjs/components/CodexConfig.tsx
@@ -1,27 +1,12 @@
"use client";
import { useEffect, useRef, useState } from "react";
import {
- Check,
- ChevronUp,
Loader2,
RefreshCw,
Trash2,
- Crown,
- User,
UserCheck,
ArrowRight,
} from "lucide-react";
-import { Button } from "./ui/button";
-import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
-} from "./ui/command";
-import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
-import { cn } from "@/lib/utils";
import { toast } from "sonner";
import { getApiUrl } from "@/utils/api";
import { MixpanelEvent, trackEvent } from "@/utils/mixpanel";
diff --git a/electron/servers/nextjs/components/ui/overlay-loader.tsx b/electron/servers/nextjs/components/ui/overlay-loader.tsx
index 58a98a6e..327b6617 100644
--- a/electron/servers/nextjs/components/ui/overlay-loader.tsx
+++ b/electron/servers/nextjs/components/ui/overlay-loader.tsx
@@ -1,5 +1,4 @@
import { cn } from "@/lib/utils"
-import { Loader } from "./loader"
import { ProgressBar } from "./progress-bar"
import { useEffect, useState } from "react"
@@ -46,14 +45,18 @@ export const OverlayLoader = ({
>
-

+
{showProgress ? (
{text && (
-
+
{text}
- {extra_info &&
{extra_info}
}
+ {extra_info &&
{extra_info}
}
)}
) : (
<>
-
+
{text}
- {extra_info &&
{extra_info}
}
+ {extra_info &&
{extra_info}
}
>
)}
+
+
+
)
}
\ No newline at end of file
diff --git a/electron/servers/nextjs/utils/storeHelpers.ts b/electron/servers/nextjs/utils/storeHelpers.ts
index fce2d783..f6923f88 100644
--- a/electron/servers/nextjs/utils/storeHelpers.ts
+++ b/electron/servers/nextjs/utils/storeHelpers.ts
@@ -110,6 +110,42 @@ export const getLLMConfigValidationError = (
return null;
};
+/** Codex is selected but no model chosen — block navigation away from Settings. */
+export function isCodexMissingSelectedModel(llmConfig: LLMConfig): boolean {
+ return llmConfig.LLM === "codex" && !isProvided(llmConfig.CODEX_MODEL);
+}
+
+/**
+ * While on Settings with Codex selected and no model (e.g. after sign-out), block leaving
+ * for any destination other than Settings. Resolves once the user picks a model, signs in again, or switches provider.
+ */
+export function shouldBlockCodexOutboundNav(
+ llmConfig: LLMConfig,
+ destinationPath: string,
+ currentPathname: string | null
+): boolean {
+ if (!isCodexMissingSelectedModel(llmConfig)) return false;
+ const onSettings =
+ currentPathname === "/settings" ||
+ (currentPathname?.startsWith("/settings/") ?? false);
+ if (!onSettings) return false;
+ const path = destinationPath.split("?")[0] || "";
+ if (path === "/settings" || path.startsWith("/settings/")) return false;
+ return true;
+}
+
+/** Keep Redux in sync when Codex signs out so nav guards see cleared CODEX_MODEL. */
+export function syncStoreAfterCodexSignOut(): void {
+ const prev = store.getState().userConfig.llm_config;
+ store.dispatch(
+ setLLMConfig({
+ ...prev,
+ LLM: "codex",
+ CODEX_MODEL: "",
+ })
+ );
+}
+
export const handleSaveLLMConfig = async (llmConfig: LLMConfig) => {
const validationError = getLLMConfigValidationError(llmConfig);
if (validationError) {