feat: completed theme & custom theme UI
This commit is contained in:
commit
28f2b18e06
21 changed files with 235 additions and 79 deletions
4
electron/package-lock.json
generated
4
electron/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 135–155).
|
||||
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 = {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue