feat: completed theme & custom theme UI

This commit is contained in:
shiva raj badu 2026-03-03 10:50:11 +05:45
commit 28f2b18e06
No known key found for this signature in database
21 changed files with 235 additions and 79 deletions

View file

@ -1,12 +1,12 @@
{
"name": "presenton",
"version": "0.6.0-beta",
"version": "0.6.1-beta",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "presenton",
"version": "0.6.0-beta",
"version": "0.6.1-beta",
"hasInstallScript": true,
"dependencies": {
"@tailwindcss/cli": "^4.1.5",

View file

@ -1,7 +1,7 @@
{
"name": "presenton",
"productName": "Presenton Open Source",
"version": "0.6.0-beta",
"version": "0.6.1-beta",
"main": "app_dist/main.js",
"description": "Open-Source AI Presentation Generator",
"homepage": "https://presenton.ai",

View file

@ -317,9 +317,9 @@ async def stream_presentation(
# This will mutate slide and add placeholder assets
process_slide_add_placeholder_assets(slide)
# This will mutate slide
# This will mutate slide - start task immediately so it runs in parallel with next slide LLM generation
async_assets_generation_tasks.append(
process_slide_and_fetch_assets(image_generation_service, slide)
asyncio.create_task(process_slide_and_fetch_assets(image_generation_service, slide))
)
yield SSEResponse(
@ -721,9 +721,9 @@ async def generate_presentation_handler(
slides.append(slide)
batch_slides.append(slide)
# Start asset fetch tasks for just-generated slides so they run while next batch is processed
# Start asset fetch tasks immediately so they run in parallel with next batch's LLM calls
asset_tasks = [
process_slide_and_fetch_assets(image_generation_service, slide)
asyncio.create_task(process_slide_and_fetch_assets(image_generation_service, slide))
for slide in batch_slides
]
async_assets_generation_tasks.extend(asset_tasks)

View file

@ -66,6 +66,7 @@ from utils.set_env import (
from utils.llm_provider import get_llm_provider, get_model
from utils.parsers import parse_bool_or_none
from utils.schema_utils import (
ensure_array_schemas_have_items,
ensure_strict_json_schema,
flatten_json_schema,
remove_titles_from_schema,
@ -702,6 +703,7 @@ class LLMClient:
path=(),
root=response_schema,
)
response_schema = ensure_array_schemas_have_items(response_schema)
if use_tool_calls_for_structured_output and depth == 0:
if all_tools is None:
all_tools = []
@ -1599,6 +1601,7 @@ class LLMClient:
path=(),
root=response_schema,
)
response_schema = ensure_array_schemas_have_items(response_schema)
if use_tool_calls_for_structured_output and depth == 0:
if all_tools is None:
@ -1793,28 +1796,16 @@ class LLMClient:
"""
client: AsyncOpenAI = self._client
response_schema = response_format
# Apply strict schema once at root
# Apply strict schema once at root (includes array "items" fix at lines 135155).
if strict and depth == 0:
response_schema = ensure_strict_json_schema(
response_schema,
path=(),
root=response_schema,
)
# Codex Responses API requires all array schemas to specify `items`.
def _fix_arrays(node: Any) -> Any:
if isinstance(node, dict):
# Add default items for arrays missing them
if node.get("type") == "array" and "items" not in node:
node["items"] = {"type": "string"}
for key, value in list(node.items()):
node[key] = _fix_arrays(value)
elif isinstance(node, list):
for idx, value in enumerate(node):
node[idx] = _fix_arrays(value)
return node
response_schema = _fix_arrays(response_schema)
# When we didn't run ensure_strict_json_schema, fix arrays for Codex API (strict=False or depth > 0).
else:
response_schema = ensure_array_schemas_have_items(response_schema)
# Responses API tool format: flat {type, name, description, parameters}
response_schema_tool = {

View file

@ -134,11 +134,25 @@ def ensure_strict_json_schema(
# arrays
# { 'type': 'array', 'items': {...} }
# OpenAI requires array schemas to have "items". Zod tuples may emit prefixItems only.
items = json_schema.get("items")
if isinstance(items, dict):
json_schema["items"] = ensure_strict_json_schema(
items, path=(*path, "items"), root=root
)
elif typ == "array":
prefix_items = json_schema.get("prefixItems")
if (
isinstance(prefix_items, list)
and len(prefix_items) > 0
and isinstance(prefix_items[0], dict)
):
json_schema["items"] = ensure_strict_json_schema(
prefix_items[0], path=(*path, "items"), root=root
)
json_schema.pop("prefixItems", None)
else:
json_schema["items"] = {"type": "string"}
# unions
any_of = json_schema.get("anyOf")
@ -281,6 +295,34 @@ def flatten_json_schema(schema: dict) -> dict:
return result
def ensure_array_schemas_have_items(schema: dict) -> dict[str, Any]:
"""
Recursively ensure every JSON schema node with type="array" has an "items" key.
Codex Responses API requires array schemas to specify items. Mutates a deep copy.
"""
result = deepcopy(schema)
def _is_array_schema_type(type_value: Any) -> bool:
if type_value == "array":
return True
if isinstance(type_value, list):
return "array" in type_value
return False
def _ensure(node: Any) -> Any:
if isinstance(node, dict):
if _is_array_schema_type(node.get("type")) and "items" not in node:
node["items"] = {"type": "string"}
for key, value in list(node.items()):
node[key] = _ensure(value)
elif isinstance(node, list):
for idx, value in enumerate(node):
node[idx] = _ensure(value)
return node
return _ensure(result)
def remove_titles_from_schema(schema: dict) -> dict[str, Any]:
def _strip_titles(node: Any) -> Any:

View file

@ -7,7 +7,7 @@ import random
import traceback
from typing import Annotated, List, Literal, Optional, Tuple
import dirtyjson
from fastapi import APIRouter, BackgroundTasks, Body, Depends, HTTPException, Path
from fastapi import APIRouter, BackgroundTasks, Body, Depends, HTTPException, Path, Request
from fastapi.responses import StreamingResponse
from sqlalchemy import delete
from sqlalchemy.ext.asyncio import AsyncSession
@ -138,6 +138,7 @@ async def create_presentation(
include_table_of_contents: Annotated[bool, Body()] = False,
include_title_slide: Annotated[bool, Body()] = True,
web_search: Annotated[bool, Body()] = False,
theme: Annotated[Optional[dict], Body()] = None,
sql_session: AsyncSession = Depends(get_async_session),
):
@ -161,6 +162,7 @@ async def create_presentation(
include_table_of_contents=include_table_of_contents,
include_title_slide=include_title_slide,
web_search=web_search,
theme=theme,
)
sql_session.add(presentation)
@ -317,9 +319,9 @@ async def stream_presentation(
# This will mutate slide and add placeholder assets
process_slide_add_placeholder_assets(slide)
# This will mutate slide
# This will mutate slide - start task immediately so it runs in parallel with next slide LLM generation
async_assets_generation_tasks.append(
process_slide_and_fetch_assets(image_generation_service, slide)
asyncio.create_task(process_slide_and_fetch_assets(image_generation_service, slide))
)
yield SSEResponse(
@ -363,9 +365,11 @@ async def stream_presentation(
@PRESENTATION_ROUTER.patch("/update", response_model=PresentationWithSlides)
async def update_presentation(
request: Request,
id: Annotated[uuid.UUID, Body()],
n_slides: Annotated[Optional[int], Body()] = None,
title: Annotated[Optional[str], Body()] = None,
theme: Annotated[Optional[dict], Body()] = None,
slides: Annotated[Optional[List[SlideModel]], Body()] = None,
sql_session: AsyncSession = Depends(get_async_session),
):
@ -374,12 +378,16 @@ async def update_presentation(
raise HTTPException(status_code=404, detail="Presentation not found")
presentation_update_dict = {}
request_body = await request.json()
theme_provided = "theme" in request_body
if n_slides:
presentation_update_dict["n_slides"] = n_slides
if title:
presentation_update_dict["title"] = title
if theme_provided:
presentation_update_dict["theme"] = theme
if n_slides or title:
if n_slides or title or theme_provided:
presentation.sqlmodel_update(presentation_update_dict)
if slides:
@ -716,9 +724,9 @@ async def generate_presentation_handler(
slides.append(slide)
batch_slides.append(slide)
# Start asset fetch tasks for just-generated slides so they run while next batch is processed
# Start asset fetch tasks immediately so they run in parallel with next batch's LLM calls
asset_tasks = [
process_slide_and_fetch_assets(image_generation_service, slide)
asyncio.create_task(process_slide_and_fetch_assets(image_generation_service, slide))
for slide in batch_slides
]
async_assets_generation_tasks.extend(asset_tasks)

View file

@ -17,4 +17,5 @@ class PresentationWithSlides(BaseModel):
updated_at: datetime
tone: Optional[str] = None
verbosity: Optional[str] = None
theme: Optional[dict] = None
slides: List[SlideModel]

View file

@ -35,6 +35,7 @@ class PresentationModel(SQLModel, table=True):
)
layout: Optional[dict] = Field(sa_column=Column(JSON), default=None)
structure: Optional[dict] = Field(sa_column=Column(JSON), default=None)
theme: Optional[dict] = Field(sa_column=Column(JSON), default=None)
instructions: Optional[str] = Field(sa_column=Column(String), default=None)
tone: Optional[str] = Field(sa_column=Column(String), default=None)
verbosity: Optional[str] = Field(sa_column=Column(String), default=None)
@ -53,6 +54,7 @@ class PresentationModel(SQLModel, table=True):
outlines=self.outlines,
layout=self.layout,
structure=self.structure,
theme=self.theme,
instructions=self.instructions,
tone=self.tone,
verbosity=self.verbosity,

View file

@ -6,6 +6,7 @@ from sqlalchemy.ext.asyncio import (
async_sessionmaker,
AsyncSession,
)
from sqlalchemy import text
from sqlmodel import SQLModel
from models.sql.async_presentation_generation_status import (
@ -66,6 +67,12 @@ async def create_db_and_tables():
],
)
)
# Lightweight schema migration for existing DBs: ensure `presentations.theme` exists.
if database_url.startswith("sqlite"):
result = await conn.execute(text("PRAGMA table_info(presentations)"))
column_names = {row[1] for row in result.fetchall()}
if "theme" not in column_names:
await conn.execute(text("ALTER TABLE presentations ADD COLUMN theme JSON"))
async with container_db_engine.begin() as conn:
await conn.run_sync(

View file

@ -66,6 +66,7 @@ from utils.set_env import (
from utils.llm_provider import get_llm_provider, get_model
from utils.parsers import parse_bool_or_none
from utils.schema_utils import (
ensure_array_schemas_have_items,
ensure_strict_json_schema,
flatten_json_schema,
remove_titles_from_schema,
@ -702,6 +703,7 @@ class LLMClient:
path=(),
root=response_schema,
)
response_schema = ensure_array_schemas_have_items(response_schema)
if use_tool_calls_for_structured_output and depth == 0:
if all_tools is None:
all_tools = []
@ -1599,6 +1601,7 @@ class LLMClient:
path=(),
root=response_schema,
)
response_schema = ensure_array_schemas_have_items(response_schema)
if use_tool_calls_for_structured_output and depth == 0:
if all_tools is None:
@ -1793,28 +1796,16 @@ class LLMClient:
"""
client: AsyncOpenAI = self._client
response_schema = response_format
# Apply strict schema once at root
# Apply strict schema once at root (includes array "items" fix in ensure_strict_json_schema).
if strict and depth == 0:
response_schema = ensure_strict_json_schema(
response_schema,
path=(),
root=response_schema,
)
# Codex Responses API requires all array schemas to specify `items`.
def _fix_arrays(node: Any) -> Any:
if isinstance(node, dict):
# Add default items for arrays missing them
if node.get("type") == "array" and "items" not in node:
node["items"] = {"type": "string"}
for key, value in list(node.items()):
node[key] = _fix_arrays(value)
elif isinstance(node, list):
for idx, value in enumerate(node):
node[idx] = _fix_arrays(value)
return node
response_schema = _fix_arrays(response_schema)
# When we didn't run ensure_strict_json_schema, fix arrays for Codex API (strict=False or depth > 0).
else:
response_schema = ensure_array_schemas_have_items(response_schema)
# Responses API tool format: flat {type, name, description, parameters}
response_schema_tool = {

View file

@ -185,6 +185,20 @@ def ensure_strict_json_schema(
items, path=(*path, "items"), root=root
)
elif typ == "array":
prefix_items = json_schema.get("prefixItems")
if (
isinstance(prefix_items, list)
and len(prefix_items) > 0
and isinstance(prefix_items[0], dict)
):
json_schema["items"] = ensure_strict_json_schema(
prefix_items[0], path=(*path, "items"), root=root
)
json_schema.pop("prefixItems", None)
else:
json_schema["items"] = {"type": "string"}
# unions
any_of = json_schema.get("anyOf")
if isinstance(any_of, list):
@ -326,6 +340,34 @@ def flatten_json_schema(schema: dict) -> dict:
return result
def ensure_array_schemas_have_items(schema: dict) -> dict[str, Any]:
"""
Recursively ensure every JSON schema node with type="array" has an "items" key.
Codex Responses API requires array schemas to specify items. Mutates a deep copy.
"""
result = deepcopy(schema)
def _is_array_schema_type(type_value: Any) -> bool:
if type_value == "array":
return True
if isinstance(type_value, list):
return "array" in type_value
return False
def _ensure(node: Any) -> Any:
if isinstance(node, dict):
if _is_array_schema_type(node.get("type")) and "items" not in node:
node["items"] = {"type": "string"}
for key, value in list(node.items()):
node[key] = _ensure(value)
elif isinstance(node, list):
for idx, value in enumerate(node):
node[idx] = _ensure(value)
return node
return _ensure(result)
def remove_titles_from_schema(schema: dict) -> dict[str, Any]:
def _strip_titles(node: Any) -> Any:

View file

@ -4,10 +4,7 @@ import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import { Button } from "@/components/ui/button";
import { LoadingState, Template } from "../types/index";
import { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";
<<<<<<< feat/revamp_design
import { ChevronRight } from "lucide-react";
=======
>>>>>>> main
interface GenerateButtonProps {
loadingState: LoadingState;

View file

@ -16,10 +16,7 @@ import { useOutlineManagement } from "../hooks/useOutlineManagement";
import { usePresentationGeneration } from "../hooks/usePresentationGeneration";
import TemplateSelection from "./TemplateSelection";
import { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";
<<<<<<< feat/revamp_design
import { Separator } from "@/components/ui/separator";
=======
>>>>>>> main
const OutlinePage: React.FC = () => {
const { presentation_id, outlines } = useSelector(

View file

@ -4,13 +4,9 @@ import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { clearPresentationData } from "@/store/slices/presentationGeneration";
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
<<<<<<< feat/revamp_design
import { LoadingState, TABS } from "../types/index";
=======
import { Template, LoadingState, TABS } from "../types/index";
import { MixpanelEvent, trackEvent } from "@/utils/mixpanel";
import { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";
>>>>>>> main
import { getCustomTemplateDetails } from "@/app/hooks/useCustomTemplates";
import { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";

View file

@ -13,6 +13,8 @@ import { DashboardApi } from "../services/api/dashboard";
import { V1ContentRender } from "../components/V1ContentRender";
import { useFontLoader } from "../hooks/useFontLoad";
import { Theme } from "../services/api/types";
@ -52,6 +54,9 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
const data = await DashboardApi.getPresentation(presentation_id);
dispatch(setPresentationData(data));
setContentLoading(false);
if (data?.theme) {
applyTheme(data.theme);
}
} catch (error) {
setError(true);
toast.error("Failed to load presentation");
@ -60,6 +65,43 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
}
};
const applyTheme = async (theme: Theme) => {
const element = document.getElementById('presentation-slides-wrapper')
if (!element) return;
if (!theme || !theme.data) { return; }
if (!theme.data.colors['graph_0']) { return; }
const cssVariables = {
'--primary-color': theme.data.colors['primary'],
'--background-color': theme.data.colors['background'],
'--card-color': theme.data.colors['card'],
'--stroke': theme.data.colors['stroke'],
'--primary-text': theme.data.colors['primary_text'],
'--background-text': theme.data.colors['background_text'],
'--graph-0': theme.data.colors['graph_0'],
'--graph-1': theme.data.colors['graph_1'],
'--graph-2': theme.data.colors['graph_2'],
'--graph-3': theme.data.colors['graph_3'],
'--graph-4': theme.data.colors['graph_4'],
'--graph-5': theme.data.colors['graph_5'],
'--graph-6': theme.data.colors['graph_6'],
'--graph-7': theme.data.colors['graph_7'],
'--graph-8': theme.data.colors['graph_8'],
'--graph-9': theme.data.colors['graph_9'],
}
Object.entries(cssVariables).forEach(([key, value]) => {
element.style.setProperty(key, value)
})
useFontLoader({ [theme.data.fonts.textFont.name]: theme.data.fonts.textFont.url })
// Apply fonts to preview container
element.style.setProperty('font-family', `"${theme.data.fonts.textFont.name}"`)
element.style.setProperty('--heading-font-family', `"${theme.data.fonts.textFont.name}"`)
element.style.setProperty('--body-font-family', `"${theme.data.fonts.textFont.name}"`)
// Update the Presentation content with theme
}
// Regular view
return (
<div className="flex overflow-hidden flex-col">

View file

@ -3,14 +3,16 @@ import React, { useState } from 'react'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Palette } from 'lucide-react';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { updateTheme } from '@/store/slices/presentationGeneration';
import { useRouter } from 'next/navigation';
import { useFontLoader } from '../../hooks/useFontLoad';
import { RootState } from '@/store/store';
const ThemeSelector = ({ presentation_id, current_theme, themes: allThemes }: { presentation_id: string, current_theme: any, themes: any[] }) => {
const [currentTheme, setCurrentTheme] = useState<any>(current_theme)
const dispatch = useDispatch()
const router = useRouter()
const { presentationData } = useSelector((state: RootState) => state.presentationGeneration)
const applyTheme = async (theme: any) => {
const element = document.getElementById('presentation-slides-wrapper')
if (!element) return;
@ -44,6 +46,7 @@ const ThemeSelector = ({ presentation_id, current_theme, themes: allThemes }: {
// Apply fonts to preview container
element.style.setProperty('font-family', `"${theme.data.fonts.textFont.name}"`)
element.style.setProperty('--heading-font-family', `"${theme.data.fonts.textFont.name}"`)
element.style.setProperty('--body-font-family', `"${theme.data.fonts.textFont.name}"`)
dispatch(updateTheme(theme))
}
@ -70,9 +73,10 @@ const ThemeSelector = ({ presentation_id, current_theme, themes: allThemes }: {
const resetTheme = async () => {
clearTheme();
dispatch(updateTheme({} as any))
dispatch(updateTheme(null))
}
console.log('presentation data', presentationData)
return (
<Popover>

View file

@ -3,7 +3,9 @@ import { useDispatch } from "react-redux";
import { toast } from "sonner";
import { setPresentationData } from "@/store/slices/presentationGeneration";
import { DashboardApi } from '../../services/api/dashboard';
import { clearHistory } from "@/store/slices/undoRedoSlice";
import { clearHistory } from "@/store/slices/undoRedoSlice";
import { useFontLoader } from "../../hooks/useFontLoad";
import { Theme } from "../../services/api/types";
export const usePresentationData = (
@ -13,6 +15,41 @@ export const usePresentationData = (
) => {
const dispatch = useDispatch();
const applyTheme = async (theme: Theme) => {
const element = document.getElementById('presentation-slides-wrapper')
if (!element) return;
if (!theme || !theme.data) { return; }
if (!theme.data.colors['graph_0']) { return; }
const cssVariables = {
'--primary-color': theme.data.colors['primary'],
'--background-color': theme.data.colors['background'],
'--card-color': theme.data.colors['card'],
'--stroke': theme.data.colors['stroke'],
'--primary-text': theme.data.colors['primary_text'],
'--background-text': theme.data.colors['background_text'],
'--graph-0': theme.data.colors['graph_0'],
'--graph-1': theme.data.colors['graph_1'],
'--graph-2': theme.data.colors['graph_2'],
'--graph-3': theme.data.colors['graph_3'],
'--graph-4': theme.data.colors['graph_4'],
'--graph-5': theme.data.colors['graph_5'],
'--graph-6': theme.data.colors['graph_6'],
'--graph-7': theme.data.colors['graph_7'],
'--graph-8': theme.data.colors['graph_8'],
'--graph-9': theme.data.colors['graph_9'],
}
Object.entries(cssVariables).forEach(([key, value]) => {
element.style.setProperty(key, value)
})
useFontLoader({ [theme.data.fonts.textFont.name]: theme.data.fonts.textFont.url })
// Apply fonts to preview container
element.style.setProperty('font-family', `"${theme.data.fonts.textFont.name}"`)
element.style.setProperty('--heading-font-family', `"${theme.data.fonts.textFont.name}"`)
element.style.setProperty('--body-font-family', `"${theme.data.fonts.textFont.name}"`)
// Update the Presentation content with theme
}
const fetchUserSlides = useCallback(async () => {
try {
const data = await DashboardApi.getPresentation(presentationId);
@ -21,6 +58,9 @@ export const usePresentationData = (
dispatch(clearHistory());
setLoading(false);
}
if (data?.theme) {
applyTheme(data.theme);
}
} catch (error) {
setError(true);
toast.error("Failed to load presentation");

View file

@ -13,13 +13,13 @@ export interface PresentationResponse {
n_slides: number;
prompt: string;
summary: string | null;
theme: string;
titles: string[];
user_id: string;
vector_store: any;
theme: Record<string, any> | null;
titles: string[];
user_id: string;
vector_store: any;
thumbnail: string;
slides: any[];
thumbnail: string;
slides: any[];
}
export class DashboardApi {
@ -32,20 +32,20 @@ export class DashboardApi {
method: "GET",
}
);
// Handle the special case where 404 means "no presentations found"
if (response.status === 404) {
console.log("No presentations found");
return [];
}
return await ApiResponseHandler.handleResponse(response, "Failed to fetch presentations");
} catch (error) {
console.error("Error fetching presentations:", error);
throw error;
}
}
static async getPresentation(id: string) {
try {
const response = await fetch(
@ -54,14 +54,14 @@ export class DashboardApi {
method: "GET",
}
);
return await ApiResponseHandler.handleResponse(response, "Presentation not found");
} catch (error) {
console.error("Error fetching presentation:", error);
throw error;
}
}
static async deletePresentation(presentation_id: string) {
try {
const response = await fetch(

View file

@ -5,12 +5,8 @@ import { Card } from "@/components/ui/card";
import { ExternalLink, Loader2, Plus } from "lucide-react";
import { templates } from "@/app/presentation-templates";
<<<<<<< feat/revamp_design
import { TemplateLayoutsWithSettings, TemplateWithData } from "@/app/presentation-templates/utils";
=======
import type { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";
import { TemplateWithData } from "@/app/presentation-templates/utils";
>>>>>>> main
import {
useCustomTemplateSummaries,
useCustomTemplatePreview,

View file

@ -33,10 +33,10 @@ export default function OpenAIConfig({
}: OpenAIConfigProps) {
const [openModelSelect, setOpenModelSelect] = useState(false);
const [availableModels, setAvailableModels] = useState<string[]>([]);
const [modelsLoading, setModelsLoading] = useState(false);
const [modelsChecked, setModelsChecked] = useState(false);
const [apiKey, setApiKey] = useState(openaiApiKey);
const isImageGenerationDisabled = llmConfig.DISABLE_IMAGE_GENERATION ?? false;
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";

View file

@ -13,7 +13,7 @@ export interface PresentationData {
n_slides: number;
title: string;
slides: any;
theme: Theme;
theme: Theme | null;
}
interface PresentationGenerationState {
@ -379,7 +379,7 @@ const presentationGenerationSlice = createSlice({
}
}
},
updateTheme: (state, action: PayloadAction<Theme>) => {
updateTheme: (state, action: PayloadAction<Theme | null>) => {
if (state.presentationData) {
state.presentationData['theme'] = action.payload;
}