diff --git a/README.md b/README.md
index c1856961..b82ae3c9 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,8 @@
**Presenton** is an open-source application for generating presentations with AI — all running locally on your device. Stay in control of your data and privacy while using models like OpenAI and Gemini, or use your own hosted models through Ollama.
+__✨ Now, generate presentations with your existing PPTX file! Just upload your presentation file to create template design and then use that template to generate on brand and on design presentation on any topic.__
+

@@ -35,7 +37,8 @@
Presenton gives you complete control over your AI presentation workflow. Choose your models, customize your experience, and keep your data private.
-* ✅ **Custom Layouts & Themes** — Create unlimited presentation designs with HTML and Tailwind CSS
+* ✅ **Custom Templates & Themes** — Create unlimited presentation designs with HTML and Tailwind CSS
+* ✅ **AI Template Generation** — Create presentation templates from existing Powerpoint documents.
* ✅ **Flexible Generation** — Build presentations from prompts or uploaded documents
* ✅ **Export Ready** — Save as PowerPoint (PPTX) and PDF with professional formatting
* ✅ **Bring Your Own Key** — Use your own API keys for OpenAI, Google Gemini, Anthropic Claude, or any compatible provider. Only pay for what you use, no hidden fees or subscriptions.
@@ -45,7 +48,6 @@ Presenton gives you complete control over your AI presentation workflow. Choose
* ✅ **Versatile Image Generation** — Choose from DALL-E 3, Gemini Flash, Pexels, or Pixabay
* ✅ **Rich Media Support** — Icons, charts, and custom graphics for professional presentations
* ✅ **Runs Locally** — All processing happens on your device, no cloud dependencies
-* ✅ **Privacy-First** — Zero tracking, no data stored by us, complete data sovereignty
* ✅ **API Deployment** — Host as your own API service for your team
* ✅ **Fully Open-Source** — Apache 2.0 licensed, inspect, modify, and contribute
* ✅ **Docker Ready** — One-command deployment with GPU support for local models
@@ -102,6 +104,10 @@ You can also set the following environment variables to customize the image gene
- **GOOGLE_API_KEY=[Your Google API Key]**: Required if using **gemini_flash** as the image provider.
- **OPENAI_API_KEY=[Your OpenAI API Key]**: Required if using **dall-e-3** as the image provider.
+You can disable anonymous telemetry using the following environment variable:
+- **DISABLE_ANONYMOUS_TELEMETRY=[true/false]**: Set this to **true** to disable anonymous telemetry.
+
+
> **Note:** You can freely choose both the LLM (text generation) and the image provider. Supported image providers: **pexels**, **pixabay**, **gemini_flash** (Google), and **dall-e-3** (OpenAI).
### Using OpenAI
@@ -160,7 +166,7 @@ Content-Type: `multipart/form-data`
| prompt | string | Yes | The main topic or prompt for generating the presentation |
| n_slides | integer | No | Number of slides to generate (default: 8, min: 5, max: 15) |
| language | string | No | Language for the presentation (default: "English") |
-| template | string | No | Presentation theme (default: "general"). Available options: "classic", "general", "modern", "professional" + Custom templates |
+| template | string | No | Presentation template (default: "general"). Available options: "classic", "general", "modern", "professional" + Custom templates |
| documents | File[] | No | Optional list of document files to include in the presentation. Supported file types: PDF, TXT, PPTX, DOCX |
| export_as | string | No | Export format ("pptx" or "pdf", default: "pptx") |
diff --git a/docker-compose.yml b/docker-compose.yml
index 85ca9210..39a24c97 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -29,6 +29,7 @@ services:
- DISABLE_THINKING=${DISABLE_THINKING}
- WEB_GROUNDING=${WEB_GROUNDING}
- DATABASE_URL=${DATABASE_URL}
+ - DISABLE_ANONYMOUS_TRACKING=${DISABLE_ANONYMOUS_TRACKING}
production-gpu:
# image: ghcr.io/presenton/presenton:latest
@@ -67,6 +68,7 @@ services:
- DISABLE_THINKING=${DISABLE_THINKING}
- WEB_GROUNDING=${WEB_GROUNDING}
- DATABASE_URL=${DATABASE_URL}
+ - DISABLE_ANONYMOUS_TRACKING=${DISABLE_ANONYMOUS_TRACKING}
development:
build:
@@ -97,6 +99,7 @@ services:
- DISABLE_THINKING=${DISABLE_THINKING}
- WEB_GROUNDING=${WEB_GROUNDING}
- DATABASE_URL=${DATABASE_URL}
+ - DISABLE_ANONYMOUS_TRACKING=${DISABLE_ANONYMOUS_TRACKING}
development-gpu:
build:
@@ -134,3 +137,4 @@ services:
- DISABLE_THINKING=${DISABLE_THINKING}
- WEB_GROUNDING=${WEB_GROUNDING}
- DATABASE_URL=${DATABASE_URL}
+ - DISABLE_ANONYMOUS_TRACKING=${DISABLE_ANONYMOUS_TRACKING}
diff --git a/servers/fastapi/api/v1/ppt/endpoints/pptx_slides.py b/servers/fastapi/api/v1/ppt/endpoints/pptx_slides.py
index 5044cd52..9b922f3c 100644
--- a/servers/fastapi/api/v1/ppt/endpoints/pptx_slides.py
+++ b/servers/fastapi/api/v1/ppt/endpoints/pptx_slides.py
@@ -249,7 +249,7 @@ async def analyze_fonts_in_all_slides(slide_xmls: List[str]) -> FontAnalysisResu
return FontAnalysisResult(
internally_supported_fonts=internally_supported_fonts,
- not_supported_fonts=not_supported_fonts
+ not_supported_fonts=[]
)
diff --git a/servers/fastapi/api/v1/ppt/endpoints/presentation.py b/servers/fastapi/api/v1/ppt/endpoints/presentation.py
index 52920367..4452553b 100644
--- a/servers/fastapi/api/v1/ppt/endpoints/presentation.py
+++ b/servers/fastapi/api/v1/ppt/endpoints/presentation.py
@@ -221,6 +221,7 @@ async def stream_presentation(
layout_group=layout.name,
layout=slide_layout.id,
index=i,
+ speaker_note=slide_content.get("__speaker_note__", ""),
content=slide_content,
)
slides.append(slide)
diff --git a/servers/fastapi/api/v1/ppt/endpoints/slide.py b/servers/fastapi/api/v1/ppt/endpoints/slide.py
index a0c81107..e1ec9e6b 100644
--- a/servers/fastapi/api/v1/ppt/endpoints/slide.py
+++ b/servers/fastapi/api/v1/ppt/endpoints/slide.py
@@ -14,7 +14,6 @@ from utils.llm_calls.edit_slide_html import get_edited_slide_html
from utils.llm_calls.select_slide_type_on_edit import get_slide_layout_from_prompt
from utils.process_slides import process_old_and_new_slides_and_fetch_assets
from utils.randomizers import get_random_uuid
-from utils.schema_utils import remove_fields_from_schema
SLIDE_ROUTER = APIRouter(prefix="/slide", tags=["Slide"])
@@ -59,6 +58,7 @@ async def edit_slide(
sql_session.add(slide)
slide.content = edited_slide_content
slide.layout = slide_layout.id
+ slide.speaker_note = edited_slide_content.get("__speaker_note__", "")
sql_session.add_all(new_assets)
await sql_session.commit()
diff --git a/servers/fastapi/models/pptx_models.py b/servers/fastapi/models/pptx_models.py
index ee1e4cd1..80da5cd9 100644
--- a/servers/fastapi/models/pptx_models.py
+++ b/servers/fastapi/models/pptx_models.py
@@ -57,6 +57,8 @@ class PptxFontModel(BaseModel):
italic: bool = False
color: str = "000000"
font_weight: Optional[int] = 400
+ underline: Optional[bool] = None
+ strike: Optional[bool] = None
class PptxFillModel(BaseModel):
@@ -154,6 +156,7 @@ class PptxConnectorModel(PptxShapeModel):
class PptxSlideModel(BaseModel):
background: Optional[PptxFillModel] = None
+ note: Optional[str] = None
shapes: List[
PptxTextBoxModel
| PptxAutoShapeBoxModel
diff --git a/servers/fastapi/models/sql/slide.py b/servers/fastapi/models/sql/slide.py
index 7c0cb7e3..5d859d82 100644
--- a/servers/fastapi/models/sql/slide.py
+++ b/servers/fastapi/models/sql/slide.py
@@ -12,6 +12,7 @@ class SlideModel(SQLModel, table=True):
index: int
content: dict = Field(sa_column=Column(JSON))
html_content: Optional[str]
+ speaker_note: str
properties: Optional[dict] = Field(sa_column=Column(JSON))
def get_new_slide(self, presentation_id: str, content: Optional[dict] = None):
@@ -21,6 +22,7 @@ class SlideModel(SQLModel, table=True):
layout_group=self.layout_group,
layout=self.layout,
index=self.index,
+ speaker_note=self.speaker_note,
content=content or self.content,
properties=self.properties,
)
diff --git a/servers/fastapi/services/html_to_text_runs_service.py b/servers/fastapi/services/html_to_text_runs_service.py
new file mode 100644
index 00000000..25a441a7
--- /dev/null
+++ b/servers/fastapi/services/html_to_text_runs_service.py
@@ -0,0 +1,65 @@
+from html.parser import HTMLParser
+from typing import List, Optional
+
+from models.pptx_models import PptxFontModel, PptxTextRunModel
+
+
+class InlineHTMLToRunsParser(HTMLParser):
+ def __init__(self, base_font: PptxFontModel):
+ super().__init__(convert_charrefs=True)
+ self.base_font = base_font
+ self.tag_stack: List[str] = []
+ self.text_runs: List[PptxTextRunModel] = []
+
+ def _current_font(self) -> PptxFontModel:
+ font_json = self.base_font.model_dump()
+ is_bold = any(tag in ("strong", "b") for tag in self.tag_stack)
+ is_italic = any(tag in ("em", "i") for tag in self.tag_stack)
+ is_underline = any(tag == "u" for tag in self.tag_stack)
+ is_strike = any(tag in ("s", "strike", "del") for tag in self.tag_stack)
+ is_code = any(tag == "code" for tag in self.tag_stack)
+
+ if is_bold:
+ font_json["font_weight"] = 700
+ if is_italic:
+ font_json["italic"] = True
+ if is_underline:
+ font_json["underline"] = True
+ if is_strike:
+ font_json["strike"] = True
+ if is_code:
+ font_json["name"] = "Courier New"
+
+ return PptxFontModel(**font_json)
+
+ def handle_starttag(self, tag, attrs):
+ tag = tag.lower()
+ if tag == "br":
+ self.text_runs.append(PptxTextRunModel(text="\n"))
+ return
+ self.tag_stack.append(tag)
+
+ def handle_endtag(self, tag):
+ tag = tag.lower()
+ for i in range(len(self.tag_stack) - 1, -1, -1):
+ if self.tag_stack[i] == tag:
+ del self.tag_stack[i]
+ break
+
+ def handle_data(self, data):
+ if data == "":
+ return
+ self.text_runs.append(PptxTextRunModel(text=data, font=self._current_font()))
+
+
+def parse_html_text_to_text_runs(
+ text: str, base_font: Optional[PptxFontModel] = None
+) -> List[PptxTextRunModel]:
+ normalized_text = text.replace("\r\n", "\n").replace("\r", "\n")
+ normalized_text = normalized_text.replace("\n", "
")
+
+ parser = InlineHTMLToRunsParser(base_font if base_font else PptxFontModel())
+ parser.feed(normalized_text)
+ return parser.text_runs
+
+
diff --git a/servers/fastapi/services/pptx_presentation_creator.py b/servers/fastapi/services/pptx_presentation_creator.py
index 44fd08ed..6563bd89 100644
--- a/servers/fastapi/services/pptx_presentation_creator.py
+++ b/servers/fastapi/services/pptx_presentation_creator.py
@@ -1,6 +1,9 @@
import os
from typing import List, Optional
from lxml import etree
+from services.html_to_text_runs_service import (
+ parse_html_text_to_text_runs as parse_inline_html_to_runs,
+)
from pptx import Presentation
from pptx.shapes.autoshape import Shape
@@ -144,6 +147,9 @@ class PptxPresentationCreator:
if slide_model.background:
self.apply_fill_to_shape(slide.background, slide_model.background)
+ if slide_model.note:
+ slide.notes_slide.notes_text_frame.text = slide_model.note
+
for shape_model in slide_model.shapes:
model_type = type(shape_model)
@@ -276,7 +282,7 @@ class PptxPresentationCreator:
text_runs = []
if paragraph_model.text:
- text_runs = self.parse_markdown_text_to_text_runs(
+ text_runs = self.parse_html_text_to_text_runs(
paragraph_model.font, paragraph_model.text
)
elif paragraph_model.text_runs:
@@ -286,78 +292,8 @@ class PptxPresentationCreator:
text_run = paragraph.add_run()
self.populate_text_run(text_run, text_run_model)
- def parse_markdown_text_to_text_runs(self, font: PptxFontModel, text: str):
- text_runs = []
- for line in text.split("\n"):
- current_pos = 0
- while current_pos < len(line):
- # Check for bold and italic (***text***)
- if (
- line[current_pos:].startswith("***")
- and "***" in line[current_pos + 3 :]
- ):
- end_pos = line.find("***", current_pos + 3)
- text_content = line[current_pos + 3 : end_pos]
- font_json = font.model_dump()
- font_json["bold"] = True
- font_json["italic"] = True
- font_json["font_weight"] = 700 # Set font weight to bold
- text_runs.append(
- PptxTextRunModel(
- text=text_content, font=PptxFontModel(**font_json)
- )
- )
- current_pos = end_pos + 3
- # Check for bold (**text**)
- elif (
- line[current_pos:].startswith("**")
- and "**" in line[current_pos + 2 :]
- ):
- end_pos = line.find("**", current_pos + 2)
- text_content = line[current_pos + 2 : end_pos]
- font_json = font.model_dump()
- font_json["bold"] = True
- font_json["font_weight"] = 700 # Set font weight to bold
- text_runs.append(
- PptxTextRunModel(
- text=text_content, font=PptxFontModel(**font_json)
- )
- )
- current_pos = end_pos + 2
- # Check for italic (*text*)
- elif (
- line[current_pos:].startswith("__")
- and "__" in line[current_pos + 2 :]
- ):
- end_pos = line.find("__", current_pos + 2)
- text_content = line[current_pos + 2 : end_pos]
- font_json = font.model_dump()
- font_json["italic"] = True
- text_runs.append(
- PptxTextRunModel(
- text=text_content, font=PptxFontModel(**font_json)
- )
- )
- current_pos = end_pos + 2
- else:
- # Find the next formatting marker or end of line
- next_marker = float("inf")
- for marker in ["***", "**", "__"]:
- pos = line.find(marker, current_pos)
- if pos != -1:
- next_marker = min(next_marker, pos)
-
- end_pos = next_marker if next_marker != float("inf") else len(line)
- text_content = line[current_pos:end_pos]
- if text_content: # Only add non-empty text
- text_runs.append(PptxTextRunModel(text=text_content, font=font))
- current_pos = end_pos
-
- # Add newline if not the last line
- if line != text.split("\n")[-1]:
- text_runs.append(PptxTextRunModel(text="\n"))
-
- return text_runs
+ def parse_html_text_to_text_runs(self, font: Optional[PptxFontModel], text: str):
+ return parse_inline_html_to_runs(text, font)
def populate_text_run(self, text_run: _Run, text_run_model: PptxTextRunModel):
text_run.text = text_run_model.text
@@ -527,6 +463,20 @@ class PptxPresentationCreator:
font.italic = font_model.italic
font.size = Pt(font_model.size)
font.bold = font_model.font_weight >= 600
+ if font_model.underline is not None:
+ font.underline = bool(font_model.underline)
+ if font_model.strike is not None:
+ self.apply_strike_to_font(font, font_model.strike)
+
+ def apply_strike_to_font(self, font: Font, strike: Optional[bool]):
+ try:
+ rPr = font._element
+ if strike is True:
+ rPr.set("strike", "sngStrike")
+ elif strike is False:
+ rPr.set("strike", "noStrike")
+ except Exception as e:
+ print(f"Could not apply strikethrough: {e}")
def save(self, path: str):
self._ppt.save(path)
diff --git a/servers/fastapi/utils/llm_calls/edit_slide.py b/servers/fastapi/utils/llm_calls/edit_slide.py
index 30599d08..5d91a607 100644
--- a/servers/fastapi/utils/llm_calls/edit_slide.py
+++ b/servers/fastapi/utils/llm_calls/edit_slide.py
@@ -3,10 +3,11 @@ from models.presentation_layout import SlideLayoutModel
from models.sql.slide import SlideModel
from services.llm_client import LLMClient
from utils.llm_provider import get_model
-from utils.schema_utils import remove_fields_from_schema
+from utils.schema_utils import add_field_in_schema, remove_fields_from_schema
system_prompt = """
- Edit Slide data based on provided prompt, follow mentioned steps and notes and provide structured output.
+ Edit Slide data and speaker note based on provided prompt, follow mentioned steps and notes and provide structured output.
+
# Notes
- Provide output in language mentioned in **Input**.
@@ -14,6 +15,8 @@ system_prompt = """
- Do not change **Image prompts** and **Icon queries** if not asked for in prompt.
- Generate **Image prompts** and **Icon queries** if asked to generate or change in prompt.
- Make sure to follow language guidelines.
+ - Speaker note should be normal text, not markdown.
+ - Speaker note should be simple, clear, concise and to the point.
**Go through all notes and steps and make sure they are followed, including mentioned constraints**
"""
@@ -61,6 +64,18 @@ async def get_edited_slide_content(
response_schema = remove_fields_from_schema(
slide_layout.json_schema, ["__image_url__", "__icon_url__"]
)
+ response_schema = add_field_in_schema(
+ response_schema,
+ {
+ "__speaker_note__": {
+ "type": "string",
+ "minLength": 100,
+ "maxLength": 250,
+ "description": "Speaker note for the slide",
+ }
+ },
+ True,
+ )
client = LLMClient()
response = await client.generate_structured(
diff --git a/servers/fastapi/utils/llm_calls/generate_slide_content.py b/servers/fastapi/utils/llm_calls/generate_slide_content.py
index be19b168..e8f695a0 100644
--- a/servers/fastapi/utils/llm_calls/generate_slide_content.py
+++ b/servers/fastapi/utils/llm_calls/generate_slide_content.py
@@ -3,7 +3,7 @@ from models.presentation_layout import SlideLayoutModel
from models.presentation_outline_model import SlideOutlineModel
from services.llm_client import LLMClient
from utils.llm_provider import get_model
-from utils.schema_utils import remove_fields_from_schema
+from utils.schema_utils import add_field_in_schema, remove_fields_from_schema
system_prompt = """
Generate structured slide based on provided outline, follow mentioned steps and notes and provide structured output.
@@ -11,6 +11,7 @@ system_prompt = """
# Steps
1. Analyze the outline.
2. Generate structured slide based on the outline.
+ 3. Generate speaker note that is simple, clear, concise and to the point.
# Notes
- Slide body should not use words like "This slide", "This presentation".
@@ -19,6 +20,7 @@ system_prompt = """
- Provide query to search icon on "__icon_query__" property.
- Only use markdown to highlight important points.
- Make sure to follow language guidelines.
+ - Speaker note should be normal text, not markdown.
**Strictly follow the max and min character limit for every property in the slide.**
"""
@@ -57,6 +59,18 @@ async def get_slide_content_from_type_and_outline(
response_schema = remove_fields_from_schema(
slide_layout.json_schema, ["__image_url__", "__icon_url__"]
)
+ response_schema = add_field_in_schema(
+ response_schema,
+ {
+ "__speaker_note__": {
+ "type": "string",
+ "minLength": 100,
+ "maxLength": 250,
+ "description": "Speaker note for the slide",
+ }
+ },
+ True,
+ )
response = await client.generate_structured(
model=model,
diff --git a/servers/fastapi/utils/schema_utils.py b/servers/fastapi/utils/schema_utils.py
index 3c82ad0f..92aafd97 100644
--- a/servers/fastapi/utils/schema_utils.py
+++ b/servers/fastapi/utils/schema_utils.py
@@ -45,6 +45,48 @@ def remove_fields_from_schema(schema: dict, fields_to_remove: List[str]):
return schema
+def add_field_in_schema(schema: dict, field: dict, required: bool = False) -> dict:
+
+ if not isinstance(field, dict) or len(field) != 1:
+ raise ValueError(
+ "`field` must be a dict with exactly one entry: {name: schema_dict}"
+ )
+
+ field_name, field_schema = next(iter(field.items()))
+ if not isinstance(field_name, str):
+ raise TypeError("Field name must be a string")
+ if not isinstance(field_schema, dict):
+ raise TypeError("Field schema must be a dictionary")
+
+ updated_schema: dict = deepcopy(schema)
+
+ root_properties = updated_schema.get("properties")
+ if not isinstance(root_properties, dict):
+ updated_schema["properties"] = {}
+ root_properties = updated_schema["properties"]
+
+ root_properties[field_name] = field_schema
+
+ # Update root-level required based on the flag
+ existing_required = updated_schema.get("required")
+ if not isinstance(existing_required, list):
+ existing_required = []
+
+ if required:
+ if field_name not in existing_required:
+ existing_required.append(field_name)
+ else:
+ if field_name in existing_required:
+ existing_required = [name for name in existing_required if name != field_name]
+
+ if existing_required:
+ updated_schema["required"] = existing_required
+ else:
+ updated_schema.pop("required", None)
+
+ return updated_schema
+
+
# From OpenAI
def ensure_strict_json_schema(
json_schema: object,
diff --git a/servers/nextjs/app/(presentation-generator)/components/HeaderNab.tsx b/servers/nextjs/app/(presentation-generator)/components/HeaderNab.tsx
index d960ad8d..cb0f0338 100644
--- a/servers/nextjs/app/(presentation-generator)/components/HeaderNab.tsx
+++ b/servers/nextjs/app/(presentation-generator)/components/HeaderNab.tsx
@@ -2,12 +2,15 @@
import { LayoutDashboard, Settings, Upload } from "lucide-react";
import React from "react";
import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { RootState } from "@/store/store";
import { useSelector } from "react-redux";
const HeaderNav = () => {
const canChangeKeys = useSelector((state: RootState) => state.userConfig.can_change_keys);
+ const pathname = usePathname();
return (
@@ -17,6 +20,7 @@ const HeaderNav = () => {
prefetch={false}
className="flex items-center gap-2 px-3 py-2 text-white hover:bg-primary/80 rounded-md transition-colors outline-none"
role="menuitem"
+ onClick={() => trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/dashboard" })}
>
@@ -29,6 +33,7 @@ const HeaderNav = () => {
prefetch={false}
className="flex items-center gap-2 px-3 py-2 text-white hover:bg-primary/80 rounded-md transition-colors outline-none"
role="menuitem"
+ onClick={() => trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/settings" })}
>
diff --git a/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx b/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx
index 29e67cc6..fb1c8337 100644
--- a/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx
+++ b/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx
@@ -15,6 +15,7 @@ import { PresentationGenerationApi } from "../services/api/presentation-generati
import { Skeleton } from "@/components/ui/skeleton";
import { toast } from "sonner";
import { PreviousGeneratedImagesResponse } from "../services/api/params";
+import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
interface ImageEditorProps {
initialImage: string | null;
imageIdx?: number;
@@ -90,6 +91,7 @@ const ImageEditor = ({
const getPreviousGeneratedImage = async () => {
try {
+ trackEvent(MixpanelEvent.ImageEditor_GetPreviousGeneratedImages_API_Call);
const response =
await PresentationGenerationApi.getPreviousGeneratedImages();
setPreviousGeneratedImages(response);
@@ -187,6 +189,7 @@ const ImageEditor = ({
try {
setIsGenerating(true);
setError(null);
+ trackEvent(MixpanelEvent.ImageEditor_GenerateImage_API_Call);
const response = await PresentationGenerationApi.generateImage({
prompt: prompt,
});
@@ -228,6 +231,7 @@ const ImageEditor = ({
const formData = new FormData();
formData.append("file", file);
+ trackEvent(MixpanelEvent.ImageEditor_UploadImage_API_Call);
const response = await fetch("/api/upload-image", {
method: "POST",
body: formData,
diff --git a/servers/nextjs/app/(presentation-generator)/components/NewSlide.tsx b/servers/nextjs/app/(presentation-generator)/components/NewSlide.tsx
index d852d803..05a6b638 100644
--- a/servers/nextjs/app/(presentation-generator)/components/NewSlide.tsx
+++ b/servers/nextjs/app/(presentation-generator)/components/NewSlide.tsx
@@ -2,7 +2,6 @@ import React from "react";
import { useDispatch } from "react-redux";
import { addNewSlide } from "@/store/slices/presentationGeneration";
import { Loader2 } from "lucide-react";
-// import { useGroupLayoutLoader } from '@/app/layout-preview/hooks/useGroupLayoutLoader';
import { useLayout, FullDataInfo } from "../context/LayoutContext";
import { v4 as uuidv4 } from "uuid";
import { Trash2 } from 'lucide-react';
diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useSlideEdit.ts b/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useSlideEdit.ts
index 5194c150..7ab1d4cb 100644
--- a/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useSlideEdit.ts
+++ b/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useSlideEdit.ts
@@ -14,20 +14,7 @@ export const useSlideEdit = (
const [slideHtml, setSlideHtml] = useState("");
const slideContentRef = useRef(null);
- // Load Tailwind CSS dynamically for slide content
- useEffect(() => {
- if (slide.processed && slide.html) {
- const existingScript = document.querySelector(
- 'script[src*="tailwindcss.com"]'
- );
- if (!existingScript) {
- const script = document.createElement("script");
- script.src = "https://cdn.tailwindcss.com";
- script.async = true;
- document.head.appendChild(script);
- }
- }
- }, [slide.processed, slide.html]);
+
// Set up canvas when entering edit mode
useEffect(() => {
diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useSlideProcessing.ts b/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useSlideProcessing.ts
index 544c4804..52e67a7c 100644
--- a/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useSlideProcessing.ts
+++ b/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useSlideProcessing.ts
@@ -157,13 +157,23 @@ export const useSlideProcessing = (
setSlides(initialSlides);
+ const hasUnsupported = Array.isArray(pptxData.fonts?.not_supported_fonts) && pptxData.fonts.not_supported_fonts.length > 0;
+
toast.success(
`Template Processing Finished`,
{
- description: `Please Upload the not supported fonts, and click Extract Template`
+ description: hasUnsupported
+ ? `Please Upload the not supported fonts, and click Extract Template`
+ : `All fonts are supported. Starting template extraction...`
}
);
+ // If all fonts are supported, auto-start extraction from the first slide
+ if (!hasUnsupported && initialSlides.length > 0) {
+ const firstSlide = initialSlides[0];
+ setTimeout(() => processSlideToHtml(firstSlide, 0), 300);
+ }
+
} catch (error) {
console.error("Error processing file:", error);
diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/page.tsx b/servers/nextjs/app/(presentation-generator)/custom-template/page.tsx
index 338ebd4d..3c23d26f 100644
--- a/servers/nextjs/app/(presentation-generator)/custom-template/page.tsx
+++ b/servers/nextjs/app/(presentation-generator)/custom-template/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import React from "react";
+import React, { useEffect } from "react";
import FontManager from "./components/FontManager";
import Header from "../dashboard/components/Header";
import { useLayout } from "../context/LayoutContext";
@@ -10,16 +10,18 @@ import { useFileUpload } from "./hooks/useFileUpload";
import { useSlideProcessing } from "./hooks/useSlideProcessing";
import { useLayoutSaving } from "./hooks/useLayoutSaving";
import { useAPIKeyCheck } from "./hooks/useAPIKeyCheck";
-import { useRouter } from "next/navigation";
+import { useRouter, usePathname } from "next/navigation";
import { LoadingSpinner } from "./components/LoadingSpinner";
import { FileUploadSection } from "./components/FileUploadSection";
import { SaveLayoutButton } from "./components/SaveLayoutButton";
import { SaveLayoutModal } from "./components/SaveLayoutModal";
import EachSlide from "./components/EachSlide/NewEachSlide";
import { APIKeyWarning } from "./components/APIKeyWarning";
+import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
const CustomTemplatePage = () => {
const router = useRouter();
+ const pathname = usePathname();
const { refetch } = useLayout();
// Custom hooks for different concerns
@@ -42,6 +44,7 @@ const CustomTemplatePage = () => {
);
const handleSaveTemplate = async (layoutName: string, description: string): Promise => {
+ trackEvent(MixpanelEvent.CustomTemplate_Save_Templates_API_Call);
const id = await saveLayout(layoutName, description);
if (id) {
router.push(`/template-preview/custom-${id}`);
@@ -67,6 +70,17 @@ const CustomTemplatePage = () => {
)
);
};
+ useEffect(() => {
+ const existingScript = document.querySelector(
+ 'script[src*="tailwindcss.com"]'
+ );
+ if (!existingScript) {
+ const script = document.createElement("script");
+ script.src = "https://cdn.tailwindcss.com";
+ script.async = true;
+ document.head.appendChild(script);
+ }
+ }, []);
// Loading state
if (isRequiredKeyLoading) {
diff --git a/servers/nextjs/app/(presentation-generator)/dashboard/components/Header.tsx b/servers/nextjs/app/(presentation-generator)/dashboard/components/Header.tsx
index 3708a494..4636347c 100644
--- a/servers/nextjs/app/(presentation-generator)/dashboard/components/Header.tsx
+++ b/servers/nextjs/app/(presentation-generator)/dashboard/components/Header.tsx
@@ -7,6 +7,7 @@ import BackBtn from "@/components/BackBtn";
import { usePathname } from "next/navigation";
import HeaderNav from "@/app/(presentation-generator)/components/HeaderNab";
import { Layout, FilePlus2 } from "lucide-react";
+import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
const Header = () => {
const pathname = usePathname();
return (
@@ -15,7 +16,7 @@ const Header = () => {
{pathname !== "/upload" &&
}
-
+
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/dashboard" })}>

{
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/custom-template" })}
className="flex items-center gap-2 px-3 py-2 text-white hover:bg-primary/80 rounded-md transition-colors outline-none"
role="menuitem"
>
@@ -36,6 +38,7 @@ const Header = () => {
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/template-preview" })}
className="flex items-center gap-2 px-3 py-2 text-white hover:bg-primary/80 rounded-md transition-colors outline-none"
role="menuitem"
>
diff --git a/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx b/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx
index 718049fb..8ed2f14a 100644
--- a/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx
+++ b/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx
@@ -19,7 +19,7 @@ import { OverlayLoader } from "@/components/ui/overlay-loader";
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
import { setPresentationId } from "@/store/slices/presentationGeneration";
import { useDispatch, useSelector } from "react-redux";
-import { useRouter } from "next/navigation";
+import { useRouter, usePathname } from "next/navigation";
import { RootState } from "@/store/store";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
@@ -28,6 +28,7 @@ import { getIconFromFile } from "../../utils/others";
import { ChevronRight, PanelRightOpen, X } from "lucide-react";
import ToolTip from "@/components/ToolTip";
import Header from "@/app/(presentation-generator)/dashboard/components/Header";
+import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
// Types
interface LoadingState {
@@ -50,6 +51,7 @@ const DocumentsPreviewPage: React.FC = () => {
// Hooks
const dispatch = useDispatch();
const router = useRouter();
+ const pathname = usePathname();
const textareaRef = useRef
(null);
// Redux state
@@ -144,6 +146,7 @@ const DocumentsPreviewPage: React.FC = () => {
const documentPaths = fileItems.map(
(fileItem: FileItem) => fileItem.file_path
);
+ trackEvent(MixpanelEvent.DocumentsPreview_Create_Presentation_API_Call);
const createResponse = await PresentationGenerationApi.createPresentation(
{
prompt: config?.prompt ?? "",
@@ -154,6 +157,7 @@ const DocumentsPreviewPage: React.FC = () => {
);
dispatch(setPresentationId(createResponse.id));
+ trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/outline" });
router.replace("/outline");
} catch (error: any) {
console.error("Error in radar presentation creation:", error);
diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/GenerateButton.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/GenerateButton.tsx
index 29359bb5..fd6a8ae2 100644
--- a/servers/nextjs/app/(presentation-generator)/outline/components/GenerateButton.tsx
+++ b/servers/nextjs/app/(presentation-generator)/outline/components/GenerateButton.tsx
@@ -1,4 +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, LayoutGroup } from "../types/index";
@@ -15,6 +17,8 @@ const GenerateButton: React.FC = ({
selectedLayoutGroup,
onSubmit
}) => {
+ const pathname = usePathname();
+
const isDisabled =
loadingState.isLoading ||
streamState.isLoading ||
@@ -30,7 +34,16 @@ const GenerateButton: React.FC = ({
return (