- {/* Web Grounding Toggle - at the end, below models dropdown */}
-
-
-
-
onInputChange(checked, "web_grounding")}
- />
+ {/* Web Grounding Toggle - show at the end, below models dropdown */}
+
+
+
Model Controls
+
+ Configure web access and advanced AI features.
+
+
+
+
+
+ onInputChange(checked, "web_grounding")}
+ />
+
+
+
+
-
-
- If enabled, the model can use web search grounding when available.
-
);
-}
\ No newline at end of file
+}
diff --git a/electron/servers/nextjs/components/Home.tsx b/electron/servers/nextjs/components/Home.tsx
index 71a833cc..bb5319c7 100644
--- a/electron/servers/nextjs/components/Home.tsx
+++ b/electron/servers/nextjs/components/Home.tsx
@@ -14,6 +14,12 @@ import {
import { LLMConfig } from "@/types/llm_config";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { usePathname } from "next/navigation";
+import OnBoardingSlidebar from "./OnBoarding/OnBoardingSlidebar";
+import OnBoardingHeader from "./OnBoarding/OnBoardingHeader";
+import ModeSelectStep from "./OnBoarding/ModeSelectStep";
+import PresentonMode from "./OnBoarding/PresentonMode";
+import GenerationWithImage from "./OnBoarding/GenerationWithImage";
+import FinalStep from "./OnBoarding/FinalStep";
// Button state interface
interface ButtonState {
@@ -25,9 +31,59 @@ interface ButtonState {
status?: string;
}
+const FINAL_STEP_CONFETTI_PIECES = [
+ // left: denser at top
+ { side: "left", offset: 1, top: 3, width: 28, height: 10, color: "#F59E0B", rotate: 12 },
+ { side: "left", offset: 7, top: 5, width: 18, height: 7, color: "#7C3AED", rotate: -10 },
+ { side: "left", offset: 12, top: 7, width: 20, height: 7, color: "#14B8A6", rotate: 22 },
+ { side: "left", offset: 3, top: 10, width: 22, height: 8, color: "#22C55E", rotate: -18 },
+ { side: "left", offset: 9, top: 12, width: 24, height: 8, color: "#E11D48", rotate: 18 },
+ { side: "left", offset: 14, top: 15, width: 18, height: 7, color: "#F43F5E", rotate: 23 },
+ { side: "left", offset: 5, top: 18, width: 20, height: 7, color: "#0EA5E9", rotate: -12 },
+ { side: "left", offset: 11, top: 21, width: 26, height: 9, color: "#2563EB", rotate: 20 },
+ { side: "left", offset: 2, top: 24, width: 19, height: 7, color: "#14B8A6", rotate: -16 },
+ { side: "left", offset: 8, top: 28, width: 21, height: 8, color: "#FB7185", rotate: 27 },
+ { side: "left", offset: 13, top: 32, width: 20, height: 7, color: "#06B6D4", rotate: 16 },
+ { side: "left", offset: 3, top: 36, width: 24, height: 9, color: "#EAB308", rotate: -22 },
+ { side: "left", offset: 10, top: 41, width: 18, height: 7, color: "#A855F7", rotate: -14 },
+ { side: "left", offset: 2, top: 50, width: 30, height: 10, color: "#EC4899", rotate: -28 },
+ { side: "left", offset: 13, top: 58, width: 19, height: 7, color: "#22C55E", rotate: 17 },
+ { side: "left", offset: 5, top: 66, width: 24, height: 8, color: "#8B5CF6", rotate: 14 },
+ { side: "left", offset: 11, top: 74, width: 18, height: 7, color: "#3B82F6", rotate: 12 },
+ { side: "left", offset: 4, top: 82, width: 20, height: 7, color: "#14B8A6", rotate: 21 },
+ { side: "left", offset: 7, top: 90, width: 24, height: 8, color: "#D946EF", rotate: -26 },
+
+ // right: denser at top
+ { side: "right", offset: 1, top: 4, width: 30, height: 10, color: "#F97316", rotate: -14 },
+ { side: "right", offset: 8, top: 6, width: 19, height: 7, color: "#0EA5E9", rotate: 12 },
+ { side: "right", offset: 13, top: 9, width: 20, height: 7, color: "#22C55E", rotate: -20 },
+ { side: "right", offset: 4, top: 12, width: 24, height: 8, color: "#EC4899", rotate: 20 },
+ { side: "right", offset: 10, top: 15, width: 22, height: 8, color: "#06B6D4", rotate: -18 },
+ { side: "right", offset: 15, top: 18, width: 20, height: 7, color: "#22C55E", rotate: -25 },
+ { side: "right", offset: 5, top: 21, width: 18, height: 7, color: "#8B5CF6", rotate: 19 },
+ { side: "right", offset: 12, top: 24, width: 21, height: 8, color: "#F43F5E", rotate: 14 },
+ { side: "right", offset: 2, top: 28, width: 26, height: 9, color: "#84CC16", rotate: 15 },
+ { side: "right", offset: 9, top: 33, width: 21, height: 8, color: "#F97316", rotate: -11 },
+ { side: "right", offset: 14, top: 38, width: 20, height: 7, color: "#A855F7", rotate: -19 },
+ { side: "right", offset: 4, top: 44, width: 19, height: 7, color: "#F43F5E", rotate: 20 },
+ { side: "right", offset: 2, top: 52, width: 28, height: 10, color: "#FACC15", rotate: 25 },
+ { side: "right", offset: 12, top: 60, width: 18, height: 7, color: "#14B8A6", rotate: -15 },
+ { side: "right", offset: 6, top: 68, width: 24, height: 8, color: "#22C55E", rotate: -17 },
+ { side: "right", offset: 1, top: 76, width: 20, height: 7, color: "#A855F7", rotate: 14 },
+ { side: "right", offset: 13, top: 84, width: 20, height: 7, color: "#3B82F6", rotate: -24 },
+ { side: "right", offset: 5, top: 92, width: 26, height: 9, color: "#EAB308", rotate: 18 },
+] as const;
+
+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);
@@ -144,124 +200,154 @@ export default function Home() {
}
return (
-
-
- {/* Branding Header */}
-
-
-

-
-
- Open-source AI presentation generator
-
-
+ //
+ //
+ // {/* Branding Header */}
+ //
+ //
+ //

+ //
+ //
+ // Open-source AI presentation generator
+ //
+ //
- {/* Main Configuration Card */}
-
-
-
+ // {/* Main Configuration Card */}
+ //
+ //
+ //
+ //
+
+ // {/* Download Progress Modal */}
+ // {showDownloadModal && downloadingModel && (
+ //
+ //
+ // {/* Modal Content */}
+ //
+ // {/* Icon */}
+ //
+ // {downloadingModel.done ? (
+ //
+ // ) : (
+ //
+ // )}
+ //
+
+ // {/* Title */}
+ //
+ // {downloadingModel.done ? "Download Complete!" : "Downloading Model"}
+ //
+
+ // {/* Model Name */}
+ //
+ // {llmConfig.OLLAMA_MODEL}
+ //
+
+ // {/* Progress Bar */}
+ // {downloadProgress > 0 && (
+ //
+ //
+ //
+ // {downloadProgress}% Complete
+ //
+ //
+ // )}
+
+ // {/* Status */}
+ // {downloadingModel.status && (
+ //
+ //
+ //
+ // {downloadingModel.status}
+ //
+ //
+ // )}
+
+ // {/* Status Message */}
+ // {downloadingModel.status && downloadingModel.status !== "pulled" && (
+ //
+ // {downloadingModel.status === "downloading" && "Downloading model files..."}
+ // {downloadingModel.status === "verifying" && "Verifying model integrity..."}
+ // {downloadingModel.status === "pulling" && "Pulling model from registry..."}
+ //
+ // )}
+
+ // {/* Download Info */}
+ // {downloadingModel.downloaded && downloadingModel.size && (
+ //
+ //
+ // Downloaded: {(downloadingModel.downloaded / 1024 / 1024).toFixed(1)} MB
+ // Total: {(downloadingModel.size / 1024 / 1024).toFixed(1)} MB
+ //
+ //
+ // )}
+ //
+ //
+ //
+ // )}
+
+ // {/* Fixed Bottom Button */}
+ //
+ //
+ //
+ //
+ //
+ //
+
+
+
+ {step === 3 && (
+
+ {FINAL_STEP_CONFETTI_PIECES.map((piece, index) => (
+
+ ))}
+
+ )}
+
+ {step === 1 && }
+ {step === 2 && selectedMode === "presenton" && }
+ {step === 2 && selectedMode === "image" && }
+ {step === 3 && }
-
- {/* Download Progress Modal */}
- {showDownloadModal && downloadingModel && (
-
-
- {/* Modal Content */}
-
- {/* Icon */}
-
- {downloadingModel.done ? (
-
- ) : (
-
- )}
-
-
- {/* Title */}
-
- {downloadingModel.done ? "Download Complete!" : "Downloading Model"}
-
-
- {/* Model Name */}
-
- {llmConfig.OLLAMA_MODEL}
-
-
- {/* Progress Bar */}
- {downloadProgress > 0 && (
-
-
-
- {downloadProgress}% Complete
-
-
- )}
-
- {/* Status */}
- {downloadingModel.status && (
-
-
-
- {downloadingModel.status}
-
-
- )}
-
- {/* Status Message */}
- {downloadingModel.status && downloadingModel.status !== "pulled" && (
-
- {downloadingModel.status === "downloading" && "Downloading model files..."}
- {downloadingModel.status === "verifying" && "Verifying model integrity..."}
- {downloadingModel.status === "pulling" && "Pulling model from registry..."}
-
- )}
-
- {/* Download Info */}
- {downloadingModel.downloaded && downloadingModel.size && (
-
-
- Downloaded: {(downloadingModel.downloaded / 1024 / 1024).toFixed(1)} MB
- Total: {(downloadingModel.size / 1024 / 1024).toFixed(1)} MB
-
-
- )}
-
-
-
- )}
-
- {/* Fixed Bottom Button */}
-
-
-
-
-
);
}
diff --git a/electron/servers/nextjs/components/ImageSelectionConfig.tsx b/electron/servers/nextjs/components/ImageSelectionConfig.tsx
new file mode 100644
index 00000000..0215bd31
--- /dev/null
+++ b/electron/servers/nextjs/components/ImageSelectionConfig.tsx
@@ -0,0 +1,357 @@
+import React from 'react'
+import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
+import { Button } from './ui/button';
+import { Check, ChevronsUpDown } from 'lucide-react';
+import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from './ui/command';
+import { LLMConfig } from '@/types/llm_config';
+import { IMAGE_PROVIDERS } from '@/utils/providerConstants';
+import { cn } from '@/lib/utils';
+import { Select, SelectItem, SelectContent, SelectTrigger, SelectValue } from './ui/select';
+
+const DALLE_3_QUALITY_OPTIONS = [
+ {
+ label: "Standard",
+ value: "standard",
+ description: "Faster generation with lower cost",
+ },
+ {
+ label: "HD",
+ value: "hd",
+ description: "Higher quality images with increased cost",
+ },
+];
+
+const GPT_IMAGE_1_5_QUALITY_OPTIONS = [
+ {
+ label: "Low",
+ value: "low",
+ description: "Fastest and most cost-effective",
+ },
+ {
+ label: "Medium",
+ value: "medium",
+ description: "Balanced quality and speed",
+ },
+ {
+ label: "High",
+ value: "high",
+ description: "Best quality with longer generation time",
+ },
+];
+const renderQualitySelector = (llmConfig: LLMConfig, input_field_changed: (value: string, field: string) => void) => {
+ if (llmConfig.IMAGE_PROVIDER === "dall-e-3") {
+ return (
+
+
+
+
+ {/* {DALLE_3_QUALITY_OPTIONS.map((option) => (
+
+ ))} */}
+
+
+ );
+ }
+
+ if (llmConfig.IMAGE_PROVIDER === "gpt-image-1.5") {
+ return (
+
+
+
+
+ {/* {GPT_IMAGE_1_5_QUALITY_OPTIONS.map((option) => (
+
+ ))} */}
+
+
+ );
+ }
+
+ return null;
+};
+
+const ImageSelectionConfig = ({ isImageGenerationDisabled, openImageProviderSelect, setOpenImageProviderSelect, llmConfig, input_field_changed, getApiKeyValue, handleApiKeyInputChange }: { isImageGenerationDisabled: boolean, openImageProviderSelect: boolean, setOpenImageProviderSelect: (open: boolean) => void, llmConfig: LLMConfig, input_field_changed: (value: string, field: string) => void, getApiKeyValue: (field: string) => string, handleApiKeyInputChange: (field: string, value: string) => void }) => {
+ return (
+
+
+
+
Image Generation Settings
+
+ Choosing where images come from.
+
+
+
+
+
+ {!isImageGenerationDisabled && (
+ <>
+ {/* Image Provider Selection */}
+
+
+
+
+
+
+
+
+
+
+
+ No provider found.
+
+ {Object.values(IMAGE_PROVIDERS).map(
+ (provider, index) => (
+ {
+ input_field_changed(value, "image_provider");
+ setOpenImageProviderSelect(false);
+ }}
+ >
+
+
+
+
+
+ {provider.label}
+
+
+
+ {provider.description}
+
+
+
+
+ )
+ )}
+
+
+
+
+
+
+
+
+ {renderQualitySelector(llmConfig, input_field_changed)}
+
+ {/* Dynamic API Key Input for Image Provider */}
+ {llmConfig.IMAGE_PROVIDER &&
+ IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER] &&
+ (() => {
+ const provider = IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER];
+
+ // Show info message when using same API key as main provider
+ if (
+ provider.value === "dall-e-3" &&
+ llmConfig.LLM === "openai"
+ ) {
+ return <>>;
+ }
+
+ if (
+ provider.value === "gpt-image-1.5" &&
+ llmConfig.LLM === "openai"
+ ) {
+ return <>>;
+ }
+
+ if (
+ provider.value === "gemini_flash" &&
+ llmConfig.LLM === "google"
+ ) {
+ return <>>;
+ }
+
+ if (
+ provider.value === "nanobanana_pro" &&
+ llmConfig.LLM === "google"
+ ) {
+ return <>>;
+ }
+
+ // Show ComfyUI configuration
+ if (provider.value === "comfyui") {
+ return (
+
+
+
+
+ {
+ input_field_changed(
+ e.target.value,
+ "comfyui_url"
+ );
+ }}
+ />
+
+
+
+ Use your machine IP address (not localhost) when
+ running in Docker
+
+
+
+
+
+
+
+ Export your workflow from ComfyUI using "Export
+ (API)" and paste the JSON here.
+
+
+
+ );
+ }
+
+ // Show API key input for other providers
+ return (
+
+
+
+
+ handleApiKeyInputChange(
+ provider.apiKeyField || "",
+ e.target.value
+ )
+ }
+ />
+
+
+
+ );
+ })()}
+ >
+ )}
+
+
+
+
+ )
+}
+
+export default ImageSelectionConfig
diff --git a/electron/servers/nextjs/components/LLMSelection.tsx b/electron/servers/nextjs/components/LLMSelection.tsx
index 3e49492e..32ba272f 100644
--- a/electron/servers/nextjs/components/LLMSelection.tsx
+++ b/electron/servers/nextjs/components/LLMSelection.tsx
@@ -1,19 +1,5 @@
"use client";
import { useState, useEffect } from "react";
-import { Tabs, TabsList, TabsTrigger, TabsContent } from "./ui/tabs";
-import { Check, ChevronsUpDown, Info } from "lucide-react";
-import { Button } from "./ui/button";
-import { Switch } from "./ui/switch";
-import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
-} from "./ui/command";
-import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
-import { cn } from "@/lib/utils";
import OpenAIConfig from "./OpenAIConfig";
import GoogleConfig from "./GoogleConfig";
import AnthropicConfig from "./AnthropicConfig";
@@ -24,39 +10,12 @@ import {
updateLLMConfig,
changeProvider as changeProviderUtil,
} from "@/utils/providerUtils";
-import { IMAGE_PROVIDERS, LLM_PROVIDERS } from "@/utils/providerConstants";
import { LLMConfig } from "@/types/llm_config";
+import ImageSelectionConfig from "./ImageSelectionConfig";
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
+
-const DALLE_3_QUALITY_OPTIONS = [
- {
- label: "Standard",
- value: "standard",
- description: "Faster generation with lower cost",
- },
- {
- label: "HD",
- value: "hd",
- description: "Higher quality images with increased cost",
- },
-];
-const GPT_IMAGE_1_5_QUALITY_OPTIONS = [
- {
- label: "Low",
- value: "low",
- description: "Fastest and most cost-effective",
- },
- {
- label: "Medium",
- value: "medium",
- description: "Balanced quality and speed",
- },
- {
- label: "High",
- value: "high",
- description: "Best quality with longer generation time",
- },
-];
// Button state interface
interface ButtonState {
@@ -77,6 +36,7 @@ interface LLMProviderSelectionProps {
) => void;
}
+
export default function LLMProviderSelection({
initialLLMConfig,
onConfigChange,
@@ -85,7 +45,6 @@ export default function LLMProviderSelection({
const [llmConfig, setLlmConfig] = useState(initialLLMConfig);
const [openImageProviderSelect, setOpenImageProviderSelect] = useState(false);
const isImageGenerationDisabled = llmConfig.DISABLE_IMAGE_GENERATION ?? false;
-
useEffect(() => {
onConfigChange(llmConfig);
}, [llmConfig]);
@@ -135,12 +94,12 @@ export default function LLMProviderSelection({
text: needsModelSelection
? "Please Select a Model"
: needsApiKey
- ? "Please Enter API Key"
- : needsOllamaUrl
- ? "Please Enter Ollama URL"
- : needsComfyUIConfig
- ? "Please Configure ComfyUI"
- : "Save Configuration",
+ ? "Please Enter API Key"
+ : needsOllamaUrl
+ ? "Please Enter Ollama URL"
+ : needsComfyUIConfig
+ ? "Please Configure ComfyUI"
+ : "Save Configuration",
showProgress: false,
});
}, [llmConfig]);
@@ -256,77 +215,7 @@ export default function LLMProviderSelection({
});
}, [llmConfig.IMAGE_PROVIDER]);
- const renderQualitySelector = () => {
- if (llmConfig.IMAGE_PROVIDER === "dall-e-3") {
- return (
-
-
-
- {DALLE_3_QUALITY_OPTIONS.map((option) => (
-
- ))}
-
-
- );
- }
- if (llmConfig.IMAGE_PROVIDER === "gpt-image-1.5") {
- return (
-
-
-
- {GPT_IMAGE_1_5_QUALITY_OPTIONS.map((option) => (
-
- ))}
-
-
- );
- }
-
- return null;
- };
return (
@@ -358,6 +247,7 @@ export default function LLMProviderSelection({
{/* OpenAI Content */}
{/* Image Generation Toggle */}
-
+
+ {/*
+
*/}
- {!isImageGenerationDisabled && (
- <>
- {/* Image Provider Selection */}
-
-
-
-
-
-
-
-
-
-
-
- No provider found.
-
- {Object.values(IMAGE_PROVIDERS).map(
- (provider, index) => (
- {
- input_field_changed(value, "image_provider");
- setOpenImageProviderSelect(false);
- }}
- >
-
-
-
-
-
- {provider.label}
-
-
-
- {provider.description}
-
-
-
-
- )
- )}
-
-
-
-
-
-
-
- {renderQualitySelector()}
-
- {/* Dynamic API Key Input for Image Provider */}
- {llmConfig.IMAGE_PROVIDER &&
- IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER] &&
- (() => {
- const provider = IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER];
-
- // Show info message when using same API key as main provider
- if (
- provider.value === "dall-e-3" &&
- llmConfig.LLM === "openai"
- ) {
- return <>>;
- }
-
- if (
- provider.value === "gpt-image-1.5" &&
- llmConfig.LLM === "openai"
- ) {
- return <>>;
- }
-
- if (
- provider.value === "gemini_flash" &&
- llmConfig.LLM === "google"
- ) {
- return <>>;
- }
-
- if (
- provider.value === "nanobanana_pro" &&
- llmConfig.LLM === "google"
- ) {
- return <>>;
- }
-
- // Show ComfyUI configuration
- if (provider.value === "comfyui") {
- return (
-
-
-
-
- {
- input_field_changed(
- e.target.value,
- "comfyui_url"
- );
- }}
- />
-
-
-
- Use your machine IP address (not localhost) when
- running in Docker
-
-
-
-
-
-
-
- Export your workflow from ComfyUI using "Export
- (API)" and paste the JSON here.
-
-
-
- );
- }
-
- // Show API key input for other providers
- return (
-
-
-
-
- handleApiKeyInputChange(
- provider.apiKeyField,
- e.target.value
- )
- }
- />
-
-
-
- API key for {provider.label} image generation
-
-
- );
- })()}
- >
- )}
{/* Model Information */}
-
+ {/*
@@ -673,7 +371,7 @@ export default function LLMProviderSelection({
<>
and{" "}
{llmConfig.IMAGE_PROVIDER &&
- IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER]
+ IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER]
? IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER].label
: "xxxxx"}{" "}
for images
@@ -682,7 +380,28 @@ export default function LLMProviderSelection({
-
+
*/}
+ {/*
*/}
);
diff --git a/electron/servers/nextjs/components/MarkDownRender.tsx b/electron/servers/nextjs/components/MarkDownRender.tsx
new file mode 100644
index 00000000..7a8b9775
--- /dev/null
+++ b/electron/servers/nextjs/components/MarkDownRender.tsx
@@ -0,0 +1,38 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+
+import { marked } from "marked";
+import { cn } from "@/lib/utils";
+
+interface MarkdownRendererProps {
+ content: string;
+ className?: string;
+}
+
+const MarkdownRenderer: React.FC = ({ content, className }) => {
+ const [markdownContent, setMarkdownContent] = useState("");
+
+ useEffect(() => {
+ const parseMarkdown = async () => {
+ try {
+ const parsed = await marked.parse(content);
+ setMarkdownContent(parsed);
+ } catch (error) {
+ console.error("Error parsing markdown:", error);
+ setMarkdownContent("");
+ }
+ };
+
+ parseMarkdown();
+ }, [content]);
+
+ return (
+
+ );
+};
+
+export default MarkdownRenderer;
\ No newline at end of file
diff --git a/electron/servers/nextjs/components/OllamaConfig.tsx b/electron/servers/nextjs/components/OllamaConfig.tsx
index 793fb91e..79ebd065 100644
--- a/electron/servers/nextjs/components/OllamaConfig.tsx
+++ b/electron/servers/nextjs/components/OllamaConfig.tsx
@@ -42,7 +42,7 @@ export default function OllamaConfig({
const fetchOllamaModels = async () => {
try {
setOllamaModelsLoading(true);
- const response = await fetch(getApiUrl('api/v1/ppt/ollama/models/supported'));
+ const response = await fetch(getApiUrl('/api/v1/ppt/ollama/models/supported'));
if (response.ok) {
const data = await response.json();
diff --git a/electron/servers/nextjs/components/OnBoarding/FinalStep.tsx b/electron/servers/nextjs/components/OnBoarding/FinalStep.tsx
new file mode 100644
index 00000000..10ad3acf
--- /dev/null
+++ b/electron/servers/nextjs/components/OnBoarding/FinalStep.tsx
@@ -0,0 +1,31 @@
+import { ArrowRight } from 'lucide-react'
+import { usePathname, useRouter } from 'next/navigation'
+import React from 'react'
+import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
+
+const FinalStep = () => {
+ const router = useRouter()
+ const pathname = usePathname()
+ const handleGoToDashboard = () => {
+ trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/dashboard" });
+ router.push('/dashboard')
+ }
+ const handleGoToUpload = () => {
+ trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/upload" });
+ router.push('/upload')
+ }
+ return (
+
+
+
+

+
Welcome on board!
+
Your AI workspace is ready. Let’s create your first presentation.
+
+
+
+
+ )
+}
+
+export default FinalStep
diff --git a/electron/servers/nextjs/components/OnBoarding/GenerationWithImage.tsx b/electron/servers/nextjs/components/OnBoarding/GenerationWithImage.tsx
new file mode 100644
index 00000000..2b65250a
--- /dev/null
+++ b/electron/servers/nextjs/components/OnBoarding/GenerationWithImage.tsx
@@ -0,0 +1,11 @@
+import React from 'react'
+
+const GenerationWithImage = () => {
+ return (
+
+
+
+ )
+}
+
+export default GenerationWithImage
\ No newline at end of file
diff --git a/electron/servers/nextjs/components/OnBoarding/ModeSelectStep.tsx b/electron/servers/nextjs/components/OnBoarding/ModeSelectStep.tsx
new file mode 100644
index 00000000..a2e87062
--- /dev/null
+++ b/electron/servers/nextjs/components/OnBoarding/ModeSelectStep.tsx
@@ -0,0 +1,61 @@
+import { ChevronRight } from 'lucide-react'
+import React from 'react'
+
+const ModeSelectStep = ({ setStep, setSelectedMode }: { setStep: (step: number) => void, setSelectedMode: (mode: string) => void }) => {
+ return (
+
+
+
+
Let’s set up your AI workspace
+
First, choose the intelligence behind your presentation generation.
+
+
+
{
+ setSelectedMode("presenton")
+ setStep(2)
+ }} className='border font-syne border-[#EDEEEF] rounded-[11px] p-3 flex items-center justify-between gap-6 cursor-pointer'>
+
+
+

+
+
+
+
Optimized for fast, structured slide generation.
+
+
+
+
+
{
+ // setSelectedMode("image")
+ // setStep(2)
+ // }}
+ className='border font-syne border-[#EDEEEF] cursor-not-allowed rounded-[11px] p-3 flex items-center justify-between gap-6 relative'>
+
Coming soon
+
+
+
+

+
+
+
+
+
Generate with Image Model
+
+
+
Generate presentations with visual layouts and elements.
+
+
+
+
+
+
+ )
+}
+
+export default ModeSelectStep
diff --git a/electron/servers/nextjs/components/OnBoarding/OnBoardingHeader.tsx b/electron/servers/nextjs/components/OnBoarding/OnBoardingHeader.tsx
new file mode 100644
index 00000000..2639466e
--- /dev/null
+++ b/electron/servers/nextjs/components/OnBoarding/OnBoardingHeader.tsx
@@ -0,0 +1,36 @@
+import React from 'react'
+
+
+const OnBoardingHeader = ({ currentStep }: { currentStep: number }) => {
+ return (
+
+
+
+
+
+
+ 2
+
+
Choose Providers
+
+
+
+
+ )
+}
+
+export default OnBoardingHeader
diff --git a/electron/servers/nextjs/components/OnBoarding/OnBoardingSlidebar.tsx b/electron/servers/nextjs/components/OnBoarding/OnBoardingSlidebar.tsx
new file mode 100644
index 00000000..7e39b524
--- /dev/null
+++ b/electron/servers/nextjs/components/OnBoarding/OnBoardingSlidebar.tsx
@@ -0,0 +1,19 @@
+import React from 'react'
+
+const OnBoardingSlidebar = () => {
+ return (
+
+

+
+
+
+ )
+}
+
+export default OnBoardingSlidebar
diff --git a/electron/servers/nextjs/components/OnBoarding/PresentonMode.tsx b/electron/servers/nextjs/components/OnBoarding/PresentonMode.tsx
new file mode 100644
index 00000000..21784bbc
--- /dev/null
+++ b/electron/servers/nextjs/components/OnBoarding/PresentonMode.tsx
@@ -0,0 +1,940 @@
+import React, { useEffect, useMemo, useState } from 'react'
+import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
+import { Button } from '../ui/button';
+import { Check, CheckCircle, ChevronLeft, ChevronRight, ChevronUp, Download, Eye, EyeOff, Loader2 } from 'lucide-react';
+import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../ui/command';
+import { DALLE_3_QUALITY_OPTIONS, GPT_IMAGE_1_5_QUALITY_OPTIONS, IMAGE_PROVIDERS, LLM_PROVIDERS } from '@/utils/providerConstants';
+import { cn } from '@/lib/utils';
+import { LLMConfig } from '@/types/llm_config';
+import { RootState } from '@/store/store';
+import { useSelector } from 'react-redux';
+import { toast } from 'sonner';
+import ToolTip from '../ToolTip';
+import { Switch } from '../ui/switch';
+import { Select, SelectItem, SelectContent, SelectValue, SelectTrigger } from '../ui/select';
+import { MixpanelEvent, trackEvent } from '@/utils/mixpanel';
+import { usePathname, useRouter } from 'next/navigation';
+import { handleSaveLLMConfig } from '@/utils/storeHelpers';
+import { checkIfSelectedOllamaModelIsPulled, pullOllamaModel } from '@/utils/providerUtils';
+import { getApiUrl } from '@/utils/api';
+
+const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep: (step: number) => void }) => {
+ const pathname = usePathname();
+ const router = useRouter();
+ const [openProviderSelect, setOpenProviderSelect] = useState(false);
+ const [openImageProviderSelect, setOpenImageProviderSelect] = useState(false);
+ const userConfigState = useSelector((state: RootState) => state.userConfig);
+
+ const [showApiKey, setShowApiKey] = useState(false);
+ const [availableModels, setAvailableModels] = useState([]);
+ const [openModelSelect, setOpenModelSelect] = useState(false);
+ const [modelsLoading, setModelsLoading] = useState(false);
+ const [modelsChecked, setModelsChecked] = useState(false);
+ const [showDownloadModal, setShowDownloadModal] = useState(false);
+ const [savingConfig, setSavingConfig] = useState(false);
+ const [llmConfig, setLlmConfig] = useState(
+ userConfigState.llm_config
+ );
+ const [downloadingModel, setDownloadingModel] = useState<{
+ name: string;
+ size: number | null;
+ downloaded: number | null;
+ status: string;
+ done: boolean;
+ } | null>(null);
+
+ const handleProviderChange = (provider: string) => {
+
+ setLlmConfig(prev => ({
+ ...prev,
+ LLM: provider
+ }));
+ setOpenProviderSelect(false);
+ setAvailableModels([]);
+ setModelsChecked(false);
+ if (currentModelField) {
+ setLlmConfig(prev => ({
+ ...prev,
+ [currentModelField]: ''
+ }));
+ }
+ };
+
+ const currentModelField = useMemo(() => {
+ switch (llmConfig.LLM) {
+ case 'openai':
+ return 'OPENAI_MODEL';
+ case 'google':
+ return 'GOOGLE_MODEL';
+ case 'anthropic':
+ return 'ANTHROPIC_MODEL';
+ case 'ollama':
+ return 'OLLAMA_MODEL';
+ case 'custom':
+ return 'CUSTOM_MODEL';
+ default:
+ return '';
+ }
+ }, [llmConfig.LLM]);
+ const currentApiKeyField = useMemo(() => {
+ switch (llmConfig.LLM) {
+ case 'openai':
+ return 'OPENAI_API_KEY';
+ case 'google':
+ return 'GOOGLE_API_KEY';
+ case 'anthropic':
+ return 'ANTHROPIC_API_KEY';
+ case 'custom':
+ return 'CUSTOM_LLM_API_KEY';
+ default:
+ return '';
+ }
+ }, [llmConfig.LLM]);
+
+
+
+ const getFieldValue = (field?: string) => {
+ if (!field) return "";
+ return (llmConfig as Record)[field] || "";
+ };
+
+ const currentApiKey = currentApiKeyField ? ((llmConfig as Record)[currentApiKeyField] as string || '') : '';
+ const currentModel = currentModelField ? ((llmConfig as Record)[currentModelField] as string || '') : '';
+ const currentOllamaUrl = llmConfig.OLLAMA_URL || '';
+ const useCustomOllamaUrl = !!llmConfig.USE_CUSTOM_URL;
+
+ const fetchAvailableModels = async () => {
+ if (llmConfig.LLM === 'openai' && !currentApiKey) return;
+ if (llmConfig.LLM === 'google' && !currentApiKey) return;
+ if (llmConfig.LLM === 'anthropic' && !currentApiKey) return;
+ if (llmConfig.LLM === 'custom' && !llmConfig.CUSTOM_LLM_URL) return;
+
+ setModelsLoading(true);
+ try {
+ let response: Response;
+ if (llmConfig.LLM === 'google') {
+ response = await fetch(getApiUrl('/api/v1/ppt/google/models/available'), {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ api_key: currentApiKey
+ }),
+ });
+ } else if (llmConfig.LLM === 'anthropic') {
+ response = await fetch(getApiUrl('/api/v1/ppt/anthropic/models/available'), {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ api_key: currentApiKey
+ }),
+ });
+ } else if (llmConfig.LLM === 'ollama') {
+ response = await fetch(getApiUrl('/api/v1/ppt/ollama/models/supported'));
+ } else {
+ response = await fetch(getApiUrl('/api/v1/ppt/openai/models/available'), {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ url: llmConfig.LLM === 'custom' ? llmConfig.CUSTOM_LLM_URL : LLM_PROVIDERS[llmConfig.LLM!]?.url || '',
+ api_key: currentApiKey
+ }),
+ });
+ }
+
+ if (response.ok) {
+ const data = await response.json();
+ const normalizedModels: string[] = llmConfig.LLM === 'ollama'
+ ? Array.isArray(data)
+ ? data.map((model: { value?: string; label?: string }) => model.value || model.label || '').filter(Boolean)
+ : []
+ : Array.isArray(data)
+ ? data
+ : [];
+
+ setAvailableModels(normalizedModels);
+ setModelsChecked(true);
+
+ if (normalizedModels.length > 0 && currentModelField) {
+ if (llmConfig[currentModelField] && normalizedModels.includes(llmConfig[currentModelField])) {
+ setLlmConfig(prev => ({
+ ...prev,
+ [currentModelField]: llmConfig[currentModelField]
+ }));
+ return;
+ }
+
+ const preferredDefault =
+ llmConfig.LLM === 'openai'
+ ? 'gpt-4.1'
+ : llmConfig.LLM === 'google'
+ ? 'models/gemini-2.5-flash'
+ : llmConfig.LLM === 'anthropic'
+ ? 'claude-sonnet-4-20250514'
+ : normalizedModels[0];
+
+ const nextModel = normalizedModels.includes(preferredDefault) ? preferredDefault : normalizedModels[0];
+ setLlmConfig(prev => ({
+ ...prev,
+ [currentModelField]: nextModel
+ }));
+ }
+ } else {
+ console.error('Failed to fetch models');
+ setAvailableModels([]);
+ setModelsChecked(true);
+ toast.error(`Failed to fetch ${LLM_PROVIDERS[llmConfig.LLM!]?.label} models`);
+ }
+ } catch (error) {
+ console.error('Error fetching models:', error);
+ toast.error('Error fetching models');
+ setAvailableModels([]);
+ setModelsChecked(true);
+ } finally {
+ setModelsLoading(false);
+ }
+ };
+
+ const renderQualitySelector = (llmConfig: LLMConfig) => {
+ if (llmConfig.IMAGE_PROVIDER === "dall-e-3") {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ if (llmConfig.IMAGE_PROVIDER === "gpt-image-1.5") {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ return null;
+ };
+ const handleModelDownload = async () => {
+ try {
+ await pullOllamaModel(llmConfig.OLLAMA_MODEL!, setDownloadingModel);
+ }
+ finally {
+ setDownloadingModel(null);
+ setShowDownloadModal(false);
+ }
+ };
+
+
+ const handleSaveConfig = async () => {
+ trackEvent(MixpanelEvent.Home_SaveConfiguration_Button_Clicked, { pathname });
+ try {
+ setSavingConfig(true);
+ // 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");
+ // Track navigation from -> to
+ trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/final onboarding step" });
+ setStep(3)
+ // router.push("/upload");
+ } catch (error) {
+ toast.info(error instanceof Error ? error.message : "Failed to save configuration");
+
+ }
+ finally {
+ setSavingConfig(false);
+ }
+ };
+
+ 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]);
+
+ useEffect(() => {
+ if (llmConfig.LLM === 'ollama' && !modelsChecked && !modelsLoading) {
+ fetchAvailableModels();
+ }
+ }, [llmConfig.LLM, modelsChecked, modelsLoading]);
+
+ return (
+
+
PRESENTON
+
+
+
Choose your content providers
+
Select the AI engines that will generate your slide text and visuals.
+
+ {/* Text Provider */}
+
+
+
+
+
+
Text Generation Settings
+
+ Choosing where text contets come from
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No provider found.
+
+ {Object.values(LLM_PROVIDERS).map(
+ (provider, index) => (
+ handleProviderChange(provider.value)}
+ >
+
+
+
+
+
+ {provider.label}
+
+
+
+ {provider.description}
+
+
+
+
+ )
+ )}
+
+
+
+
+
+
+
+
+ {llmConfig.LLM === 'ollama' ? (
+ <>
+ {!useCustomOllamaUrl ? (
+
+ ) : (
+ <>
+
+
+ setLlmConfig(prev => ({
+ ...prev,
+ OLLAMA_URL: e.target.value
+ }))}
+ className="w-full px-2 py-3 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
+ placeholder="http://localhost:11434"
+ />
+
+
+ >
+ )}
+ >
+ ) : (
+ <>
+
+
+ setLlmConfig(prev => ({
+ ...prev,
+ [currentApiKeyField]: e.target.value
+ }))}
+ className="w-full px-2 py-3 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
+ placeholder={`Enter your ${llmConfig.LLM} API key`}
+ />
+
+
+ >
+ )}
+ {llmConfig.LLM === 'custom' && (
+
setLlmConfig(prev => ({
+ ...prev,
+ CUSTOM_LLM_URL: e.target.value
+ }))}
+ className="w-full mt-2 px-2 py-3 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
+ placeholder="OpenAI-compatible URL"
+ />
+ )}
+
+
+
+
+
+ {llmConfig.LLM !== 'ollama' && (!modelsChecked || (modelsChecked && availableModels.length === 0)) && (
+
+
+ )}
+
+
+
+
+
+
+ {/* Model Selection - only show if models are available */}
+ {modelsChecked && availableModels.length > 0 && (
+
+
+
+
+
+
+
+
+
+
+
+
+ No model found.
+
+ {availableModels.map((model, index) => (
+ {
+ if (currentModelField) {
+ setLlmConfig(prev => ({
+ ...prev,
+ [currentModelField]: value
+ }));
+ }
+ setOpenModelSelect(false);
+ }}
+ >
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ )}
+
+
+ {/* Image Provider */}
+
+
+
+ setLlmConfig(prev => ({
+ ...prev,
+ DISABLE_IMAGE_GENERATION: !checked
+ }))}
+ />
+
+
+
+
+
+

+
+
+
+
Image Generation Settings
+
+ Choosing where images come from
+
+
+
+ {!llmConfig.DISABLE_IMAGE_GENERATION && (
+
+ {/* Image Provider Selection */}
+
+
+
+
+
+
+
+
+
+
+
+ No provider found.
+
+ {Object.values(IMAGE_PROVIDERS).map(
+ (provider, index) => (
+ {
+ setLlmConfig(prev => ({
+ ...prev,
+ IMAGE_PROVIDER: value
+ }));
+ setOpenImageProviderSelect(false);
+ }}
+ >
+
+
+
+
+
+ {provider.label}
+
+
+
+ {provider.description}
+
+
+
+
+ )
+ )}
+
+
+
+
+
+
+
+
+
+
+ {/* Dynamic API Key Input for Image Provider */}
+ {llmConfig.IMAGE_PROVIDER &&
+ IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER] &&
+ (() => {
+ const provider = IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER];
+
+
+
+ // Show ComfyUI configuration
+ if (provider.value === "comfyui") {
+ return (
+
+
+
+
+ {
+ setLlmConfig(prev => ({
+ ...prev,
+ COMFYUI_URL: e.target.value
+ }));
+ }}
+ />
+
+
+
+
+
+ );
+ }
+
+ // Show API key input for other providers
+ return (
+
+
+
+ {
+ setLlmConfig((prev) => ({
+ ...prev,
+ [provider.apiKeyField as keyof LLMConfig]: e.target.value
+ }))
+ }
+
+ }
+ />
+
+
+
+
+ );
+ })()}
+
+
+ )}
+ {!llmConfig.DISABLE_IMAGE_GENERATION &&
+
+
+ {renderQualitySelector(llmConfig)}
+
+ {llmConfig.IMAGE_PROVIDER === "comfyui" &&
+
+
+
+
+
}
+
}
+
+
+
+
+
+
+ {/* Download Progress Modal */}
+ {showDownloadModal && downloadingModel && (
+
+
+ {/* Modal Content */}
+
+ {/* Icon */}
+
+ {downloadingModel.done ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Title */}
+
+ {downloadingModel.done ? "Download Complete!" : "Downloading Model"}
+
+
+ {/* Model Name */}
+
+ {llmConfig.OLLAMA_MODEL}
+
+
+ {/* Progress Bar */}
+ {downloadProgress > 0 && (
+
+
+
+ {downloadProgress}% Complete
+
+
+ )}
+
+ {/* Status */}
+ {downloadingModel.status && (
+
+
+
+ {downloadingModel.status}
+
+
+ )}
+
+ {/* Status Message */}
+ {downloadingModel.status && downloadingModel.status !== "pulled" && (
+
+ {downloadingModel.status === "downloading" && "Downloading model files..."}
+ {downloadingModel.status === "verifying" && "Verifying model integrity..."}
+ {downloadingModel.status === "pulling" && "Pulling model from registry..."}
+
+ )}
+
+ {/* Download Info */}
+ {downloadingModel.downloaded && downloadingModel.size && (
+
+
+ Downloaded: {(downloadingModel.downloaded / 1024 / 1024).toFixed(1)} MB
+ Total: {(downloadingModel.size / 1024 / 1024).toFixed(1)} MB
+
+
+ )}
+
+
+
+ )}
+
+ )
+}
+
+export default PresentonMode
diff --git a/electron/servers/nextjs/components/OpenAIConfig.tsx b/electron/servers/nextjs/components/OpenAIConfig.tsx
index c5f61f79..2968820d 100644
--- a/electron/servers/nextjs/components/OpenAIConfig.tsx
+++ b/electron/servers/nextjs/components/OpenAIConfig.tsx
@@ -14,6 +14,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import { Switch } from "./ui/switch";
+import { LLMConfig } from "@/types/llm_config";
import { getApiUrl } from "@/utils/api";
interface OpenAIConfigProps {
@@ -21,19 +22,22 @@ interface OpenAIConfigProps {
openaiModel: string;
webGrounding?: boolean;
onInputChange: (value: string | boolean, field: string) => void;
+ llmConfig: LLMConfig;
}
export default function OpenAIConfig({
openaiApiKey,
openaiModel,
webGrounding,
- onInputChange
+ onInputChange,
+ llmConfig
}: OpenAIConfigProps) {
const [openModelSelect, setOpenModelSelect] = useState(false);
const [availableModels, setAvailableModels] = useState([]);
- const [modelsLoading, setModelsLoading] = useState(false);
- const [modelsChecked, setModelsChecked] = useState(false);
- const [apiKey, setApiKey] = useState(openaiApiKey);
+const [modelsLoading, setModelsLoading] = useState(false);
+const [modelsChecked, setModelsChecked] = useState(false);
+const [apiKey, setApiKey] = useState(openaiApiKey);
+const isImageGenerationDisabled = llmConfig?.DISABLE_IMAGE_GENERATION ?? false;
const openaiUrl = "https://api.openai.com/v1";
@@ -53,7 +57,7 @@ export default function OpenAIConfig({
setModelsLoading(true);
try {
- const response = await fetch(getApiUrl('api/v1/ppt/openai/models/available'), {
+ const response = await fetch(getApiUrl('/api/v1/ppt/openai/models/available'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -85,152 +89,189 @@ export default function OpenAIConfig({
};
return (
-
+
{/* API Key Input */}
-
-
-
- onApiKeyChange(e.target.value)}
- className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
- placeholder="Enter your API key"
- />
-
-
-
- Your API key will be stored locally and never shared
-
-
+
+
-
-
- {/* Check for available models button - show when no models checked or no models found */}
- {(!modelsChecked || (modelsChecked && availableModels.length === 0)) && (
-
-
-
- )}
-
- {/* Show message if no models found */}
- {modelsChecked && availableModels.length === 0 && (
-
-
- No models found. Please make sure your API key is valid and has access to OpenAI models.
+
OpenAI API key
+
+ Your API key will be stored locally and never shared
- )}
+
- {/* Model Selection - only show if models are available */}
- {modelsChecked && availableModels.length > 0 ? (
-
-
-
-
-
-
-
-
+
+
+
+ onApiKeyChange(e.target.value)}
+ className="w-full px-2 py-3 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
+ placeholder="Enter your API key"
+ />
+
+
+ {/* Check for available models button - show when no models checked or no models found */}
+
+ {(!modelsChecked || (modelsChecked && availableModels.length === 0)) && (
+
+
-
+ {modelsLoading ? (
+
+
+ Checking for models...
+
+ ) : (
+ "Check for available models"
+ )}
+
+
+ )}
+
+
+ {/* Show message if no models found */}
+ {modelsChecked && availableModels.length === 0 && (
+
+
+ No models found. Please make sure your API key is valid and has access to OpenAI models.
+
+
+ )}
+
+ {/* Model Selection - only show if models are available */}
+ {modelsChecked && availableModels.length > 0 ? (
+
+
+
+
+
+
+
+
+
+
+
+ No model found.
+
+ {availableModels.map((model, index) => (
+ {
+ onInputChange(value, "openai_model");
+ setOpenModelSelect(false);
+ }}
+ >
+
+
+
+ ))}
+
+
+
+
+
+
+
+ ) : null}
- ) : null}
+
+
+
+
+
+
{/* Web Grounding Toggle - show at the end, below models dropdown */}
-
-
-
-
onInputChange(checked, "web_grounding")}
- />
+
+
+
Model Controls
+
+
+ Configure web access, image generation, and advanced AI features.
+
-
-
- If enabled, the model can use web search grounding when available.
-
+
+
+
+
+ onInputChange(checked, "web_grounding")}
+ />
+
+
+
+ onInputChange(checked, "disable_image_generation")}
+ />
+
+
+
+
+
+
+
+
+
);
}
\ No newline at end of file
diff --git a/electron/servers/nextjs/components/ToolTip.tsx b/electron/servers/nextjs/components/ToolTip.tsx
index e1b6c59a..2e84a721 100644
--- a/electron/servers/nextjs/components/ToolTip.tsx
+++ b/electron/servers/nextjs/components/ToolTip.tsx
@@ -3,9 +3,9 @@ import { TooltipProvider } from '@radix-ui/react-tooltip'
import React from 'react'
import { TooltipContent, TooltipTrigger, } from './ui/tooltip'
-const ToolTip = ({ children, content }: { children: React.ReactNode, content: string }) => {
+const ToolTip = ({ children, content, className }: { children: React.ReactNode, content: string, className?: string }) => {
return (
-
+
diff --git a/electron/servers/nextjs/components/Wrapper.tsx b/electron/servers/nextjs/components/Wrapper.tsx
index 9e042e4f..9782ead5 100644
--- a/electron/servers/nextjs/components/Wrapper.tsx
+++ b/electron/servers/nextjs/components/Wrapper.tsx
@@ -7,5 +7,5 @@ interface WrapperProps {
}
export default function Wrapper({ children, className }: WrapperProps) {
- return {children}
;
+ return {children}
;
}
diff --git a/electron/servers/nextjs/next.config.mjs b/electron/servers/nextjs/next.config.mjs
index 2616fc62..379115b3 100644
--- a/electron/servers/nextjs/next.config.mjs
+++ b/electron/servers/nextjs/next.config.mjs
@@ -6,7 +6,16 @@ const nextConfig = {
// This Next.js app is always bundled for Electron, so we can
// unconditionally use static export.
output: "export",
- ...(isDevelopment ? { allowedDevOrigins: ['127.0.0.1:*', 'localhost:*'] } : {}),
+ ...(isDevelopment
+ ? {
+ allowedDevOrigins: [
+ "http://127.0.0.1:40001",
+ "http://localhost:40001",
+ "127.0.0.1",
+ "localhost",
+ ],
+ }
+ : {}),
// Disable font optimization to avoid Google Fonts download warnings during build
optimizeFonts: false,
diff --git a/electron/servers/nextjs/package-lock.json b/electron/servers/nextjs/package-lock.json
index 54604588..1de09b41 100644
--- a/electron/servers/nextjs/package-lock.json
+++ b/electron/servers/nextjs/package-lock.json
@@ -51,6 +51,7 @@
"prismjs": "^1.30.0",
"puppeteer": "^24.13.0",
"react": "^18.3.1",
+ "react-colorful": "^5.6.1",
"react-dom": "^18.3.1",
"react-redux": "^9.1.2",
"react-simple-code-editor": "^0.14.1",
@@ -8117,6 +8118,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-colorful": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
+ "integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
diff --git a/electron/servers/nextjs/package.json b/electron/servers/nextjs/package.json
index f8a7d656..28d5797d 100644
--- a/electron/servers/nextjs/package.json
+++ b/electron/servers/nextjs/package.json
@@ -53,6 +53,7 @@
"prismjs": "^1.30.0",
"puppeteer": "^24.13.0",
"react": "^18.3.1",
+ "react-colorful": "^5.6.1",
"react-dom": "^18.3.1",
"react-redux": "^9.1.2",
"react-simple-code-editor": "^0.14.1",
diff --git a/electron/servers/nextjs/public/card_bg.svg b/electron/servers/nextjs/public/card_bg.svg
new file mode 100644
index 00000000..ab159058
--- /dev/null
+++ b/electron/servers/nextjs/public/card_bg.svg
@@ -0,0 +1,5311 @@
+
diff --git a/electron/servers/nextjs/public/create_presentation.png b/electron/servers/nextjs/public/create_presentation.png
new file mode 100644
index 00000000..2acb9f9f
Binary files /dev/null and b/electron/servers/nextjs/public/create_presentation.png differ
diff --git a/electron/servers/nextjs/public/final_onboarding.png b/electron/servers/nextjs/public/final_onboarding.png
new file mode 100644
index 00000000..e8a0c914
Binary files /dev/null and b/electron/servers/nextjs/public/final_onboarding.png differ
diff --git a/electron/servers/nextjs/public/image-markup.svg b/electron/servers/nextjs/public/image-markup.svg
new file mode 100644
index 00000000..a1270df9
--- /dev/null
+++ b/electron/servers/nextjs/public/image-markup.svg
@@ -0,0 +1,9 @@
+
diff --git a/electron/servers/nextjs/public/image_mode.png b/electron/servers/nextjs/public/image_mode.png
new file mode 100644
index 00000000..bca8ca22
Binary files /dev/null and b/electron/servers/nextjs/public/image_mode.png differ
diff --git a/electron/servers/nextjs/public/logo-with-bg.png b/electron/servers/nextjs/public/logo-with-bg.png
new file mode 100644
index 00000000..faf3a947
Binary files /dev/null and b/electron/servers/nextjs/public/logo-with-bg.png differ
diff --git a/electron/servers/nextjs/public/providers/image-provider.png b/electron/servers/nextjs/public/providers/image-provider.png
new file mode 100644
index 00000000..c27e0591
Binary files /dev/null and b/electron/servers/nextjs/public/providers/image-provider.png differ
diff --git a/electron/servers/nextjs/public/providers/openai.png b/electron/servers/nextjs/public/providers/openai.png
new file mode 100644
index 00000000..4146ce93
Binary files /dev/null and b/electron/servers/nextjs/public/providers/openai.png differ
diff --git a/electron/servers/nextjs/store/slices/presentationGeneration.ts b/electron/servers/nextjs/store/slices/presentationGeneration.ts
index 9c10e3dd..788828d5 100644
--- a/electron/servers/nextjs/store/slices/presentationGeneration.ts
+++ b/electron/servers/nextjs/store/slices/presentationGeneration.ts
@@ -1,3 +1,4 @@
+import { Theme } from "@/app/(presentation-generator)/services/api/types";
import { Slide } from "@/app/(presentation-generator)/types/slide";
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
@@ -12,6 +13,7 @@ export interface PresentationData {
n_slides: number;
title: string;
slides: any;
+ theme: Theme | null;
}
interface PresentationGenerationState {
@@ -377,7 +379,13 @@ const presentationGenerationSlice = createSlice({
}
}
},
+ updateTheme: (state, action: PayloadAction) => {
+ if (state.presentationData) {
+ state.presentationData['theme'] = action.payload;
+ }
+ },
},
+
});
export const {
@@ -401,6 +409,7 @@ export const {
updateImageProperties,
updateSlideIcon,
addNewSlide,
+ updateTheme,
} = presentationGenerationSlice.actions;
export default presentationGenerationSlice.reducer;
diff --git a/electron/servers/nextjs/store/slices/userConfig.ts b/electron/servers/nextjs/store/slices/userConfig.ts
index dc8ec3c9..04815529 100644
--- a/electron/servers/nextjs/store/slices/userConfig.ts
+++ b/electron/servers/nextjs/store/slices/userConfig.ts
@@ -7,7 +7,11 @@ interface UserConfigState {
}
const initialState: UserConfigState = {
- llm_config: {},
+ llm_config: {
+ LLM: "openai",
+ IMAGE_PROVIDER: "gpt-image-1.5",
+
+ },
can_change_keys: false,
}
diff --git a/electron/servers/nextjs/tailwind.config.ts b/electron/servers/nextjs/tailwind.config.ts
index f555dbb6..4ac5d559 100644
--- a/electron/servers/nextjs/tailwind.config.ts
+++ b/electron/servers/nextjs/tailwind.config.ts
@@ -82,9 +82,9 @@ const config: Config = {
"accordion-up": "accordion-up 0.2s ease-out",
},
fontFamily: {
- instrument_sans: ["var(--font-instrument-sans)"],
+ syne: ["var(--font-syne)"],
+ unbounded: ["var(--font-unbounded)"],
inter: ["var(--font-inter)"],
- roboto: ["var(--font-roboto)"],
},
},
},
diff --git a/electron/servers/nextjs/utils/api.ts b/electron/servers/nextjs/utils/api.ts
index 94259819..6f8c3c6d 100644
--- a/electron/servers/nextjs/utils/api.ts
+++ b/electron/servers/nextjs/utils/api.ts
@@ -14,10 +14,32 @@ export function getFastAPIUrl(): string {
return "http://127.0.0.1:8000";
}
-// Utility to construct full API URL
+function isAbsoluteHttpUrl(path: string): boolean {
+ return /^https?:\/\//i.test(path);
+}
+
+function withLeadingSlash(path: string): string {
+ return path.startsWith("/") ? path : `/${path}`;
+}
+
+function isElectronRuntime(): boolean {
+ return typeof window !== "undefined" && !!(window as any).electron;
+}
+
+// Utility to construct API URL that works in both web and Electron.
export function getApiUrl(path: string): string {
- const baseUrl = getFastAPIUrl();
- // Remove leading slash if present to avoid double slashes
- const cleanPath = path.startsWith('/') ? path.slice(1) : path;
- return `${baseUrl}/${cleanPath}`;
+ if (isAbsoluteHttpUrl(path)) {
+ return path;
+ }
+
+ const normalizedPath = withLeadingSlash(path);
+ const isFastApiEndpoint = normalizedPath.startsWith("/api/v1/");
+
+ // In web/docker, /api/v1 is typically reverse-proxied by the web server.
+ // In Electron, Next and FastAPI run on different ports, so use FastAPI base URL.
+ if (isFastApiEndpoint && (isElectronRuntime() || !!process.env.NEXT_PUBLIC_FAST_API)) {
+ return `${getFastAPIUrl()}${normalizedPath}`;
+ }
+
+ return normalizedPath;
}
\ No newline at end of file
diff --git a/electron/servers/nextjs/utils/pptx_models_utils.ts b/electron/servers/nextjs/utils/pptx_models_utils.ts
index afc6c62e..7cae718c 100644
--- a/electron/servers/nextjs/utils/pptx_models_utils.ts
+++ b/electron/servers/nextjs/utils/pptx_models_utils.ts
@@ -200,7 +200,7 @@ function convertToAutoShapeBox(element: ElementAttributes): PptxAutoShapeBoxMode
shadow,
position,
text_wrap: element.textWrap ?? true,
- border_radius: borderRadius ? Math.round(borderRadius) : undefined,
+ border_radius: borderRadius || undefined,
paragraphs
};
}
diff --git a/electron/servers/nextjs/utils/providerConstants.ts b/electron/servers/nextjs/utils/providerConstants.ts
index 9b34e093..1b1756ed 100644
--- a/electron/servers/nextjs/utils/providerConstants.ts
+++ b/electron/servers/nextjs/utils/providerConstants.ts
@@ -22,6 +22,8 @@ export interface LLMProviderOption {
description?: string;
model_value?: string;
model_label?: string;
+ url?: string;
+ icon?: string;
}
export const IMAGE_PROVIDERS: Record = {
@@ -95,30 +97,70 @@ export const LLM_PROVIDERS: Record = {
value: "openai",
label: "OpenAI",
description: "OpenAI's latest text generation model",
+ url: "https://api.openai.com/v1",
+ icon: "/icons/openai.png",
},
google: {
value: "google",
label: "Google",
description: "Google's primary text generation model",
+ url: "https://api.google.com/v1",
+ icon: "/icons/google.png",
},
anthropic: {
value: "anthropic",
label: "Anthropic",
description: "Anthropic's Claude models",
+ url: "https://api.anthropic.com/v1",
+ icon: "/icons/anthropic.png",
},
ollama: {
value: "ollama",
label: "Ollama",
description: "Ollama's primary text generation model",
+ icon: "/icons/ollama.png",
},
custom: {
value: "custom",
label: "Custom",
description: "Custom LLM",
+ icon: "/icons/custom.png",
},
codex: {
value: "codex",
label: "ChatGPT",
description: "ChatGPT Plus/Pro via OAuth",
+ icon: "/icons/chatgpt.png",
},
};
+
+export const DALLE_3_QUALITY_OPTIONS = [
+ {
+ label: "Standard",
+ value: "standard",
+ description: "Faster generation with lower cost",
+ },
+ {
+ label: "HD",
+ value: "hd",
+ description: "Higher quality images with increased cost",
+ },
+];
+
+export const GPT_IMAGE_1_5_QUALITY_OPTIONS = [
+ {
+ label: "Low",
+ value: "low",
+ description: "Fastest and most cost-effective",
+ },
+ {
+ label: "Medium",
+ value: "medium",
+ description: "Balanced quality and speed",
+ },
+ {
+ label: "High",
+ value: "high",
+ description: "Best quality with longer generation time",
+ },
+];
\ No newline at end of file
diff --git a/electron/servers/nextjs/utils/providerUtils.ts b/electron/servers/nextjs/utils/providerUtils.ts
index b52e6d9d..2a5f6062 100644
--- a/electron/servers/nextjs/utils/providerUtils.ts
+++ b/electron/servers/nextjs/utils/providerUtils.ts
@@ -1,5 +1,5 @@
import { LLMConfig } from "@/types/llm_config";
-import { getApiUrl } from "./api";
+import { getApiUrl } from "@/utils/api";
export interface OllamaModel {
label: string;
@@ -88,13 +88,7 @@ export const changeProvider = (
export const checkIfSelectedOllamaModelIsPulled = async (ollamaModel: string) => {
try {
- const response = await fetch(getApiUrl('api/v1/ppt/ollama/models/available'));
-
- if (!response.ok) {
- console.error('Ollama model check failed with status:', response.status);
- return false;
- }
-
+ const response = await fetch(getApiUrl('/api/v1/ppt/ollama/models/available'));
const models = await response.json();
const pulledModels = models.map((model: any) => model.name);
return pulledModels.includes(ollamaModel);
@@ -128,7 +122,7 @@ export const pullOllamaModel = async (
const interval = setInterval(async () => {
try {
const response = await fetch(
- getApiUrl(`api/v1/ppt/ollama/model/pull?model=${model}`)
+ getApiUrl(`/api/v1/ppt/ollama/model/pull?model=${model}`)
);
if (response.status === 200) {
const data = await response.json();
diff --git a/package-lock.json b/package-lock.json
index a92e236e..e6ea511c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,26 +7,49 @@
"": {
"name": "presenton",
"version": "1.0.0",
- "devDependencies": {
- "@types/node": "^25.3.5"
- }
- },
- "node_modules/@types/node": {
- "version": "25.3.5",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz",
- "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==",
- "dev": true,
- "license": "MIT",
"dependencies": {
- "undici-types": "~7.18.0"
+ "react-colorful": "^5.6.1"
}
},
- "node_modules/undici-types": {
- "version": "7.18.2",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
- "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
- "dev": true,
- "license": "MIT"
+ "node_modules/react": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
+ "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-colorful": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
+ "integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
+ "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.4"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT",
+ "peer": true
}
}
}