refactor: Improve Mix panel events

This commit is contained in:
shiva raj badu 2026-04-16 12:59:14 +05:45
parent 5018c9b9f1
commit cfc7233447
No known key found for this signature in database
23 changed files with 502 additions and 181 deletions

0
servers/nextjs/.codex Normal file
View file

View file

@ -7,16 +7,17 @@ import { PresentationGrid } from "@/app/(presentation-generator)/(dashboard)/das
import Link from "next/link";
import { ChevronRight } from "lucide-react";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { usePathname } from "next/navigation";
const DashboardPage: React.FC = () => {
const pathname = usePathname();
const [presentations, setPresentations] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
trackEvent(MixpanelEvent.Dashboard_Page_Viewed);
const loadData = async () => {
await fetchPresentations();
};
@ -24,19 +25,28 @@ const DashboardPage: React.FC = () => {
}, []);
const fetchPresentations = async () => {
let fetchedCount = 0;
let hasError = false;
try {
setIsLoading(true);
setError(null);
const data = await DashboardApi.getPresentations();
fetchedCount = data.length;
data.sort(
(a: any, b: any) =>
new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
);
setPresentations(data);
} catch (err) {
hasError = true;
setError(null);
setPresentations([]);
} finally {
trackEvent(MixpanelEvent.Dashboard_Page_Viewed, {
pathname,
presentation_count: fetchedCount,
load_failed: hasError,
});
setIsLoading(false);
}
};
@ -61,7 +71,7 @@ const DashboardPage: React.FC = () => {
<Link
href="/upload"
onClick={() => trackEvent(MixpanelEvent.Dashboard_New_Presentation_Clicked)}
onClick={() => trackEvent(MixpanelEvent.Dashboard_New_Presentation_Clicked, { pathname, source: "dashboard_header" })}
className="inline-flex items-center gap-2 rounded-xl px-4 py-2.5 text-black text-sm font-semibold font-syne shadow-sm hover:shadow-md"
aria-label="Create new presentation"
style={{

View file

@ -9,7 +9,7 @@ import {
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
import { useRouter } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import { toast } from "sonner";
import { useFontLoader } from "@/app/(presentation-generator)/hooks/useFontLoad";
@ -29,9 +29,16 @@ export const PresentationCard = ({
onDeleted?: (presentationId: string) => void;
}) => {
const router = useRouter();
const pathname = usePathname();
const handlePreview = (e: React.MouseEvent) => {
e.preventDefault();
trackEvent(MixpanelEvent.Dashboard_Presentation_Opened, {
pathname,
presentation_id: id,
title_length: (title || "").length,
slide_count: presentation?.slides?.length || 0,
});
router.push(`/presentation?id=${id}&type=standard`);
};
useEffect(() => {
@ -84,6 +91,11 @@ export const PresentationCard = ({
const response = await DashboardApi.deletePresentation(id);
if (response) {
trackEvent(MixpanelEvent.Dashboard_Presentation_Deleted, {
pathname,
presentation_id: id,
slide_count: presentation?.slides?.length || 0,
});
toast.success("Presentation deleted", {
description: "The presentation has been deleted successfully",
});

View file

@ -1,7 +1,7 @@
import React from "react";
import { PresentationCard } from "./PresentationCard";
import { PlusIcon } from "@radix-ui/react-icons";
import { useRouter } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import { PresentationResponse } from "@/app/(presentation-generator)/services/api/dashboard";
import { ArrowRight } from "lucide-react";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
@ -22,8 +22,13 @@ export const PresentationGrid = ({
onPresentationDeleted,
}: PresentationGridProps) => {
const router = useRouter();
const pathname = usePathname();
const handleCreateNewPresentation = () => {
trackEvent(MixpanelEvent.Dashboard_Create_New_Card_Clicked, { type });
trackEvent(MixpanelEvent.Dashboard_Create_New_Card_Clicked, {
pathname,
type,
source: "dashboard_grid_card",
});
if (type === "slide") {
router.push("/upload");
} else {

View file

@ -27,11 +27,12 @@ import { Theme, ThemeParams } from '@/app/(presentation-generator)/services/api/
import { ImagesApi } from '@/app/(presentation-generator)/services/api/images'
import { Input } from '@/components/ui/input'
import { getTemplatesByTemplateName } from '@/app/presentation-templates'
import { useSearchParams } from 'next/navigation'
import { usePathname, useSearchParams } from 'next/navigation'
import CustomTabEmpty from './CustomTabEmpty'
import ThemeApi from '@/app/(presentation-generator)/services/api/theme'
import { useFontLoader } from '@/app/(presentation-generator)/hooks/useFontLoad'
import Link from 'next/link'
import { MixpanelEvent, trackEvent } from '@/utils/mixpanel'
// Fallback theme used before defaults are loaded from API (unified Theme type)
const FALLBACK_THEME: Theme = {
@ -67,6 +68,7 @@ const FALLBACK_THEME: Theme = {
}
const ThemePanel: React.FC = () => {
const searchParams = useSearchParams()
const pathname = usePathname()
const [selectedTheme, setSelectedTheme] = useState<Theme>(FALLBACK_THEME)
@ -91,6 +93,10 @@ const ThemePanel: React.FC = () => {
const slideContainerRef = useRef<HTMLDivElement>(null)
const [slideContainerWidth, setSlideContainerWidth] = useState<number>(0)
useEffect(() => {
trackEvent(MixpanelEvent.Theme_Page_Viewed, { pathname })
}, [pathname])
// Calculate scale dynamically based on container width
const slideScale = () => {
const BASE_WIDTH = 1280
@ -258,6 +264,18 @@ const ThemePanel: React.FC = () => {
setThemeCompanyName(theme.company_name || '')
applyTheme(theme)
trackEvent(MixpanelEvent.Theme_Selected, {
pathname,
theme_id: theme.id,
theme_name: theme.name,
theme_source: theme.user === 'system' ? 'built_in' : 'custom',
})
trackEvent(MixpanelEvent.Theme_Editor_Opened, {
pathname,
theme_id: theme.id,
theme_name: theme.name,
theme_source: theme.user === 'system' ? 'built_in' : 'custom',
})
}
const handleColorChange = (colorKey: keyof ThemeColors, value: string) => {
@ -272,6 +290,12 @@ const ThemePanel: React.FC = () => {
const handleFontSelect = (fontName: string, url: string) => {
setCustomFonts({ textFont: { name: fontName, url: url } })
trackEvent(MixpanelEvent.Theme_Font_Changed, {
pathname,
font_name: fontName,
font_url: url,
theme_id: selectedTheme.id,
})
}
const handleBrandLogoUpload = async (file: File) => {
@ -280,6 +304,12 @@ const ThemePanel: React.FC = () => {
const uploaded = await ImagesApi.uploadImage(file)
setCustomBrandLogo(uploaded.path)
setCustomBrandLogoId(uploaded.id)
trackEvent(MixpanelEvent.Theme_Logo_Uploaded, {
pathname,
theme_id: selectedTheme.id,
file_name: file.name,
file_size_bytes: file.size,
})
} catch (error: any) {
console.error('Failed to upload logo', error)
toast.error(error?.message || 'Failed to upload logo')
@ -288,8 +318,19 @@ const ThemePanel: React.FC = () => {
}
}
const generateTheme = async ({ primary, background }: { primary?: string, background?: string }): Promise<ThemeColors> => {
const generateTheme = async ({
primary,
background,
source,
}: { primary?: string, background?: string; source: "new_theme" | "refresh" }): Promise<ThemeColors> => {
const generatedTheme = await ThemeApi.generateTheme({ primary, background })
trackEvent(MixpanelEvent.Theme_Palette_Generated, {
pathname,
source,
theme_id: selectedTheme.id,
has_primary_seed: Boolean(primary),
has_background_seed: Boolean(background),
})
return {
'primary': generatedTheme.primary,
'background': generatedTheme.background,
@ -311,6 +352,7 @@ const ThemePanel: React.FC = () => {
}
const createNewCustomTheme = async () => {
trackEvent(MixpanelEvent.Theme_New_Theme_Clicked, { pathname })
setIsNewTheme(true)
const newTheme: Theme = {
id: `custom-${Date.now()}`,
@ -345,7 +387,7 @@ const ThemePanel: React.FC = () => {
}
}
const generatedColors = await generateTheme({})
const generatedColors = await generateTheme({ source: "new_theme" })
const theme = {
@ -365,10 +407,16 @@ const ThemePanel: React.FC = () => {
setThemeCompanyName('')
applyTheme(theme)
trackEvent(MixpanelEvent.Theme_Editor_Opened, {
pathname,
theme_id: theme.id,
theme_name: theme.name,
theme_source: "new_draft",
})
}
const refeshTheme = async ({ primary, background }: { primary?: string, background?: string }) => {
const generatedTheme = await generateTheme({ primary, background })
const generatedTheme = await generateTheme({ primary, background, source: "refresh" })
setCustomColors(generatedTheme)
}
const saveAsCustom = async () => {
@ -376,6 +424,12 @@ const ThemePanel: React.FC = () => {
if (selectedTheme.user && selectedTheme.user !== 'system' && !selectedTheme.id.startsWith('custom-')) {
; (async () => {
try {
trackEvent(MixpanelEvent.Theme_Save_Started, {
pathname,
mode: "update",
theme_id: selectedTheme.id,
theme_name: selectedTheme.name,
})
const params: ThemeParams = {
id: selectedTheme.id,
name: selectedTheme.name,
@ -392,6 +446,14 @@ const ThemePanel: React.FC = () => {
setCustomThemes(customThemes.map(t => t.id === updated.id ? updated : t))
setSelectedTheme(updated)
setIsSheetOpen(false)
trackEvent(MixpanelEvent.Theme_Saved, {
pathname,
mode: "update",
theme_id: updated.id,
theme_name: updated.name,
has_logo: Boolean(updated.logo_url),
font_name: updated.data?.fonts?.textFont?.name || "",
})
toast.success('Theme updated')
} catch (error: any) {
console.error('Failed to update theme', error)
@ -401,6 +463,12 @@ const ThemePanel: React.FC = () => {
return
}
try {
trackEvent(MixpanelEvent.Theme_Save_Started, {
pathname,
mode: "create",
theme_id: selectedTheme.id,
theme_name: selectedTheme.name,
})
const params: ThemeParams = {
name: selectedTheme.name,
description: selectedTheme.description || `Custom version of ${selectedTheme.name}`,
@ -419,6 +487,14 @@ const ThemePanel: React.FC = () => {
window.history.pushState({}, '', '/theme')
trackEvent(MixpanelEvent.Theme_Saved, {
pathname,
mode: "create",
theme_id: created.id,
theme_name: created.name,
has_logo: Boolean(created.logo_url),
font_name: created.data?.fonts?.textFont?.name || "",
})
toast.success('Theme saved')
} catch (error: any) {
console.error('Failed to save theme', error)
@ -432,6 +508,10 @@ const ThemePanel: React.FC = () => {
const handleDelete = async (themeId: string) => {
await ThemeApi.deleteTheme(themeId)
setCustomThemes(customThemes.filter(theme => theme.id !== themeId))
trackEvent(MixpanelEvent.Theme_Deleted, {
pathname,
theme_id: themeId,
})
toast.success("Theme deleted successfully")
}
const handleCustomFontChange = async (fontFile: File) => {
@ -444,6 +524,19 @@ const ThemePanel: React.FC = () => {
url: url,
}
})
trackEvent(MixpanelEvent.Theme_Custom_Font_Uploaded, {
pathname,
font_name: name,
file_name: fontFile.name,
file_size_bytes: fontFile.size,
})
trackEvent(MixpanelEvent.Theme_Font_Changed, {
pathname,
theme_id: selectedTheme.id,
font_name: name,
font_url: url,
source: "uploaded_font",
})
// Add the newly uploaded font to userFonts if not already present
if (!userFonts.fonts.find(f => f.name === name)) {
setUserFonts(prev => ({
@ -853,6 +946,10 @@ const ThemePanel: React.FC = () => {
</h3>
<Link
href="/theme?tab=new-theme"
onClick={() => trackEvent(MixpanelEvent.Theme_New_Theme_Clicked, {
pathname,
source: "theme_page_header",
})}
className="inline-flex items-center gap-2 rounded-xl px-4 py-2.5 text-black text-sm font-semibold font-syne shadow-sm hover:shadow-md"
aria-label="Create new theme"
style={{
@ -869,7 +966,10 @@ const ThemePanel: React.FC = () => {
{/* Tabs */}
<div className='p-1 rounded-[40px] bg-[#F7F6F9] w-fit border border-[#F4F4F4] flex items-center justify-center '>
<button className='px-5 py-2 text-xs font-medium text-[#3A3A3A] rounded-[70px]'
onClick={() => setTab('custom')}
onClick={() => {
trackEvent(MixpanelEvent.Theme_Tab_Switched, { pathname, tab: 'custom' })
setTab('custom')
}}
style={{
background: tab === 'custom' ? 'linear-gradient(270deg, #D5CAFC 2.4%, #E3D2EB 27.88%, #F4DCD3 69.23%, #FDE4C2 100%)' : 'transparent'
}}
@ -878,7 +978,10 @@ const ThemePanel: React.FC = () => {
<path d="M1 0V16.5" stroke="#EDECEC" strokeWidth="2" />
</svg>
<button className='px-5 py-2 text-xs font-medium text-[#3A3A3A] rounded-[70px]'
onClick={() => setTab('default')}
onClick={() => {
trackEvent(MixpanelEvent.Theme_Tab_Switched, { pathname, tab: 'default' })
setTab('default')
}}
style={{
background: tab === 'default' ? 'linear-gradient(270deg, #D5CAFC 2.4%, #E3D2EB 27.88%, #F4DCD3 69.23%, #FDE4C2 100%)' : 'transparent'
}}

View file

@ -4,6 +4,7 @@ import { ApiResponseHandler } from "@/app/(presentation-generator)/services/api/
import { ProcessedSlide } from "../types";
import { getHeader } from "@/app/(presentation-generator)/services/api/header";
import { getApiUrl } from "@/utils/api";
import { MixpanelEvent, trackEvent } from "@/utils/mixpanel";
export const useLayoutSaving = (
@ -15,8 +16,12 @@ export const useLayoutSaving = (
const [isModalOpen, setIsModalOpen] = useState(false);
const openSaveModal = useCallback(() => {
trackEvent(MixpanelEvent.CustomTemplate_Save_Modal_Opened, {
slide_count: slides.length,
processed_slides: slides.filter((slide) => slide.processed).length,
});
setIsModalOpen(true);
}, []);
}, [slides]);
const closeSaveModal = useCallback(() => {
setIsModalOpen(false);
@ -34,6 +39,14 @@ export const useLayoutSaving = (
setIsSavingLayout(true);
try {
trackEvent(MixpanelEvent.CustomTemplate_Save_Started, {
template_info_id,
layout_name: layoutName,
layout_name_length: layoutName.length,
description_length: description.length,
slide_count: slides.length,
processed_slides: slides.filter((slide) => slide.processed).length,
});
@ -77,6 +90,12 @@ export const useLayoutSaving = (
});
toast.success(`Layout "${layoutName}" saved successfully`);
trackEvent(MixpanelEvent.CustomTemplate_Saved, {
template_info_id,
saved_template_id: data.id,
layout_name: layoutName,
slide_count: slides.length,
});
closeSaveModal();
return data.id;
@ -101,4 +120,4 @@ export const useLayoutSaving = (
closeSaveModal,
saveLayout,
};
};
};

View file

@ -12,6 +12,7 @@ import {
ProcessedSlide,
} from "../types";
import { getApiUrl } from "@/utils/api";
import { MixpanelEvent, trackEvent } from "@/utils/mixpanel";
const initialState: TemplateCreationState = {
step: 'file-upload',
@ -48,6 +49,14 @@ export const useTemplateCreation = () => {
updateState({ isLoading: true, error: null });
try {
const extensionIndex = pptxFile.name.lastIndexOf(".");
const fileExtension = extensionIndex >= 0 ? pptxFile.name.slice(extensionIndex).toLowerCase() : "";
trackEvent(MixpanelEvent.CustomTemplate_Creation_Started, {
source: "pptx_upload",
file_name: pptxFile.name,
file_size_bytes: pptxFile.size,
file_extension: fileExtension,
});
const formData = new FormData();
formData.append("pptx_file", pptxFile);
@ -223,6 +232,12 @@ export const useTemplateCreation = () => {
totalSlides: state.previewData.slide_image_urls.length,
isLoading: false
});
trackEvent(MixpanelEvent.CustomTemplate_Creation_Started, {
source: "template_init",
template_id: typeof data === "string" ? data : data.id,
total_slides: state.previewData.slide_image_urls.length,
uploaded_font_count: state.previewData.fonts?.length || 0,
});
toast.success("Template creation initialized");
@ -300,6 +315,12 @@ export const useTemplateCreation = () => {
const allProcessed = newSlides.every(s => s.processed || s.error);
if (allProcessed) {
updateState({ step: 'completed' });
trackEvent(MixpanelEvent.CustomTemplate_Creation_Completed, {
template_id: templateId,
total_slides: newSlides.length,
processed_slides: newSlides.filter(s => s.processed).length,
failed_slides: newSlides.filter(s => Boolean(s.error)).length,
});
toast.success("All slides processed successfully!");
}
}
@ -399,4 +420,3 @@ export const useTemplateCreation = () => {
updateState,
};
};

View file

@ -15,7 +15,7 @@ export const CustomTemplateCard = memo(function CustomTemplateCard({
selectedTemplate,
}: {
template: CustomTemplates;
onSelectTemplate: (template: string) => void;
onSelectTemplate: (template: CustomTemplates) => void;
selectedTemplate: string | null;
}) {
const { previewLayouts, loading } = useCustomTemplatePreview(template.id);
@ -29,7 +29,7 @@ export const CustomTemplateCard = memo(function CustomTemplateCard({
? " border-blue-500 ring-2 ring-blue-500/25 shadow-sm"
: " border-[#E8E9EC]"
)}
onClick={() => onSelectTemplate(template.id)}
onClick={() => onSelectTemplate(template)}
>
<TemplatePreviewStage>
<LayoutsBadge count={template.layoutCount} />

View file

@ -1,8 +1,6 @@
import React from "react";
import { usePathname } from "next/navigation";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { Button } from "@/components/ui/button";
import { LoadingState, Template } from "../types/index";
import { LoadingState } from "../types/index";
import { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";
import { ChevronRight } from "lucide-react";
@ -11,18 +9,14 @@ interface GenerateButtonProps {
streamState: { isStreaming: boolean; isLoading: boolean };
selectedTemplate: TemplateLayoutsWithSettings | string | null;
onSubmit: () => void;
outlineCount: number;
}
const GenerateButton: React.FC<GenerateButtonProps> = ({
loadingState,
streamState,
selectedTemplate,
outlineCount,
onSubmit,
}) => {
const pathname = usePathname();
const isDisabled =
loadingState.isLoading || streamState.isLoading || streamState.isStreaming;
@ -37,18 +31,6 @@ const GenerateButton: React.FC<GenerateButtonProps> = ({
<Button
disabled={isDisabled}
onClick={() => {
if (!streamState.isLoading && !streamState.isStreaming) {
if (!selectedTemplate) {
trackEvent(MixpanelEvent.Outline_Select_Template_Button_Clicked, {
pathname,
});
} else {
trackEvent(
MixpanelEvent.Outline_Generate_Presentation_Button_Clicked,
{ pathname }
);
}
}
onSubmit();
}}
className=" w-full flex items-center gap-0.5 rounded-[58px] text-sm py-3 px-5 font-instrument_sans font-semibold text-[#101323] disabled:opacity-50 disabled:cursor-not-allowed font-syne"

View file

@ -16,8 +16,6 @@ import {
import { OutlineItem } from "./OutlineItem";
import { Button } from "@/components/ui/button";
import { FileText, Loader2 } from "lucide-react";
import { usePathname } from "next/navigation";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
interface OutlineContentProps {
outlines: { content: string }[] | null;
@ -45,8 +43,6 @@ const OutlineContent: React.FC<OutlineContentProps> = ({
})
);
const pathname = usePathname();
return (
<div className="space-y-6 font-syne ">
{isLoading && (!outlines || outlines.length === 0) && (
@ -109,7 +105,6 @@ const OutlineContent: React.FC<OutlineContentProps> = ({
<Button
variant="outline"
onClick={() => {
trackEvent(MixpanelEvent.Outline_Add_Slide_Button_Clicked, { pathname });
onAddSlide();
}}
disabled={isLoading || isStreaming}
@ -128,7 +123,6 @@ const OutlineContent: React.FC<OutlineContentProps> = ({
<Button
variant="outline"
onClick={() => {
trackEvent(MixpanelEvent.Outline_Add_Slide_Button_Clicked, { pathname });
onAddSlide();
}}
className="text-blue-600 border-blue-200"
@ -141,4 +135,4 @@ const OutlineContent: React.FC<OutlineContentProps> = ({
);
};
export default OutlineContent;
export default OutlineContent;

View file

@ -10,7 +10,7 @@ import OutlineContent from "./OutlineContent";
import EmptyStateView from "./EmptyStateView";
import GenerateButton from "./GenerateButton";
import { TABS, Template } from "../types/index";
import { TABS } from "../types/index";
import { useOutlineStreaming } from "../hooks/useOutlineStreaming";
import { useOutlineManagement } from "../hooks/useOutlineManagement";
import { usePresentationGeneration } from "../hooks/usePresentationGeneration";
@ -105,7 +105,6 @@ const OutlinePage: React.FC = () => {
<div className="fixed bottom-[26px] right-[26px] z-50">
<GenerateButton
outlineCount={outlines.length}
loadingState={loadingState}
streamState={streamState}
selectedTemplate={selectedTemplate}
@ -121,4 +120,4 @@ const OutlinePage: React.FC = () => {
);
};
export default OutlinePage;
export default OutlinePage;

View file

@ -7,6 +7,8 @@ import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { CustomTemplates, useCustomTemplateSummaries } from "@/app/hooks/useCustomTemplates";
import { Loader2 } from "lucide-react";
import { usePathname } from "next/navigation";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import CreateCustomTemplate from "../../(dashboard)/templates/components/CreateCustomTemplate";
import { CustomTemplateCard } from "./CustomTemplateCard";
@ -64,6 +66,8 @@ const TemplateSelection: React.FC<TemplateSelectionProps> = memo(function Templa
selectedTemplate,
onSelectTemplate,
}) {
const pathname = usePathname();
useEffect(() => {
const existingScript = document.querySelector(
'script[src*="tailwindcss.com"]'
@ -79,13 +83,31 @@ const TemplateSelection: React.FC<TemplateSelectionProps> = memo(function Templa
const { templates: customTemplates, loading: customLoading } = useCustomTemplateSummaries();
const handleCustomSelect = useCallback(
(template: TemplateLayoutsWithSettings | string) => onSelectTemplate(template),
[onSelectTemplate]
(template: CustomTemplates) => {
trackEvent(MixpanelEvent.Outline_Template_Selected, {
pathname,
template_type: "custom",
template_id: template.id,
template_name: template.name,
layout_count: template.layoutCount,
});
onSelectTemplate(template.id);
},
[onSelectTemplate, pathname]
);
const handleBuiltInSelect = useCallback(
(template: TemplateLayoutsWithSettings) => onSelectTemplate(template),
[onSelectTemplate]
(template: TemplateLayoutsWithSettings) => {
trackEvent(MixpanelEvent.Outline_Template_Selected, {
pathname,
template_type: "built_in",
template_id: template.id,
template_name: template.name,
layout_count: template.layouts.length,
});
onSelectTemplate(template);
},
[onSelectTemplate, pathname]
);
const selectedCustomId = useMemo(

View file

@ -1,13 +1,13 @@
import { useState, useCallback } from "react";
import { useDispatch } from "react-redux";
import { useRouter } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import { toast } from "sonner";
import { clearPresentationData } from "@/store/slices/presentationGeneration";
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
import { Template, LoadingState, TABS } from "../types/index";
import { MixpanelEvent, trackEvent } from "@/utils/mixpanel";
import { LoadingState, TABS } from "../types/index";
import { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";
import { getCustomTemplateDetails } from "@/app/hooks/useCustomTemplates";
import { MixpanelEvent, trackEvent } from "@/utils/mixpanel";
const DEFAULT_LOADING_STATE: LoadingState = {
message: "",
@ -24,6 +24,7 @@ export const usePresentationGeneration = (
) => {
const dispatch = useDispatch();
const router = useRouter();
const pathname = usePathname();
const [loadingState, setLoadingState] = useState<LoadingState>(DEFAULT_LOADING_STATE);
const validateInputs = useCallback(() => {
@ -76,6 +77,27 @@ export const usePresentationGeneration = (
}
if (!validateInputs()) return;
const selectedTemplateId =
typeof selectedTemplate === "string"
? selectedTemplate
: selectedTemplate?.id || null;
const selectedTemplateType =
typeof selectedTemplate === "string" ? "custom" : "built_in";
const selectedTemplateName =
typeof selectedTemplate === "string" ? null : selectedTemplate?.name || null;
const selectedTemplateLayoutCount =
typeof selectedTemplate === "string" ? null : selectedTemplate?.layouts?.length || 0;
trackEvent(MixpanelEvent.Outline_Presentation_Generation_Started, {
pathname,
presentation_id: presentationId,
outline_count: outlines?.length || 0,
template_id: selectedTemplateId,
template_type: selectedTemplateType,
template_name: selectedTemplateName,
template_layout_count: selectedTemplateLayoutCount,
});
setLoadingState({
message: "Generating presentation data...",
isLoading: true,
@ -159,7 +181,7 @@ export const usePresentationGeneration = (
} finally {
setLoadingState(DEFAULT_LOADING_STATE);
}
}, [validateInputs, presentationId, outlines, dispatch, router, selectedTemplate]);
}, [validateInputs, presentationId, outlines, dispatch, router, selectedTemplate, pathname]);
return { loadingState, handleSubmit };
};
};

View file

@ -7,6 +7,8 @@ import { v4 as uuidv4 } from "uuid";
import { toast } from 'sonner';
import { getCustomTemplateDetails } from "@/app/hooks/useCustomTemplates";
import { getTemplatesByTemplateName } from "@/app/presentation-templates";
import { usePathname } from "next/navigation";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
interface LayoutItemProps {
layout: any;
@ -42,6 +44,7 @@ const NewSlideV1 = ({
presentationId,
}: NewSlideV1Props) => {
const dispatch = useDispatch();
const pathname = usePathname();
const [layouts, setLayouts] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
@ -57,12 +60,20 @@ const NewSlideV1 = ({
presentation: presentationId,
};
dispatch(addNewSlide({ slideData: newSlide, index }));
trackEvent(MixpanelEvent.Presentation_Slide_Added, {
pathname,
presentation_id: presentationId,
inserted_after_index: index,
template_id: templateID,
layout_id: id,
is_custom_template: isCustomTemplate,
});
setShowNewSlideSelection(false);
} catch (error: any) {
console.error(error);
toast.error("Error adding new slide");
}
}, [index, templateID, presentationId, dispatch, setShowNewSlideSelection]);
}, [index, templateID, presentationId, dispatch, setShowNewSlideSelection, isCustomTemplate, pathname]);
@ -132,4 +143,3 @@ export default NewSlideV1;

View file

@ -110,6 +110,12 @@ const PresentationHeader = ({
trimmed || presentationData.title || "Presentation";
if (next !== presentationData.title) {
dispatch(updateTitle(next));
trackEvent(MixpanelEvent.Presentation_Title_Updated, {
pathname,
presentation_id,
previous_title_length: (presentationData.title || "").length,
next_title_length: next.length,
});
}
setIsEditingTitle(false);
};
@ -147,11 +153,6 @@ const PresentationHeader = ({
const exportViaIpc = async (format: "pptx" | "pdf"): Promise<boolean> => {
if (typeof window === 'undefined') return false;
if (!(window as any).electron?.exportPresentation) return false;
trackEvent(
format === "pptx"
? MixpanelEvent.Header_ExportAsPPTX_API_Call
: MixpanelEvent.Header_ExportAsPDF_API_Call
);
const result = await (window as any).electron.exportPresentation(
presentation_id,
presentationData?.title || 'presentation',
@ -167,10 +168,15 @@ const PresentationHeader = ({
if (isStreaming) return;
try {
trackEvent(MixpanelEvent.Presentation_Export_Started, {
pathname,
presentation_id,
format: "pptx",
slide_count: presentationData?.slides?.length || 0,
});
toast.info("Exporting PPTX...");
setIsExporting(true);
// Save the presentation data before exporting
trackEvent(MixpanelEvent.Header_UpdatePresentationContent_API_Call);
await PresentationGenerationApi.updatePresentationContent(presentationData);
if (await exportViaIpc("pptx")) {
@ -178,12 +184,10 @@ const PresentationHeader = ({
return;
}
trackEvent(MixpanelEvent.Header_GetPptxModel_API_Call);
const pptx_model = await get_presentation_pptx_model(presentation_id);
if (!pptx_model) {
throw new Error("Failed to get presentation PPTX model");
}
trackEvent(MixpanelEvent.Header_ExportAsPPTX_API_Call);
const pptx_path = await PresentationGenerationApi.exportAsPPTX(pptx_model);
if (pptx_path) {
// window.open(pptx_path, '_self');
@ -206,13 +210,17 @@ const PresentationHeader = ({
if (isStreaming) return;
try {
trackEvent(MixpanelEvent.Presentation_Export_Started, {
pathname,
presentation_id,
format: "pdf",
slide_count: presentationData?.slides?.length || 0,
});
toast.info("Exporting PDF...");
setIsExporting(true);
// Save the presentation data before exporting
trackEvent(MixpanelEvent.Header_UpdatePresentationContent_API_Call);
await PresentationGenerationApi.updatePresentationContent(presentationData);
trackEvent(MixpanelEvent.Header_ExportAsPDF_API_Call);
if (await exportViaIpc("pdf")) {
toast.success("PDF exported successfully!");
return;
@ -246,7 +254,11 @@ const PresentationHeader = ({
const handleReGenerate = () => {
dispatch(clearPresentationData());
dispatch(clearHistory())
trackEvent(MixpanelEvent.Header_ReGenerate_Button_Clicked, { pathname });
trackEvent(MixpanelEvent.Presentation_Regenerated, {
pathname,
presentation_id,
slide_count: presentationData?.slides?.length || 0,
});
router.push(`/presentation?id=${presentation_id}&stream=true`);
};
const downloadLink = (path: string) => {
@ -270,7 +282,6 @@ const PresentationHeader = ({
<Button
onClick={() => {
trackEvent(MixpanelEvent.Header_Export_PDF_Button_Clicked, { pathname });
handleExportPdf();
setOpen(false);
}}
@ -282,7 +293,6 @@ const PresentationHeader = ({
</Button>
<Button
onClick={() => {
trackEvent(MixpanelEvent.Header_Export_PPTX_Button_Clicked, { pathname });
handleExportPptx();
setOpen(false);
}}
@ -430,6 +440,12 @@ const PresentationHeader = ({
<button
onClick={() => {
const to = `?id=${presentation_id}&mode=present&slide=${currentSlide || 0}`;
trackEvent(MixpanelEvent.Presentation_Mode_Entered, {
pathname,
presentation_id,
slide_index: currentSlide || 0,
slide_count: presentationData?.slides?.length || 0,
});
trackEvent(MixpanelEvent.Navigation, { from: pathname, to });
router.push(to);
}}

View file

@ -1,5 +1,5 @@
"use client";
import React, { useLayoutEffect, useState } from "react";
import React, { useEffect, useLayoutEffect, useState } from "react";
import { useSelector } from "react-redux";
import { RootState } from "@/store/store";
import "../../utils/prism-languages";
@ -80,6 +80,15 @@ const PresentationPage: React.FC<PresentationPageProps> = ({
usePresentationUndoRedo();
useEffect(() => {
trackEvent(MixpanelEvent.Presentation_Editor_Viewed, {
pathname,
presentation_id,
stream_mode: !!stream,
presentation_mode: isPresentMode ? "present" : "edit",
});
}, [pathname, presentation_id, stream, isPresentMode]);
/** Editor tree unmounts in present mode; remount loses inline theme CSS — re-apply from Redux. */
useLayoutEffect(() => {
if (isPresentMode) return;
@ -146,7 +155,6 @@ const PresentationPage: React.FC<PresentationPageProps> = ({
onSlideClick={handleSlideClick}
presentationId={presentation_id}
loading={loading}
/>
</div>
<div className=" w-full h-[calc(100vh-20px)] pr-[25px] overflow-y-auto">

View file

@ -21,8 +21,9 @@ import { setPresentationData } from "@/store/slices/presentationGeneration";
import { SortableSlide } from "./SortableSlide";
import SlideScale from "../../components/PresentationRender";
import { Separator } from "@/components/ui/separator";
import { useRouter } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import NewSlide from "./NewSlide";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
interface SidePanelProps {
selectedSlide: number;
@ -41,6 +42,7 @@ const SidePanel = ({
}: SidePanelProps) => {
const router = useRouter();
const pathname = usePathname();
const [showNewSlideSelection, setShowNewSlideSelection] = useState(false);
const { presentationData, isStreaming } = useSelector(
@ -109,6 +111,13 @@ const SidePanel = ({
dispatch(
setPresentationData({ ...presentationData, slides: updatedArray })
);
trackEvent(MixpanelEvent.Presentation_Slides_Reordered, {
pathname,
presentation_id: presentationId,
from_index: oldIndex,
to_index: newIndex,
slide_count: updatedArray.length,
});
}
};

View file

@ -19,7 +19,6 @@ import {
import { usePathname } from "next/navigation";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { addToHistory } from "@/store/slices/undoRedoSlice";
import { V1ContentRender } from "../../components/V1ContentRender";
import NewSlide from "./NewSlide";
import SlideScale from "../../components/PresentationRender";
@ -52,8 +51,6 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
setIsUpdating(true);
try {
trackEvent(MixpanelEvent.Slide_Update_From_Prompt_Button_Clicked, { pathname });
trackEvent(MixpanelEvent.Slide_Edit_API_Call);
const response = await PresentationGenerationApi.editSlide(
slide.id,
editPrompt
@ -61,6 +58,15 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
if (response) {
dispatch(updateSlide({ index: slide.index, slide: response }));
trackEvent(MixpanelEvent.Presentation_Slide_Updated, {
pathname,
presentation_id: presentationId,
slide_id: slide.id,
slide_index: slide.index,
layout: slide.layout,
prompt_char_count: editPrompt.trim().length,
prompt_word_count: editPrompt.trim().split(/\s+/).filter(Boolean).length,
});
toast.success("Slide updated successfully");
setEditPrompt("");
}
@ -76,8 +82,13 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
const onDeleteSlide = async () => {
try {
trackEvent(MixpanelEvent.Slide_Delete_Slide_Button_Clicked, { pathname });
trackEvent(MixpanelEvent.Slide_Delete_API_Call);
trackEvent(MixpanelEvent.Presentation_Slide_Deleted, {
pathname,
presentation_id: presentationId,
slide_id: slide.id,
slide_index: slide.index,
layout: slide.layout,
});
// Add current state to past
dispatch(addToHistory({
slides: presentationData?.slides,
@ -154,7 +165,6 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
{!isStreaming && (
<div
onClick={() => {
trackEvent(MixpanelEvent.Slide_Add_New_Slide_Button_Clicked, { pathname });
setShowNewSlideSelection(true);
}}
className=" bg-white shadow-md w-[80px] py-2 border hover:border-[#5141e5] duration-300 flex items-center justify-center rounded-lg cursor-pointer mx-auto"

View file

@ -5,17 +5,19 @@ import { Palette } from 'lucide-react';
import { useDispatch } from 'react-redux';
import { updateTheme } from '@/store/slices/presentationGeneration';
import { useRouter } from 'next/navigation';
import { usePathname, useRouter } from 'next/navigation';
import {
applyPresentationThemeToElement,
clearPresentationThemeFromElement,
} from "../utils/applyPresentationThemeDom";
import { trackEvent, MixpanelEvent } from '@/utils/mixpanel';
const ThemeSelector = ({ current_theme, themes: allThemes }: { current_theme: any, themes: any[] }) => {
const [currentTheme, setCurrentTheme] = useState<any>(current_theme)
const dispatch = useDispatch()
const [isOpen, setIsOpen] = useState(false)
const router = useRouter()
const pathname = usePathname()
const applyTheme = async (theme: any) => {
const element = document.getElementById("presentation-slides-wrapper");
if (!element) return;
@ -25,12 +27,19 @@ const ThemeSelector = ({ current_theme, themes: allThemes }: { current_theme: an
if (!theme.data?.colors?.["graph_0"]) return;
applyPresentationThemeToElement(element, theme);
dispatch(updateTheme(theme));
trackEvent(MixpanelEvent.Presentation_Theme_Changed, {
pathname,
theme_id: theme.id,
theme_name: theme.name,
theme_source: theme.user === "system" ? "built_in" : "custom",
});
};
const resetTheme = async () => {
dispatch(updateTheme(null));
clearPresentationThemeFromElement(
document.getElementById("presentation-slides-wrapper")
);
trackEvent(MixpanelEvent.Presentation_Theme_Reset, { pathname });
};
@ -43,7 +52,10 @@ const ThemeSelector = ({ current_theme, themes: allThemes }: { current_theme: an
</PopoverTrigger>
<PopoverContent className="w-fit rounded-[18px] max-h-80 overflow-y-auto hide-scrollbar">
<div className='pb-2 flex gap-2 justify-end'>
<button className='text-xs text-gray-500 pb-2 text-right underline' onClick={() => router.push(`/theme?tab=new-theme`)}>+Customize Theme</button>
<button className='text-xs text-gray-500 pb-2 text-right underline' onClick={() => {
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/theme?tab=new-theme" });
router.push(`/theme?tab=new-theme`)
}}>+Customize Theme</button>
<button className='text-xs text-gray-500 pb-2 text-right underline' onClick={resetTheme}>Reset Theme</button>
</div>
<div className="grid grid-cols-3 gap-4">
@ -76,4 +88,4 @@ const ThemeSelector = ({ current_theme, themes: allThemes }: { current_theme: an
)
}
export default ThemeSelector
export default ThemeSelector

View file

@ -154,6 +154,13 @@ const UploadPage = () => {
};
};
const trackUploadValidationFailure = (reason: string) => {
trackEvent(MixpanelEvent.Upload_Configuration_Invalid, {
...getUploadSnapshotProps(),
reason,
});
};
const handleConfigChange = (key: keyof PresentationConfig, value: unknown) => {
setConfig((prev) => ({ ...prev, [key]: value } as PresentationConfig));
};
@ -194,28 +201,19 @@ const UploadPage = () => {
*/
const validateConfiguration = (): boolean => {
if (!config.language) {
trackEvent(MixpanelEvent.Upload_Validation_Failed, {
...getUploadSnapshotProps(),
reason: "language_missing",
});
trackUploadValidationFailure("language_missing");
toast.error("Please select language");
return false;
}
if (files.length > 0 && config.language === LanguageType.Auto) {
trackEvent(MixpanelEvent.Upload_Validation_Failed, {
...getUploadSnapshotProps(),
reason: "language_auto_with_documents",
});
trackUploadValidationFailure("language_auto_with_documents");
toast.error("Please choose a language before processing uploaded documents");
return false;
}
if (!config.prompt.trim() && files.length === 0) {
trackEvent(MixpanelEvent.Upload_Validation_Failed, {
...getUploadSnapshotProps(),
reason: "prompt_or_document_missing",
});
trackUploadValidationFailure("prompt_or_document_missing");
toast.error("No Prompt or Document Provided");
return false;
}
@ -227,15 +225,12 @@ const UploadPage = () => {
*/
const handleGeneratePresentation = async () => {
if (!validateConfiguration()) return;
trackEvent(MixpanelEvent.Upload_Generation_Started, getUploadSnapshotProps());
trackEvent(MixpanelEvent.Upload_GetStarted_Button_Clicked, getUploadSnapshotProps());
const isStockProviderReady = await ensureStockImageProviderReady();
if (!isStockProviderReady) {
trackEvent(MixpanelEvent.Upload_Validation_Failed, {
...getUploadSnapshotProps(),
reason: "stock_image_provider_unreachable",
});
trackUploadValidationFailure("stock_image_provider_unreachable");
return;
}
@ -267,7 +262,6 @@ const UploadPage = () => {
let documents = [];
if (files.length > 0) {
trackEvent(MixpanelEvent.Upload_Upload_Documents_API_Call);
const uploadResponse = await PresentationGenerationApi.uploadDoc(files);
documents = uploadResponse;
}
@ -277,7 +271,6 @@ const UploadPage = () => {
const promises: Promise<any>[] = [];
if (documents.length > 0) {
trackEvent(MixpanelEvent.Upload_Decompose_Documents_API_Call);
promises.push(
PresentationGenerationApi.decomposeDocuments(
documents,
@ -291,6 +284,12 @@ const UploadPage = () => {
files: responses,
}));
dispatch(clearOutlines())
trackEvent(MixpanelEvent.Upload_Documents_Processed, {
...getUploadSnapshotProps(),
uploaded_documents_count: documents.length,
decompose_job_count: responses.length,
destination: "/documents-preview",
});
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/documents-preview" });
router.push("/documents-preview");
};
@ -309,7 +308,6 @@ const UploadPage = () => {
const selectedLanguage = config?.language ?? "";
// Use the first available layout group for direct generation
trackEvent(MixpanelEvent.Upload_Create_Presentation_API_Call);
const createResponse = await PresentationGenerationApi.createPresentation({
content: config?.prompt ?? "",
n_slides: config?.slides ? parseInt(config.slides, 10) : null,
@ -326,6 +324,11 @@ const UploadPage = () => {
dispatch(setPresentationId(createResponse.id));
dispatch(clearOutlines())
trackEvent(MixpanelEvent.Upload_Outline_Generation_Requested, {
...getUploadSnapshotProps(),
presentation_id: createResponse.id,
destination: "/outline",
});
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/outline" });
router.push("/outline");
};
@ -375,7 +378,7 @@ const UploadPage = () => {
</div>
</div>
<div className="p-4 ">
<h3 className="text-sm font-normal font-unbounded text-[#333333] mb-2">Attachments (optional)</h3>
<h3 className="text-sm font-medium text-[#333333] mb-2">Attachments (optional)</h3>
<SupportingDoc
files={[...files]}
onFilesChange={setFiles}

View file

@ -70,10 +70,18 @@ const FinalStep = () => {
);
const handleGoToDashboard = () => {
trackEvent(MixpanelEvent.Onboarding_Completed, {
pathname,
destination: "/dashboard",
});
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/dashboard" });
router.push("/dashboard");
};
const handleGoToUpload = () => {
trackEvent(MixpanelEvent.Onboarding_Completed, {
pathname,
destination: "/upload",
});
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/upload" });
router.push("/upload");
};

View file

@ -1,7 +1,7 @@
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 { Check, CheckCircle, ChevronLeft, 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';
@ -13,7 +13,7 @@ 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 { usePathname } from 'next/navigation';
import { handleSaveLLMConfig } from '@/utils/storeHelpers';
import { checkIfSelectedOllamaModelIsPulled, pullOllamaModel } from '@/utils/providerUtils';
import { getApiUrl } from '@/utils/api';
@ -21,7 +21,6 @@ import CodexConfig, { CHATGPT_MODELS } from '../CodexConfig';
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);
@ -45,7 +44,6 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
} | null>(null);
const handleProviderChange = (provider: string) => {
setLlmConfig(prev => ({
...prev,
LLM: provider
@ -73,8 +71,6 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
return 'OLLAMA_MODEL';
case 'custom':
return 'CUSTOM_MODEL';
case 'codex':
return 'CODEX_MODEL';
default:
return '';
}
@ -94,6 +90,18 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
}
}, [llmConfig.LLM]);
const getFieldValue = (field?: string) => {
if (!field) return "";
return (llmConfig as Record<string, string | undefined>)[field] || "";
};
const currentApiKey = currentApiKeyField ? ((llmConfig as Record<string, unknown>)[currentApiKeyField] as string || '') : '';
const currentModel = currentModelField ? ((llmConfig as Record<string, unknown>)[currentModelField] as string || '') : '';
const currentOllamaUrl = llmConfig.OLLAMA_URL || '';
const useCustomOllamaUrl = !!llmConfig.USE_CUSTOM_URL;
const getSelectedTextModel = (config: LLMConfig): string => {
switch (config.LLM) {
case 'openai':
@ -119,22 +127,11 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
return '';
};
const getFieldValue = (field?: string) => {
if (!field) return "";
return (llmConfig as Record<string, string | undefined>)[field] || "";
};
const currentApiKey = currentApiKeyField ? ((llmConfig as Record<string, unknown>)[currentApiKeyField] as string || '') : '';
const currentModel = currentModelField ? ((llmConfig as Record<string, unknown>)[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;
@ -295,25 +292,15 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
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();
}
}
@ -329,21 +316,30 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
text_provider_label: LLM_PROVIDERS[textProvider]?.label || textProvider || '',
text_model: textModel,
uses_chatgpt_login: textProvider === 'codex',
codex_model: llmConfig.CODEX_MODEL || '',
ollama_model: llmConfig.OLLAMA_MODEL || '',
ollama_uses_custom_url: !!llmConfig.USE_CUSTOM_URL,
custom_llm_url_set: Boolean((llmConfig.CUSTOM_LLM_URL || '').trim()),
image_generation_enabled: imageGenerationEnabled,
image_provider: imageProvider,
image_provider_label: imageGenerationEnabled
? (IMAGE_PROVIDERS[imageProvider]?.label || imageProvider || '')
: 'Image generation disabled',
image_quality: imageGenerationEnabled ? getSelectedImageQuality(llmConfig) : '',
image_quality: imageGenerationEnabled ? getSelectedImageQuality(llmConfig) : ''
});
trackEvent(MixpanelEvent.Onboarding_Configuration_Saved, {
pathname,
text_provider: textProvider,
text_model: textModel,
image_generation_enabled: imageGenerationEnabled,
image_provider: imageProvider,
});
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");
toast.error(error instanceof Error ? error.message : "Failed to save configuration");
}
finally {
@ -360,7 +356,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
useEffect(() => {
if (llmConfig.LLM === 'ollama' && !modelsChecked && !modelsLoading) {
fetchAvailableModels();
void fetchAvailableModels();
}
}, [llmConfig.LLM, modelsChecked, modelsLoading]);
@ -372,24 +368,20 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
<h2 className='mb-4 text-black text-[26px] font-normal font-unbounded '>Choose your content providers</h2>
<p className='text-[#000000CC] text-xl font-normal font-syne'>Select the AI engines that will generate your slide text and visuals.</p>
</div>
{llmConfig.LLM === 'codex' && (
<div className="mb-5">
<CodexConfig
codexModel={llmConfig.CODEX_MODEL || ''}
onInputChange={(value, field) => {
const normalizedField = field === 'codex_model' ? 'CODEX_MODEL' : field;
setLlmConfig((prev) => ({
...prev,
[normalizedField]: value,
}));
}}
/>
</div>
)}
<CodexConfig
codexModel={llmConfig.CODEX_MODEL || ''}
onInputChange={(value, field) => {
const normalizedField = field === 'codex_model' ? 'CODEX_MODEL' : field;
setLlmConfig(prev => ({
...prev,
[normalizedField]: value
}));
}}
/>
{/* Text Provider */}
<div className='p-3 border border-[#EDEEEF] rounded-[11px] '>
<div className="flex items-center gap-6 mb-7">
<div className='w-[60px] h-[60px] rounded-[4px] flex items-center justify-center'
<div className="flex items-center gap-[24.3px] mb-[42px]">
<div className='w-[74px] h-[74px] rounded-[4px] pt-[16.8px] pr-[17.15px] pb-[17.2px] pl-[16.85px] flex items-center justify-center'
style={{ backgroundColor: '#4C55541A' }}
>
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" fill="none">
@ -421,7 +413,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
variant="outline"
role="combobox"
aria-expanded={openProviderSelect}
className=" h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
className=" h-12 px-4 py-4 outline-none border border-[#E8E8E9] rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
>
<div className="flex gap-3 items-center">
<span className="text-sm font-medium text-gray-900">
@ -563,9 +555,9 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
key={model.id}
value={model.id}
onSelect={(value) => {
setLlmConfig((prev) => ({
setLlmConfig(prev => ({
...prev,
CODEX_MODEL: value,
CODEX_MODEL: value
}));
setOpenModelSelect(false);
}}
@ -641,7 +633,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
(llmConfig.LLM === 'anthropic' && !currentApiKey) ||
(llmConfig.LLM === 'custom' && !llmConfig.CUSTOM_LLM_URL)
}
className={`mt-4 py-2.5 bg-[#EDEEEF] px-3.5 w-fit rounded-[48px] text-xs font-semibold text-[#101323] transition-all duration-200 border ${modelsLoading
className={`mt-4 py-2.5 bg-[#EDEEEF] disabled:opacity-50 disabled:cursor-not-allowed px-3.5 w-fit rounded-[48px] text-xs font-semibold text-[#101323] transition-all duration-200 border ${modelsLoading
? " border-gray-300 cursor-not-allowed text-gray-500"
: " border-[#EDEEEF] text-[#101323] hover:bg-[#E8F0FF]/90 focus:ring-2 focus:ring-blue-500/20"
}`}
@ -748,12 +740,12 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</div>
</div>
{/* Image Provider */}
<div className='p-3 border border-[#EDEEEF] rounded-[11px] mt-5'>
<ToolTip content="Enable/Disable Image Generation" className='flex justify-end items-center'>
<div className={`p-3 border border-[#EDEEEF] rounded-[11px] relative mt-5 ${llmConfig.DISABLE_IMAGE_GENERATION ? "bg-[#F9FAFB]" : ""}`}>
<ToolTip content="Enable/Disable Image Generation" className='flex justify-end items-center absolute top-3 right-3'>
<div className='flex justify-end items-center'>
<Switch
checked={!llmConfig.DISABLE_IMAGE_GENERATION}
className='data-[state=checked]:bg-[#4791FF] data-[state=unchecked]:bg-gray-400'
className='data-[state=checked]:bg-[#4791FF] h-[22px] w-[36px] data-[state=unchecked]:bg-[#E2E0E1]'
onCheckedChange={(checked) => setLlmConfig(prev => ({
...prev,
DISABLE_IMAGE_GENERATION: !checked
@ -762,8 +754,8 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</div>
</ToolTip>
<div className=" mb-7 flex items-center gap-6">
<div className='w-[60px] h-[60px] px-[13.5px] py-[14.2px] rounded-[4px] flex items-center justify-center'
<div className={` flex items-center gap-6 ${llmConfig.DISABLE_IMAGE_GENERATION ? "" : "mb-[42px]"}`}>
<div className='w-[74px] h-[74px] px-[13.5px] py-[14.2px] rounded-[4px] flex items-center justify-center'
style={{ backgroundColor: '#F4F3FF' }}
>
<img src="/image-markup.svg" className='w-full h-full object-cover' alt='image-markup' />
@ -964,7 +956,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
</div>}
</div>
<div className='absolute bottom-16 mr-8 max-w-[1440px] right-0 flex justify-end items-center gap-2.5 '>
<div className='fixed bottom-16 mr-8 max-w-[1440px] right-16 flex justify-end items-center gap-2.5 '>
<button
disabled={currentStep === 1}
onClick={() => {
@ -977,7 +969,7 @@ const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep:
disabled={savingConfig}
onClick={handleSaveConfig}
className='border border-[#EDEEEF] bg-[#7C51F8] rounded-[58px] px-5 py-2.5 text-white text-xs font-semibold'>
className='border font-syne border-[#EDEEEF] bg-[#7C51F8] rounded-[58px] px-5 py-2.5 text-white text-xs font-semibold'>
Continue to Finish
</button>
</div>

View file

@ -9,13 +9,37 @@ export enum MixpanelEvent {
Navigation = 'Navigation',
Home_SaveConfiguration_Button_Clicked = 'Home Save Configuration Button Clicked',
Home_SaveConfiguration_API_Call = 'Home Save Configuration API Call',
Onboarding_Providers_Models_Selected = 'Onboarding Providers Models Selected',
Codex_SignIn_API_Call = 'Codex Sign In API Call',
Home_CheckOllamaModelPulled_API_Call = 'Home Check Ollama Model Pulled API Call',
Home_DownloadOllamaModel_API_Call = 'Home Download Ollama Model API Call',
Onboarding_Providers_Models_Selected = 'Onboarding Providers Models Selected',
Onboarding_Configuration_Saved = 'Onboarding Configuration Saved',
Onboarding_Completed = 'Onboarding Completed',
Codex_SignIn_API_Call = 'Codex Sign In API Call',
Upload_Configuration_Invalid = 'Upload Configuration Invalid',
Upload_Generation_Started = 'Upload Generation Started',
Upload_Documents_Processed = 'Upload Documents Processed',
Upload_Outline_Generation_Requested = 'Upload Outline Generation Requested',
Outline_Generate_Presentation_Button_Clicked = 'Outline Generate Presentation Button Clicked',
Outline_Select_Template_Button_Clicked = 'Outline Select Template Button Clicked',
Outline_Add_Slide_Button_Clicked = 'Outline Add Slide Button Clicked',
Outline_Template_Selected = 'Outline Template Selected',
Outline_Presentation_Generation_Started = 'Outline Presentation Generation Started',
Presentation_Editor_Viewed = 'Presentation Editor Viewed',
Presentation_Mode_Entered = 'Presentation Mode Entered',
Presentation_Title_Updated = 'Presentation Title Updated',
Presentation_Slides_Reordered = 'Presentation Slides Reordered',
Presentation_Slide_Added = 'Presentation Slide Added',
Presentation_Slide_Updated = 'Presentation Slide Updated',
Presentation_Slide_Deleted = 'Presentation Slide Deleted',
Presentation_Theme_Changed = 'Presentation Theme Changed',
Presentation_Theme_Reset = 'Presentation Theme Reset',
Presentation_Export_Started = 'Presentation Export Started',
Presentation_Regenerated = 'Presentation Regenerated',
Presentation_Prepare_API_Call = 'Presentation Prepare API Call',
Presentation_Stream_API_Call = 'Presentation Stream API Call',
Group_Layout_Selected_Clicked = 'Group Layout Selected Clicked',
@ -40,6 +64,8 @@ export enum MixpanelEvent {
Upload_Upload_Documents_API_Call = 'Upload Upload Documents API Call',
Upload_Decompose_Documents_API_Call = 'Upload Decompose Documents API Call',
Upload_Create_Presentation_API_Call = 'Upload Create Presentation API Call',
Upload_GetStarted_Button_Clicked = 'Upload Get Started Button Clicked',
Upload_Validation_Failed = 'Upload Validation Failed',
DocumentsPreview_Create_Presentation_API_Call = 'Documents Preview Create Presentation API Call',
DocumentsPreview_Next_Button_Clicked = 'Documents Preview Next Button Clicked',
Settings_SaveConfiguration_Button_Clicked = 'Settings Save Configuration Button Clicked',
@ -53,6 +79,40 @@ export enum MixpanelEvent {
ImageEditor_GenerateImage_API_Call = 'Image Editor Generate Image API Call',
ImageEditor_UploadImage_API_Call = 'Image Editor Upload Image API Call',
Header_ReGenerate_Button_Clicked = 'Header ReGenerate Button Clicked',
Dashboard_Page_Viewed = 'Dashboard Page Viewed',
Dashboard_New_Presentation_Clicked = 'Dashboard New Presentation Clicked',
Dashboard_Presentation_Opened = 'Dashboard Presentation Opened',
Dashboard_Presentation_Deleted = 'Dashboard Presentation Deleted',
Dashboard_Create_New_Card_Clicked = 'Dashboard Create New Card Clicked',
Sidebar_Navigation_Clicked = 'Sidebar Navigation Clicked',
Templates_Page_Viewed = 'Templates Page Viewed',
Templates_Tab_Switched = 'Templates Tab Switched',
Templates_Inbuilt_Opened = 'Templates Inbuilt Opened',
Templates_Custom_Opened = 'Templates Custom Opened',
Templates_New_Template_Clicked = 'Templates New Template Clicked',
Templates_Build_Template_Clicked = 'Templates Build Template Clicked',
Theme_Page_Viewed = 'Theme Page Viewed',
Theme_Selected = 'Theme Selected',
Theme_Saved = 'Theme Saved',
Theme_Deleted = 'Theme Deleted',
Theme_Font_Changed = 'Theme Font Changed',
Theme_Custom_Font_Uploaded = 'Theme Custom Font Uploaded',
Theme_Logo_Uploaded = 'Theme Logo Uploaded',
Theme_Tab_Switched = 'Theme Tab Switched',
Theme_New_Theme_Clicked = 'Theme New Theme Clicked',
Theme_Palette_Generated = 'Theme Palette Generated',
Theme_Editor_Opened = 'Theme Editor Opened',
Theme_Save_Started = 'Theme Save Started',
CustomTemplate_Creation_Started = 'Custom Template Creation Started',
CustomTemplate_Creation_Completed = 'Custom Template Creation Completed',
CustomTemplate_Save_Started = 'Custom Template Save Started',
CustomTemplate_Saved = 'Custom Template Saved',
CustomTemplate_Save_Modal_Opened = 'Custom Template Save Modal Opened',
}
export type MixpanelProps = Record<string, unknown>;
@ -76,24 +136,27 @@ async function ensureTelemetryStatus(): Promise<boolean> {
return window.__mixpanel_telemetry_enabled;
}
if (!trackingCheckPromise) {
trackingCheckPromise = fetch('/api/telemetry-status')
.then(async (res) => {
try {
const data = await res.json();
const enabled = Boolean(data?.telemetryEnabled);
window.__mixpanel_telemetry_enabled = enabled;
return enabled;
} catch {
// If the API response is malformed, default to enabling tracking
window.__mixpanel_telemetry_enabled = true;
return true;
trackingCheckPromise = (async () => {
try {
let data;
// Check if running in Electron environment
if (typeof window !== 'undefined' && window.electron?.telemetryStatus) {
// Use Electron IPC handler
data = await window.electron.telemetryStatus();
} else {
// Fallback to API route for web-based deployments
const res = await fetch('/api/telemetry-status');
data = await res.json();
}
})
.catch(() => {
const enabled = Boolean(data?.telemetryEnabled);
window.__mixpanel_telemetry_enabled = enabled;
return enabled;
} catch {
// If the API call fails, default to enabling tracking
window.__mixpanel_telemetry_enabled = true;
return true;
});
}
})();
}
return trackingCheckPromise;
}
@ -105,7 +168,11 @@ export function initMixpanel(): void {
void ensureTelemetryStatus().then((enabled) => {
if (!enabled) return;
if (window.__mixpanel_initialized) return;
mixpanel.init(MIXPANEL_TOKEN as string, { track_pageview: false });
mixpanel.init(MIXPANEL_TOKEN as string, { track_pageview: false, api_host: 'https://api-eu.mixpanel.com', });
const appVersion = window.env?.APP_VERSION;
if (appVersion) {
mixpanel.register({ app_version: appVersion });
}
mixpanel.identify(mixpanel.get_distinct_id());
window.__mixpanel_initialized = true;
});
@ -164,7 +231,7 @@ export function setTelemetryEnabled(enabled: boolean): void {
window.__mixpanel_telemetry_enabled = enabled;
}
trackingCheckPromise = null;
if (enabled && typeof window !== 'undefined' && !window.__mixpanel_initialized) {
if (enabled && !window?.__mixpanel_initialized) {
initMixpanel();
}
}
@ -178,5 +245,3 @@ export default {
resetTelemetryCache,
setTelemetryEnabled,
};