Merge pull request #187 from presenton/feat/improved-document-parsing

feat/improved document parsing
This commit is contained in:
Saurav Niraula 2025-08-04 15:15:39 +05:45 committed by GitHub
commit 6c193833fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
58 changed files with 3872 additions and 1071 deletions

View file

@ -1,13 +1,9 @@
.venv
.env
.next
out
build
.git
.gitignore
tmp
debug
.fastembed_cache
servers/fastapi/tmp
servers/fastapi/debug
servers/nextjs/node_modules
servers/fastapi/.venv
servers/nextjs/node_modules
servers/nextjs/.next

3
.gitignore vendored
View file

@ -12,4 +12,5 @@ tmp
debug
.fastembed_cache
my-doc.txt
generated_models
generated_models
nltk

View file

@ -24,9 +24,10 @@ ENV PYTHONPATH="${PYTHONPATH}:/app/servers/fastapi"
RUN curl -fsSL https://ollama.com/install.sh | sh
# Install dependencies for FastAPI
COPY servers/fastapi/requirements.txt ./
RUN pip install -r requirements.txt
RUN pip install fastmcp
RUN pip install aiohttp aiomysql aiosqlite asyncpg fastapi[standard] \
pathvalidate pdfplumber nltk chromadb sqlmodel redis \
anthropic google-genai openai fastmcp
RUN pip install docling --extra-index-url https://download.pytorch.org/whl/cpu
# Install dependencies for Next.js
WORKDIR /app/servers/nextjs

View file

@ -26,9 +26,10 @@ ENV PYTHONPATH="${PYTHONPATH}:/app/servers/fastapi"
RUN curl -fsSL http://ollama.com/install.sh | sh
# Install dependencies for FastAPI
COPY servers/fastapi/requirements.txt ./
RUN pip install -r requirements.txt
RUN pip install fastmcp
RUN pip install aiohttp aiomysql aiosqlite asyncpg fastapi[standard] \
pathvalidate pdfplumber nltk chromadb sqlmodel redis \
anthropic google-genai openai fastmcp
RUN pip install docling --extra-index-url https://download.pytorch.org/whl/cpu
# Install dependencies for Next.js
WORKDIR /node_dependencies

View file

@ -89,6 +89,8 @@ You may want to directly provide your API KEYS as environment variables and keep
- **CUSTOM_LLM_URL=[Custom OpenAI Compatible URL]**: Provide this if **LLM** is set to **custom**
- **CUSTOM_LLM_API_KEY=[Custom OpenAI Compatible API KEY]**: Provide this if **LLM** is set to **custom**
- **CUSTOM_MODEL=[Custom Model ID]**: Provide this if **LLM** is set to **custom**
- **TOOL_CALLS=[Enable/Disable Tool Calls on Custom LLM]**: If **true**, **LLM** will use Tool Call instead of Json Schema for Structured Output.
- **DISABLE_THINKING=[Enable/Disable Thinking on Custom LLM]**: If **true**, Thinking will be disabled.
You can also set the following environment variables to customize the image generation provider and API keys:

View file

@ -0,0 +1 @@
3.11

View file

@ -7,7 +7,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
from models.presentation_outline_model import PresentationOutlineModel
from models.sql.presentation import PresentationModel
from models.sse_response import SSECompleteResponse, SSEResponse, SSEStatusResponse
from services import TEMP_FILE_SERVICE
from services.database import get_async_session
from services.documents_loader import DocumentsLoader
from services.score_based_chunker import ScoreBasedChunker
from utils.llm_calls.generate_presentation_outlines import generate_ppt_outline
OUTLINES_ROUTER = APIRouter(prefix="/outlines", tags=["Outlines"])
@ -22,38 +25,66 @@ async def stream_outlines(
if not presentation:
raise HTTPException(status_code=404, detail="Presentation not found")
temp_dir = TEMP_FILE_SERVICE.create_temp_dir()
async def inner():
yield SSEStatusResponse(
status="Generating presentation outlines..."
).to_string()
presentation_content_text = ""
async for chunk in generate_ppt_outline(
presentation.prompt,
presentation.n_slides,
presentation.language,
presentation.summary,
):
# Give control to the event loop
await asyncio.sleep(0)
presentation_outlines = None
additional_context = ""
if presentation.file_paths:
documents_loader = DocumentsLoader(file_paths=presentation.file_paths)
await documents_loader.load_documents(temp_dir)
documents = documents_loader.documents
if documents:
additional_context = documents[0]
chunker = ScoreBasedChunker()
try:
chunks = await chunker.get_n_chunks(
documents[0], presentation.n_slides
)
presentation_outlines = PresentationOutlineModel(
slides=[chunk.to_slide_outline() for chunk in chunks]
)
except Exception as e:
print(e)
yield SSEResponse(
event="response",
data=json.dumps({"type": "chunk", "chunk": chunk}),
).to_string()
presentation_content_text += chunk
if not presentation_outlines:
presentation_outlines_text = ""
async for chunk in generate_ppt_outline(
presentation.prompt,
presentation.n_slides,
presentation.language,
additional_context,
):
# Give control to the event loop
await asyncio.sleep(0)
presentation_content_json = json.loads(presentation_content_text)
yield SSEResponse(
event="response",
data=json.dumps({"type": "chunk", "chunk": chunk}),
).to_string()
presentation_outlines_text += chunk
presentation_content = PresentationOutlineModel(**presentation_content_json)
presentation_content.slides = presentation_content.slides[
presentation_outlines_json = json.loads(presentation_outlines_text)
presentation_outlines = PresentationOutlineModel(
**presentation_outlines_json
)
presentation_outlines.slides = presentation_outlines.slides[
: presentation.n_slides
]
presentation.title = presentation_content.title
presentation.outlines = [
each.model_dump() for each in presentation_content.slides
]
presentation.outlines = presentation_outlines.model_dump()
presentation.title = (
presentation_outlines.slides[0][:50]
.replace("#", "")
.replace("/", "")
.replace("\\", "")
.replace("\n", "")
)
sql_session.add(presentation)
await sql_session.commit()

View file

@ -2,7 +2,6 @@ import asyncio
import json
import os
import random
import importlib
from typing import Annotated, List, Literal, Optional
from fastapi import APIRouter, Body, Depends, File, HTTPException, UploadFile
from fastapi.responses import StreamingResponse
@ -12,14 +11,12 @@ from sqlmodel import select
from constants.documents import UPLOAD_ACCEPTED_FILE_TYPES
from models.presentation_and_path import PresentationPathAndEditPath
from models.presentation_from_template import GetPresentationUsingTemplateRequest
from models.presentation_outline_model import (
PresentationOutlineModel,
SlideOutlineModel,
)
from models.presentation_outline_model import PresentationOutlineModel
from models.pptx_models import PptxPresentationModel
from models.presentation_layout import PresentationLayoutModel
from models.presentation_structure_model import PresentationStructureModel
from models.presentation_with_slides import PresentationWithSlides
from services.score_based_chunker import ScoreBasedChunker
from utils.get_layout_by_name import get_layout_by_name
from services.icon_finder_service import IconFinderService
from services.image_generation_service import ImageGenerationService
@ -34,7 +31,6 @@ from services.documents_loader import DocumentsLoader
from models.sql.presentation import PresentationModel
from services.pptx_presentation_creator import PptxPresentationCreator
from utils.asset_directory_utils import get_exports_directory, get_images_directory
from utils.llm_calls.generate_document_summary import generate_document_summary
from utils.llm_calls.generate_presentation_structure import (
generate_presentation_structure,
)
@ -113,20 +109,12 @@ async def create_presentation(
):
presentation_id = get_random_uuid()
summary = None
if file_paths:
temp_dir = TEMP_FILE_SERVICE.create_temp_dir(presentation_id)
documents_loader = DocumentsLoader(file_paths=file_paths)
await documents_loader.load_documents(temp_dir)
summary = await generate_document_summary(documents_loader.documents)
presentation = PresentationModel(
id=presentation_id,
prompt=prompt,
n_slides=n_slides,
language=language,
summary=summary,
file_paths=file_paths,
)
sql_session.add(presentation)
@ -138,7 +126,7 @@ async def create_presentation(
@PRESENTATION_ROUTER.post("/prepare", response_model=PresentationModel)
async def prepare_presentation(
presentation_id: Annotated[str, Body()],
outlines: Annotated[List[SlideOutlineModel], Body()],
outlines: Annotated[List[str], Body()],
layout: Annotated[PresentationLayoutModel, Body()],
title: Annotated[Optional[str], Body()] = None,
sql_session: AsyncSession = Depends(get_async_session),
@ -173,7 +161,7 @@ async def prepare_presentation(
presentation_structure.slides[index] = random_slide_index
sql_session.add(presentation)
presentation.outlines = [each.model_dump() for each in outlines]
presentation.outlines = PresentationOutlineModel(slides=outlines).model_dump()
presentation.title = title or presentation.title
presentation.set_layout(layout)
presentation.set_structure(presentation_structure)
@ -328,37 +316,48 @@ async def generate_presentation_api(
presentation_id = get_random_uuid()
temp_dir = TEMP_FILE_SERVICE.create_temp_dir()
# 1. Save uploaded files
file_paths = []
if files:
temp_dir = TEMP_FILE_SERVICE.create_temp_dir()
for upload in files:
file_path = os.path.join(temp_dir, upload.filename)
with open(file_path, "wb") as f:
f.write(await upload.read())
file_paths.append(file_path)
# 2. Create Presentation Summary (if documents are provided)
summary = None
# 3. Generate Outlines
presentation_outlines = None
additional_context = ""
if file_paths:
temp_dir = TEMP_FILE_SERVICE.create_temp_dir(presentation_id)
documents_loader = DocumentsLoader(file_paths=file_paths)
await documents_loader.load_documents(temp_dir)
summary = await generate_document_summary(documents_loader.documents)
documents = documents_loader.documents
if documents:
additional_context = documents[0]
chunker = ScoreBasedChunker()
try:
chunks = await chunker.get_n_chunks(documents[0], n_slides)
presentation_outlines = PresentationOutlineModel(
slides=[chunk.to_slide_outline() for chunk in chunks]
)
except Exception as e:
print(e)
# 3. Generate Outlines
presentation_content_text = ""
async for chunk in generate_ppt_outline(
prompt,
n_slides,
language,
summary,
):
presentation_content_text += chunk
if not presentation_outlines:
presentation_outlines_text = ""
async for chunk in generate_ppt_outline(
prompt,
n_slides,
language,
additional_context,
):
presentation_outlines_text += chunk
presentation_content_json = json.loads(presentation_content_text)
presentation_content = PresentationOutlineModel(**presentation_content_json)
outlines = presentation_content.slides[:n_slides]
presentation_outlines_json = json.loads(presentation_outlines_text)
presentation_outlines = PresentationOutlineModel(**presentation_outlines_json)
outlines = presentation_outlines.slides[:n_slides]
total_outlines = len(outlines)
print("-" * 40)
@ -374,11 +373,8 @@ async def generate_presentation_api(
else:
presentation_structure: PresentationStructureModel = (
await generate_presentation_structure(
presentation_outline=PresentationOutlineModel(
title=presentation_content.title,
slides=outlines,
),
presentation_layout=layout_model,
presentation_outlines,
layout_model,
)
)
@ -397,9 +393,7 @@ async def generate_presentation_api(
prompt=prompt,
n_slides=n_slides,
language=language,
title=presentation_content.title,
summary=summary,
outlines=[each.model_dump() for each in outlines],
outlines=presentation_outlines.model_dump(),
layout=layout_model.model_dump(),
structure=presentation_structure.model_dump(),
)
@ -445,7 +439,7 @@ async def generate_presentation_api(
# 9. Export
presentation_and_path = await export_presentation(
presentation_id, presentation_content.title, export_as
presentation_id, presentation.title or get_random_uuid(), export_as
)
return PresentationPathAndEditPath(
@ -482,7 +476,7 @@ async def from_template(
await sql_session.commit()
presentation_and_path = await export_presentation(
new_presentation.id, new_presentation.title, data.export_as
new_presentation.id, new_presentation.title or get_random_uuid(), data.export_as
)
return PresentationPathAndEditPath(

View file

@ -1,419 +0,0 @@
import json
from typing import List, Literal, Optional
from pydantic import BaseModel, Field, HttpUrl, EmailStr
from models.presentation_layout import PresentationLayoutModel, SlideLayoutModel
from models.presentation_outline_model import PresentationOutlineModel
from utils.dict_utils import get_dict_at_path, get_dict_paths_with_key
from utils.schema_utils import remove_fields_from_schema
class ContactInfoModel(BaseModel):
email: Optional[EmailStr] = Field(None, description="Contact email")
phone: Optional[str] = Field(
None, min_length=5, max_length=50, description="Contact phone number"
)
website: Optional[HttpUrl] = Field(None, description="Website URL")
class ImageModel(BaseModel):
__image_url__: str = Field(description="Image URL")
__image_prompt__: str = Field(description="Image prompt")
# First Slide Layout
class FirstSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Main title of the presentation",
)
subtitle: Optional[str] = Field(
min_length=10, max_length=200, description="Optional subtitle or tagline"
)
author: Optional[str] = Field(
min_length=2,
max_length=100,
description="Author or presenter name",
)
date: Optional[str] = Field(description="Presentation date")
company: Optional[str] = Field(
min_length=2,
max_length=100,
description="Company or organization name",
)
backgroundImage: Optional[ImageModel] = Field(
description="Background image for the slide"
)
# Bullet Point Slide Layout
class BulletPointSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
icon: Optional[str] = Field(description="Icon to display in the slide")
bulletPoints: List[str] = Field(
min_length=2,
max_length=8,
description="List of bullet points (2-8 items)",
)
# Image Slide Layout
class ImageSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
image: HttpUrl = Field(
description="Main image URL",
)
imageCaption: Optional[str] = Field(
min_length=5,
max_length=200,
description="Optional image caption or description",
)
content: Optional[str] = Field(
min_length=10,
max_length=600,
description="Optional supporting content text",
)
backgroundImage: Optional[HttpUrl] = Field(
description="URL to background image for the slide"
)
# Statistics Slide Layout
class StatisticItemModel(BaseModel):
value: str = Field(
min_length=1,
max_length=20,
description="Statistical value (e.g., '250%', '$1.2M', '99.9%')",
)
label: str = Field(
min_length=3, max_length=100, description="Description of the statistic"
)
trend: Optional[str] = Field(
description="Trend direction indicator", pattern="^(up|down|neutral)$"
)
context: Optional[str] = Field(
min_length=5,
max_length=200,
description="Additional context or time period",
)
class StatisticsSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
statistics: List[StatisticItemModel] = Field(
min_length=2,
max_length=6,
description="List of statistics (2-6 items)",
)
backgroundImage: Optional[HttpUrl] = Field(
description="URL to background image for the slide"
)
# Quote Slide Layout
class QuoteSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
quote: str = Field(
min_length=10,
max_length=500,
description="The main quote or testimonial",
)
author: str = Field(
min_length=2,
max_length=100,
description="Quote author name",
)
authorTitle: Optional[str] = Field(
min_length=2, max_length=100, description="Author job title or position"
)
company: Optional[str] = Field(
min_length=2, max_length=100, description="Author company or organization"
)
authorImage: Optional[HttpUrl] = Field(description="URL to author photo")
backgroundImage: Optional[HttpUrl] = Field(
description="URL to background image for the slide"
)
# Timeline Slide Layout
class TimelineItemModel(BaseModel):
date: str = Field(min_length=2, max_length=50, description="Date or time period")
title: str = Field(
min_length=3, max_length=100, description="Event or milestone title"
)
description: str = Field(
min_length=10, max_length=300, description="Event description"
)
status: str = Field(
description="Timeline item status",
pattern="^(completed|current|upcoming)$",
)
class TimelineSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
timelineItems: List[TimelineItemModel] = Field(
min_length=2,
max_length=6,
description="Timeline events (2-6 items)",
)
backgroundImage: Optional[HttpUrl] = Field(
description="URL to background image for the slide"
)
# Team Slide Layout
class TeamMemberModel(BaseModel):
name: str = Field(min_length=2, max_length=100, description="Team member name")
title: str = Field(min_length=2, max_length=100, description="Job title or role")
image: Optional[HttpUrl] = Field(description="URL to team member photo")
bio: Optional[str] = Field(
min_length=10,
max_length=300,
description="Brief biography or description",
)
email: Optional[EmailStr] = Field(description="Contact email")
linkedin: Optional[HttpUrl] = Field(description="LinkedIn profile URL")
class TeamSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
min_length=3,
max_length=150,
description="Optional subtitle or team description",
)
teamMembers: List[TeamMemberModel] = Field(
min_length=1,
max_length=6,
description="Team members (1-6 people)",
)
backgroundImage: Optional[HttpUrl] = Field(
description="URL to background image for the slide"
)
# Process Slide Layout
class ProcessStepModel(BaseModel):
step: int = Field(ge=1, le=10, description="Step number")
title: str = Field(min_length=3, max_length=100, description="Step title")
description: str = Field(
min_length=10, max_length=200, description="Step description"
)
class ProcessSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
processSteps: List[ProcessStepModel] = Field(
min_length=2,
max_length=6,
description="Process steps (2-6 items)",
)
backgroundImage: Optional[HttpUrl] = Field(
description="URL to background image for the slide"
)
# Two Column Slide Layout
class ColumnContentModel(BaseModel):
title: str = Field(min_length=3, max_length=100, description="Column title")
content: str = Field(min_length=10, max_length=800, description="Column content")
class TwoColumnSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
leftColumn: ColumnContentModel = Field(
description="Left column content",
)
rightColumn: ColumnContentModel = Field(
description="Right column content",
)
backgroundImage: Optional[HttpUrl] = Field(
description="URL to background image for the slide"
)
# Conclusion Slide Layout
class ConclusionSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
keyTakeaways: List[str] = Field(
min_length=2,
max_length=6,
description="Key takeaways or summary points (2-6 items)",
)
callToAction: Optional[str] = Field(
min_length=5,
max_length=150,
description="Optional call to action or next steps",
)
contactInfo: Optional[ContactInfoModel] = Field(
description="Optional contact information"
)
backgroundImage: Optional[HttpUrl] = Field(
description="URL to background image for the slide"
)
# Content Slide Layout
class ContentSlideModel(BaseModel):
title: str = Field(
min_length=3,
max_length=100,
description="Title of the slide",
)
subtitle: Optional[str] = Field(
min_length=3,
max_length=150,
description="Optional subtitle or description",
)
content: str = Field(
min_length=10,
max_length=1000,
description="Main content text",
)
backgroundImage: Optional[HttpUrl] = Field(
description="URL to background image for the slide"
)
# Create the presentation layout with all slide types
presentation_layout = PresentationLayoutModel(
name="Complete Presentation Layout",
slides=[
SlideLayoutModel(
id="first-slide",
name="First Slide",
json_schema=FirstSlideModel.model_json_schema(),
),
# SlideLayoutModel(
# id="bullet-point-slide",
# name="Bullet Point Slide",
# json_schema=BulletPointSlideModel.model_json_schema(),
# ),
# SlideLayoutModel(
# id="image-slide",
# name="Image Slide",
# json_schema=ImageSlideModel.model_json_schema(),
# ),
# SlideLayoutModel(
# id="statistics-slide",
# name="Statistics Slide",
# json_schema=StatisticsSlideModel.model_json_schema(),
# ),
# SlideLayoutModel(
# id="quote-slide",
# name="Quote Slide",
# json_schema=QuoteSlideModel.model_json_schema(),
# ),
# SlideLayoutModel(
# id="timeline-slide",
# name="Timeline Slide",
# json_schema=TimelineSlideModel.model_json_schema(),
# ),
# SlideLayoutModel(
# id="team-slide",
# name="Team Slide",
# json_schema=TeamSlideModel.model_json_schema(),
# ),
# SlideLayoutModel(
# id="process-slide",
# name="Process Slide",
# json_schema=ProcessSlideModel.model_json_schema(),
# ),
# SlideLayoutModel(
# id="two-column-slide",
# name="Two Column Slide",
# json_schema=TwoColumnSlideModel.model_json_schema(),
# ),
# SlideLayoutModel(
# id="conclusion-slide",
# name="Conclusion Slide",
# json_schema=ConclusionSlideModel.model_json_schema(),
# ),
# SlideLayoutModel(
# id="content-slide",
# name="Content Slide",
# json_schema=ContentSlideModel.model_json_schema(),
# ),
],
)
print(json.dumps(StatisticsSlideModel.model_json_schema()))

View file

@ -0,0 +1,11 @@
from pydantic import BaseModel
class DocumentChunk(BaseModel):
heading: str
content: str
heading_index: int
score: float
def to_slide_outline(self) -> str:
return f"{self.heading}\n{self.content}"

View file

@ -1,31 +1,13 @@
from typing import List, Optional
from pydantic import BaseModel, Field
class SlideOutlineModel(BaseModel):
title: str = Field(
description="Title of the slide in about 3 to 5 words",
)
body: str = Field(
description="Content of the slide in markdown format",
)
from typing import List
from pydantic import BaseModel
class PresentationOutlineModel(BaseModel):
title: str = Field(
description="Title of the presentation in about 3 to 8 words",
)
slides: List[SlideOutlineModel] = Field(description="List of slides")
slides: List[str]
def to_string(self):
message = f"# Presentation Title: {self.title} \n\n"
message = ""
for i, slide in enumerate(self.slides):
message += f"## Slide {i+1}:\n"
message += f" - Title: {slide.title} \n"
message += f" - Body: {slide.body} \n"
# if self.notes:
# message += f"# Notes: \n"
# for note in self.notes:
# message += f" - {note} \n"
message += f" - Content: {slide} \n"
return message

View file

@ -4,7 +4,7 @@ from datetime import datetime
from pydantic import BaseModel
from models.presentation_layout import PresentationLayoutModel
from models.presentation_outline_model import SlideOutlineModel
from models.presentation_outline_model import PresentationOutlineModel
from models.presentation_structure_model import PresentationStructureModel
from models.sql.presentation import PresentationModel
from models.sql.slide import SlideModel
@ -16,9 +16,7 @@ class PresentationWithSlides(BaseModel):
n_slides: int
language: str
title: Optional[str] = None
notes: Optional[List[str]]
outlines: Optional[List[SlideOutlineModel]]
summary: Optional[str]
outlines: Optional[PresentationOutlineModel]
created_at: datetime
updated_at: datetime
layout: Optional[PresentationLayoutModel]

View file

@ -4,10 +4,7 @@ from sqlalchemy import JSON, Column, DateTime
from sqlmodel import SQLModel, Field
from models.presentation_layout import PresentationLayoutModel
from models.presentation_outline_model import (
PresentationOutlineModel,
SlideOutlineModel,
)
from models.presentation_outline_model import PresentationOutlineModel
from models.presentation_structure_model import PresentationStructureModel
from utils.randomizers import get_random_uuid
@ -18,9 +15,8 @@ class PresentationModel(SQLModel, table=True):
n_slides: int
language: str
title: Optional[str] = None
notes: Optional[List[str]] = Field(sa_column=Column(JSON), default=None)
outlines: Optional[List[dict]] = Field(sa_column=Column(JSON), default=None)
summary: Optional[str] = None
file_paths: Optional[List[str]] = Field(sa_column=Column(JSON), default=None)
outlines: Optional[dict] = Field(sa_column=Column(JSON), default=None)
created_at: datetime = Field(sa_column=Column(DateTime, default=datetime.now))
updated_at: datetime = Field(sa_column=Column(DateTime, default=datetime.now))
layout: Optional[dict] = Field(sa_column=Column(JSON), default=None)
@ -33,9 +29,8 @@ class PresentationModel(SQLModel, table=True):
n_slides=self.n_slides,
language=self.language,
title=self.title,
notes=self.notes,
file_paths=self.file_paths,
outlines=self.outlines,
summary=self.summary,
layout=self.layout,
structure=self.structure,
)
@ -43,11 +38,7 @@ class PresentationModel(SQLModel, table=True):
def get_presentation_outline(self):
if not self.outlines:
return None
return PresentationOutlineModel(
title=self.title,
slides=[SlideOutlineModel(**each) for each in self.outlines],
# notes=self.notes,
)
return PresentationOutlineModel(**self.outlines)
def get_layout(self):
return PresentationLayoutModel(**self.layout)

View file

@ -32,4 +32,6 @@ class UserConfig(BaseModel):
PIXABAY_API_KEY: Optional[str] = None
# Reasoning
TOOL_CALLS: Optional[bool] = None
DISABLE_THINKING: Optional[bool] = None
EXTENDED_REASONING: Optional[bool] = None

View file

@ -0,0 +1,28 @@
[project]
name = "presenton-backend"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.11,<3.12"
dependencies = [
"aiohttp>=3.12.15",
"aiomysql>=0.2.0",
"aiosqlite>=0.21.0",
"anthropic>=0.60.0",
"asyncpg>=0.30.0",
"chromadb>=1.0.15",
"docling>=2.43.0",
"fastapi[standard]>=0.116.1",
"fastmcp>=2.11.0",
"google-genai>=1.28.0",
"nltk>=3.9.1",
"openai>=1.98.0",
"pathvalidate>=3.3.1",
"pdfplumber>=0.11.7",
"python-pptx>=1.0.2",
"redis>=6.2.0",
"sqlmodel>=0.0.24",
]
[[tool.uv.index]]
url = "https://download.pytorch.org/whl/cpu"

View file

@ -1,149 +0,0 @@
aiohappyeyeballs==2.6.1
aiohttp==3.12.14
aiomysql==0.2.0
aiosignal==1.4.0
aiosqlite==0.21.0
annotated-types==0.7.0
anthropic==0.60.0
anyio==4.9.0
argcomplete==3.6.2
async-timeout==5.0.1
asyncpg==0.30.0
attrs==25.3.0
backoff==2.2.1
bcrypt==4.3.0
black==25.1.0
build==1.2.2.post1
cachetools==5.5.2
certifi==2025.7.14
cffi==1.17.1
charset-normalizer==3.4.2
chromadb==1.0.15
click==8.2.1
coloredlogs==15.0.1
cryptography==45.0.5
distro==1.9.0
dnspython==2.7.0
durationpy==0.10
email_validator==2.2.0
fastapi==0.116.1
fastapi-cli==0.0.8
fastapi-cloud-cli==0.1.4
fastembed==0.7.1
filelock==3.18.0
flatbuffers==25.2.10
frozenlist==1.7.0
fsspec==2025.7.0
genson==1.3.0
google-auth==2.40.3
google-genai==1.25.0
googleapis-common-protos==1.70.0
greenlet==3.2.3
grpcio==1.74.0
h11==0.16.0
h2==4.2.0
hf-xet==1.1.5
hpack==4.1.0
httpcore==1.0.9
httptools==0.6.4
httpx==0.28.1
huggingface-hub==0.34.1
humanfriendly==10.0
hyperframe==6.1.0
idna==3.10
importlib_metadata==8.7.0
importlib_resources==6.5.2
inflect==7.5.0
iniconfig==2.1.0
isort==6.0.1
Jinja2==3.1.6
jiter==0.10.0
jsonschema==4.25.0
jsonschema-specifications==2025.4.1
kubernetes==33.1.0
loguru==0.7.3
lxml==6.0.0
markdown-it-py==3.0.0
MarkupSafe==3.0.2
mdurl==0.1.2
mmh3==5.1.0
more-itertools==10.7.0
mpmath==1.3.0
multidict==6.6.3
mypy_extensions==1.1.0
numpy==2.3.2
oauthlib==3.3.1
onnxruntime==1.22.1
openai==1.95.1
opentelemetry-api==1.35.0
opentelemetry-exporter-otlp-proto-common==1.35.0
opentelemetry-exporter-otlp-proto-grpc==1.35.0
opentelemetry-proto==1.35.0
opentelemetry-sdk==1.35.0
opentelemetry-semantic-conventions==0.56b0
orjson==3.11.1
overrides==7.7.0
packaging==25.0
pathspec==0.12.1
pathvalidate==3.3.1
pdfminer.six==20250506
pdfplumber==0.11.7
pillow==11.3.0
platformdirs==4.3.8
pluggy==1.6.0
portalocker==3.2.0
posthog==5.4.0
propcache==0.3.2
protobuf==6.31.1
py_rust_stemmers==0.1.5
pyasn1==0.6.1
pyasn1_modules==0.4.2
pybase64==1.4.2
pycparser==2.22
pydantic==2.11.7
pydantic_core==2.33.2
Pygments==2.19.2
pypdfium2==4.30.1
PyPika==0.48.9
pyproject_hooks==1.2.0
pytest==8.4.1
python-dateutil==2.9.0.post0
python-docx==1.2.0
python-dotenv==1.1.1
python-multipart==0.0.20
python-pptx==1.0.2
PyYAML==6.0.2
redis==6.2.0
referencing==0.36.2
requests==2.32.4
requests-oauthlib==2.0.0
rich==14.0.0
rich-toolkit==0.14.8
rignore==0.6.2
rpds-py==0.26.0
rsa==4.9.1
sentry-sdk==2.32.0
shellingham==1.5.4
six==1.17.0
sniffio==1.3.1
SQLAlchemy==2.0.41
sqlmodel==0.0.24
starlette==0.47.1
sympy==1.14.0
tenacity==8.5.0
tokenizers==0.21.2
tomli==2.2.1
tqdm==4.67.1
typeguard==4.4.4
typer==0.16.0
typing-inspection==0.4.1
typing_extensions==4.14.1
urllib3==2.5.0
uvicorn==0.35.0
uvloop==0.21.0
watchfiles==1.1.0
websocket-client==1.8.0
websockets==15.0.1
xlsxwriter==3.2.5
yarl==1.20.1
zipp==3.23.0

View file

@ -0,0 +1,27 @@
from docling.document_converter import DocumentConverter, PdfFormatOption
from docling.datamodel.pipeline_options import PdfPipelineOptions
from docling.datamodel.base_models import InputFormat
class DoclingService:
def __init__(self):
self.pipeline_options = PdfPipelineOptions()
self.pipeline_options.do_ocr = False
self.converter = DocumentConverter(
format_options={
InputFormat.DOCX: PdfFormatOption(
pipeline_options=self.pipeline_options,
),
InputFormat.PPTX: PdfFormatOption(
pipeline_options=self.pipeline_options,
),
InputFormat.PDF: PdfFormatOption(
pipeline_options=self.pipeline_options,
),
}
)
def parse_to_markdown(self, file_path: str) -> str:
result = self.converter.convert(file_path)
return result.document.export_to_markdown()

View file

@ -1,9 +1,8 @@
import mimetypes
from fastapi import HTTPException
import os, pdfplumber, asyncio
import os, asyncio
from typing import List, Tuple
from docx import Document
from pptx import Presentation
import pdfplumber
from constants.documents import (
PDF_MIME_TYPES,
@ -11,6 +10,7 @@ from constants.documents import (
TEXT_MIME_TYPES,
WORD_TYPES,
)
from services.docling_service import DoclingService
class DocumentsLoader:
@ -18,6 +18,8 @@ class DocumentsLoader:
def __init__(self, file_paths: List[str]):
self._file_paths = file_paths
self.docling_service = DoclingService()
self._documents: List[str] = []
self._images: List[List[str]] = []
@ -76,9 +78,7 @@ class DocumentsLoader:
document: str = ""
if load_text:
with pdfplumber.open(file_path) as pdf:
for page in pdf.pages:
document += await asyncio.to_thread(page.extract_text)
document = self.docling_service.parse_to_markdown(file_path)
if load_images:
image_paths = await self.get_page_images_from_pdf_async(file_path, temp_dir)
@ -90,23 +90,10 @@ class DocumentsLoader:
return await asyncio.to_thread(file.read)
def load_msword(self, file_path: str) -> str:
document = Document(file_path)
text = "\n".join([paragraph.text for paragraph in document.paragraphs])
return text
return self.docling_service.parse_to_markdown(file_path)
def load_powerpoint(self, file_path: str) -> str:
presentation = Presentation(file_path)
extracted_text = ""
for index, slide in enumerate(presentation.slides):
extracted_text += f"# Slide {index + 1}\n"
for shape in slide.shapes:
if shape.has_text_frame:
for paragraph in shape.text_frame.paragraphs:
extracted_text += f"{paragraph.text}\n"
extracted_text += "\n"
extracted_text += "\n\n"
return extracted_text
return self.docling_service.parse_to_markdown(file_path)
def get_page_images_from_pdf(self, file_path: str, temp_dir: str):
with pdfplumber.open(file_path) as pdf:

View file

@ -15,11 +15,14 @@ from utils.get_env import (
get_anthropic_api_key_env,
get_custom_llm_api_key_env,
get_custom_llm_url_env,
get_disable_thinking_env,
get_google_api_key_env,
get_ollama_url_env,
get_openai_api_key_env,
get_tool_calls_env,
)
from utils.llm_provider import get_llm_provider
from utils.parsers import parse_bool_or_none
from utils.schema_utils import ensure_strict_json_schema
@ -28,13 +31,17 @@ class LLMClient:
self.llm_provider = get_llm_provider()
self._client = self._get_client()
# Supports json_schema
def supports_json_schema(self, model: str) -> bool:
if model.startswith("deepseek"):
# ? Use tool calls
def use_tool_calls(self) -> bool:
if self.llm_provider != LLMProvider.CUSTOM:
return False
if model.startswith("claude"):
return parse_bool_or_none(get_tool_calls_env()) or False
# ? Disable thinking
def disable_thinking(self) -> bool:
if self.llm_provider != LLMProvider.CUSTOM:
return False
return True
return parse_bool_or_none(get_disable_thinking_env()) or False
# ? Clients
def _get_client(self):
@ -121,6 +128,9 @@ class LLMClient:
model=model,
messages=[message.model_dump() for message in messages],
max_completion_tokens=max_tokens,
extra_body={
"enable_thinking": not self.disable_thinking(),
},
)
return response.choices[0].message.content
@ -212,7 +222,7 @@ class LLMClient:
max_tokens: Optional[int] = None,
):
client: AsyncOpenAI = self._client
supports_json_schema = self.supports_json_schema(model)
use_tool_calls = self.use_tool_calls()
response_schema = response_format
if strict:
response_schema = ensure_strict_json_schema(
@ -220,7 +230,7 @@ class LLMClient:
path=(),
root=response_schema,
)
if supports_json_schema:
if not use_tool_calls:
response = await client.chat.completions.create(
model=model,
messages=[message.model_dump() for message in messages],
@ -235,6 +245,9 @@ class LLMClient:
),
},
max_completion_tokens=max_tokens,
extra_body={
"enable_thinking": not self.disable_thinking(),
},
)
content = response.choices[0].message.content
else:
@ -254,6 +267,9 @@ class LLMClient:
],
tool_choice="required",
max_completion_tokens=max_tokens,
extra_body={
"enable_thinking": not self.disable_thinking(),
},
)
tool_calls = response.choices[0].message.tool_calls
if tool_calls:
@ -396,6 +412,9 @@ class LLMClient:
model=model,
messages=[message.model_dump() for message in messages],
max_completion_tokens=max_tokens,
extra_body={
"enable_thinking": not self.disable_thinking(),
},
) as stream:
async for event in stream:
if event.type == "content.delta":
@ -482,7 +501,7 @@ class LLMClient:
max_tokens: Optional[int] = None,
):
client: AsyncOpenAI = self._client
supports_json_schema = self.supports_json_schema(model)
use_tool_calls = self.use_tool_calls()
response_schema = response_format
if strict:
response_schema = ensure_strict_json_schema(
@ -490,7 +509,7 @@ class LLMClient:
path=(),
root=response_schema,
)
if supports_json_schema:
if not use_tool_calls:
async with client.chat.completions.stream(
model=model,
messages=[message.model_dump() for message in messages],
@ -505,6 +524,9 @@ class LLMClient:
},
}
),
extra_body={
"enable_thinking": not self.disable_thinking(),
},
) as stream:
async for event in stream:
if event.type == "content.delta":
@ -526,6 +548,9 @@ class LLMClient:
}
],
tool_choice="required",
extra_body={
"enable_thinking": not self.disable_thinking(),
},
) as stream:
async for event in stream:
if event.type == "tool_calls.function.arguments.delta":

View file

@ -0,0 +1,199 @@
import asyncio
from typing import List
import nltk
from models.document_chunk import DocumentChunk
try:
nltk.data.find("tokenizers/punkt")
except LookupError:
nltk.download("punkt", download_dir="./nltk")
class ScoreBasedChunker:
def extract_sentences(self, text: str, min_sentences: int) -> List[str]:
sentences = self.extract_sentences_markdown(text)
if len(sentences) < min_sentences:
sentences = self.extract_sentences_nltk(text)
if len(sentences) < min_sentences:
sentences = self.extract_sentences_by_stop_words(text)
if len(sentences) < min_sentences:
sentences = self.extract_sentences_by_new_line(text)
if len(sentences) < min_sentences:
raise ValueError(
f"Only {len(sentences)} sentences found, requested {min_sentences}"
)
return sentences
def extract_sentences_markdown(self, text: str) -> List[str]:
lines = text.split("\n")
sentences = []
for line in lines:
line = line.strip()
if line:
if line.startswith("#"):
sentences.append(line)
else:
if line.endswith((".", "!", "?")):
sentences.append(line)
else:
sentences.append(line)
return sentences
def extract_sentences_nltk(self, text: str) -> List[str]:
sentences = nltk.sent_tokenize(text)
return sentences
def extract_sentences_by_stop_words(self, text: str) -> List[str]:
sentences = []
current_sentence = ""
for char in text:
current_sentence += char
if char in ".!?":
sentences.append(current_sentence.strip())
current_sentence = ""
if current_sentence.strip():
sentences.append(current_sentence.strip())
return [s for s in sentences if s]
def extract_sentences_by_new_line(self, text: str) -> List[str]:
sentences = text.split("\n")
result = []
for i, sentence in enumerate(sentences):
if i < len(sentences) - 1:
result.append(sentence + "\n")
else:
result.append(sentence)
return result
def score_sentences_for_heading(self, sentences: List[str]) -> List[float]:
sentences_scores = []
last_heading_index = -1
first_heading_found = False
for i, sentence in enumerate(sentences):
score = 0.0
if sentence.strip().startswith("#"):
heading_level = len(sentence) - len(sentence.lstrip("#"))
if heading_level <= 3:
score += 10.0 - (heading_level - 1) * 2.0
else:
score += 4.0 - (heading_level - 4) * 0.5
if not first_heading_found:
score += 5.0
first_heading_found = True
if last_heading_index != -1:
distance = i - last_heading_index
distance_bonus = min(5.0, distance * 0.5)
score += distance_bonus
last_heading_index = i
sentences_scores.append(score)
return sentences_scores
def get_chunks(
self, sentences: List[str], sentences_scores: List[float], top_k: int = 10
) -> List[DocumentChunk]:
if not sentences_scores:
sentences_scores = self.score_sentences_for_heading(sentences)
chunks = []
heading_scores = []
for i, score in enumerate(sentences_scores):
if score > 0:
heading_scores.append((i, score))
if len(heading_scores) == 0:
return chunks
heading_scores.sort(key=lambda x: (-x[1], x[0]))
if len(heading_scores) <= top_k:
selected_headings = [idx for idx, _ in heading_scores]
selected_headings.sort()
else:
score_groups = {}
for idx, score in heading_scores:
rounded_score = round(score)
if rounded_score not in score_groups:
score_groups[rounded_score] = []
score_groups[rounded_score].append(idx)
sorted_groups = sorted(
score_groups.items(), key=lambda x: x[0], reverse=True
)
selected_headings = []
for score, headings in sorted_groups:
headings.sort()
remaining_needed = top_k - len(selected_headings)
if remaining_needed <= 0:
break
if len(headings) <= remaining_needed:
selected_headings.extend(headings)
else:
if remaining_needed == 1:
mid_idx = len(headings) // 2
selected_headings.append(headings[mid_idx])
elif remaining_needed == 2:
selected_headings.append(headings[0])
selected_headings.append(headings[-1])
else:
step = (len(headings) - 1) / (remaining_needed - 1)
for i in range(remaining_needed):
index = int(round(i * step))
if index < len(headings):
selected_headings.append(headings[index])
selected_headings.sort()
for i, heading_idx in enumerate(selected_headings):
heading = sentences[heading_idx]
if i + 1 < len(selected_headings):
next_heading_idx = selected_headings[i + 1]
content_end = next_heading_idx
else:
content_end = len(sentences)
content_sentences = sentences[heading_idx + 1 : content_end]
content = " ".join(content_sentences).strip()
chunk = DocumentChunk(
heading=heading,
content=content,
heading_index=heading_idx,
score=sentences_scores[heading_idx],
)
chunks.append(chunk)
return chunks
async def get_n_chunks(self, text: str, n: int) -> List[DocumentChunk]:
sentences = await asyncio.to_thread(self.extract_sentences, text, n)
sentences_scores = await asyncio.to_thread(
self.score_sentences_for_heading, sentences
)
chunks = await asyncio.to_thread(
self.get_chunks, sentences, sentences_scores, n
)
if len(chunks) < n:
raise ValueError(f"Only {len(chunks)} chunks found, requested {n}")
return chunks

View file

@ -1,29 +1,15 @@
from typing import List, Optional
from typing import List
from pydantic import Field
from models.presentation_outline_model import (
PresentationOutlineModel,
SlideOutlineModel,
)
from models.presentation_outline_model import PresentationOutlineModel
from models.presentation_structure_model import PresentationStructureModel
class SlideOutlineModelWithValidation(SlideOutlineModel):
title: str = Field(
description="Title of the slide in about 3 to 5 words",
min_length=10,
max_length=50,
)
def get_presentation_outline_model_with_n_slides(n_slides: int):
class PresentationOutlineModelWithNSlides(PresentationOutlineModel):
title: str = Field(
description="Title of the presentation in about 3 to 8 words",
min_length=10,
max_length=50,
)
slides: List[SlideOutlineModelWithValidation] = Field(
description="List of slides", min_items=n_slides, max_items=n_slides
slides: List[str] = Field(
description="Markdown content for each slide",
min_items=n_slides,
max_items=n_slides,
)
return PresentationOutlineModelWithNSlides

View file

@ -97,5 +97,13 @@ def get_redis_password_env():
return os.getenv("REDIS_PASSWORD")
def get_tool_calls_env():
return os.getenv("TOOL_CALLS")
def get_disable_thinking_env():
return os.getenv("DISABLE_THINKING")
def get_extended_reasoning_env():
return os.getenv("EXTENDED_REASONING")

View file

@ -1,44 +0,0 @@
import asyncio
from typing import List
from models.llm_message import LLMMessage
from services.llm_client import LLMClient
from utils.llm_provider import get_model
sysmte_prompt = """
Generate a blog-style summary of the provided document in **more than 2000 words**.
Maintain as much information as possible.
### Output Format
- Provide the summary in a **blog format** with an **engaging introduction** and a **clear structure**.
- Ensure the **logical flow** of the document is preserved.
### Notes
- **Retain the main ideas and essential details** from the document.
- **Show line-breaks** clearly.
- If **slides structure is mentioned** in document, structure the summary in the same way.
"""
async def generate_document_summary(documents: List[str]):
client = LLMClient()
model = get_model()
coroutines = []
for document in documents:
truncated_text = document[:200000]
coroutine = client.generate(
model=model,
messages=[
LLMMessage(role="system", content=sysmte_prompt),
LLMMessage(role="user", content=truncated_text),
],
)
coroutines.append(coroutine)
completions: List[str] = await asyncio.gather(*coroutines)
combined = "\n\n\n\n".join(completions)
return combined

View file

@ -7,42 +7,13 @@ from utils.get_dynamic_models import get_presentation_outline_model_with_n_slide
from utils.llm_provider import get_model
system_prompt = """
You are an expert presentation creator. Generate structured presentations based on user requirements and format them according to the specified JSON schema with markdown content.
You are an expert presentation creator. Generate structured presentations based on user requirements and format them according to the specified JSON schema with markdown content.
## Core Requirements
### Input Processing
1. **Extract key information** from the user's prompt:
- Main topic/subject matter
- Required number of slides
- Target language for output
- Specific content requirements or focus areas
- Target audience (if specified)
- Presentation style or tone preferences
## Content Generation Guidelines
### Presentation Title
- Create a **concise, descriptive title** that captures the essence of the topic
- Use **plain text format** (no markdown formatting)
- Make it **engaging and professional**
- Ensure it reflects the main theme and target audience
### Slide Titles
- Generate **clear, specific titles** for each slide
- Use **plain text format** (no markdown, no "Slide 1", "Slide 2" prefixes)
- Make each title **descriptive and informative**
- Ensure titles create a **logical flow** through the presentation
- Keep titles **concise but meaningful**
## Special Considerations
### Slide Count Compliance
- Generate **exactly** the number of slides requested
- Distribute content **evenly** across slides
- Create **balanced information flow**
- Provide content for each slide in markdown format.
- Make sure that flow of the presentation is logical and consistent.
- Place greater emphasis on numerical data.
- If Additional Information is provided, divide it into slides.
- Make sure that content follows language guidelines.
"""

View file

@ -1,29 +1,28 @@
from models.llm_message import LLMMessage
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
system_prompt = """
Generate structured slide based on provided title and outline, follow mentioned steps and notes and provide structured output.
Generate structured slide based on provided outline, follow mentioned steps and notes and provide structured output.
# Steps
1. Analyze the outline and title.
2. Generate structured slide based on the outline and title.
1. Analyze the outline.
2. Generate structured slide based on the outline.
# Notes
- Slide body should not use words like "This slide", "This presentation".
- Rephrase the slide body to make it flow naturally.
- Provide prompt to generate image on "__image_prompt__" property.
- Provide query to search icon on "__icon_query__" property.
- Do not use markdown formatting in slide body.
- Only use markdown to highlight important points.
- Make sure to follow language guidelines.
**Strictly follow the max and min character limit for every property in the slide.**
"""
def get_user_prompt(title: str, outline: str, language: str):
def get_user_prompt(outline: str, language: str):
return f"""
## Icon Query And Image Prompt Language
English
@ -31,15 +30,12 @@ def get_user_prompt(title: str, outline: str, language: str):
## Slide Content Language
{language}
## Slide Title
{title}
## Slide Outline
{outline}
"""
def get_messages(title: str, outline: str, language: str):
def get_messages(outline: str, language: str):
return [
LLMMessage(
@ -48,13 +44,13 @@ def get_messages(title: str, outline: str, language: str):
),
LLMMessage(
role="user",
content=get_user_prompt(title, outline, language),
content=get_user_prompt(outline, language),
),
]
async def get_slide_content_from_type_and_outline(
slide_layout: SlideLayoutModel, outline: SlideOutlineModel, language: str
slide_layout: SlideLayoutModel, outline: str, language: str
):
client = LLMClient()
model = get_model()
@ -66,8 +62,7 @@ async def get_slide_content_from_type_and_outline(
response = await client.generate_structured(
model=model,
messages=get_messages(
outline.title,
outline.body,
outline,
language,
),
response_format=response_schema,

View file

@ -0,0 +1,4 @@
def parse_bool_or_none(value: str | None) -> bool | None:
if value is None:
return None
return value.lower() == "true"

View file

@ -69,5 +69,13 @@ def set_pixabay_api_key_env(value):
os.environ["PIXABAY_API_KEY"] = value
def set_tool_calls_env(value):
os.environ["TOOL_CALLS"] = value
def set_disable_thinking_env(value):
os.environ["DISABLE_THINKING"] = value
def set_extended_reasoning_env(value):
os.environ["EXTENDED_REASONING"] = value

View file

@ -8,6 +8,7 @@ from utils.get_env import (
get_custom_llm_api_key_env,
get_custom_llm_url_env,
get_custom_model_env,
get_disable_thinking_env,
get_google_api_key_env,
get_google_model_env,
get_llm_provider_env,
@ -16,17 +17,20 @@ from utils.get_env import (
get_openai_api_key_env,
get_openai_model_env,
get_pexels_api_key_env,
get_tool_calls_env,
get_user_config_path_env,
get_image_provider_env,
get_pixabay_api_key_env,
get_extended_reasoning_env,
)
from utils.parsers import parse_bool_or_none
from utils.set_env import (
set_anthropic_api_key_env,
set_anthropic_model_env,
set_custom_llm_api_key_env,
set_custom_llm_url_env,
set_custom_model_env,
set_disable_thinking_env,
set_extended_reasoning_env,
set_google_api_key_env,
set_google_model_env,
@ -38,6 +42,7 @@ from utils.set_env import (
set_pexels_api_key_env,
set_image_provider_env,
set_pixabay_api_key_env,
set_tool_calls_env,
)
@ -53,12 +58,6 @@ def get_user_config():
print("Error while loading user config")
pass
new_extended_reasoning = (
existing_config.EXTENDED_REASONING or get_extended_reasoning_env()
)
if new_extended_reasoning is not None:
new_extended_reasoning = bool(new_extended_reasoning)
return UserConfig(
LLM=existing_config.LLM or get_llm_provider_env(),
OPENAI_API_KEY=existing_config.OPENAI_API_KEY or get_openai_api_key_env(),
@ -77,7 +76,12 @@ def get_user_config():
IMAGE_PROVIDER=existing_config.IMAGE_PROVIDER or get_image_provider_env(),
PIXABAY_API_KEY=existing_config.PIXABAY_API_KEY or get_pixabay_api_key_env(),
PEXELS_API_KEY=existing_config.PEXELS_API_KEY or get_pexels_api_key_env(),
EXTENDED_REASONING=new_extended_reasoning,
TOOL_CALLS=existing_config.TOOL_CALLS
or parse_bool_or_none(get_tool_calls_env()),
DISABLE_THINKING=existing_config.DISABLE_THINKING
or parse_bool_or_none(get_disable_thinking_env()),
EXTENDED_REASONING=existing_config.EXTENDED_REASONING
or parse_bool_or_none(get_extended_reasoning_env()),
)
@ -113,6 +117,10 @@ def update_env_with_user_config():
set_pixabay_api_key_env(user_config.PIXABAY_API_KEY)
if user_config.PEXELS_API_KEY:
set_pexels_api_key_env(user_config.PEXELS_API_KEY)
if user_config.TOOL_CALLS:
set_tool_calls_env(str(user_config.TOOL_CALLS))
if user_config.DISABLE_THINKING:
set_disable_thinking_env(str(user_config.DISABLE_THINKING))
if user_config.EXTENDED_REASONING:
if user_config.EXTENDED_REASONING:
set_extended_reasoning_env(str(user_config.EXTENDED_REASONING))

3149
servers/fastapi/uv.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,9 @@
import { useEditor, EditorContent } from "@tiptap/react"
import StarterKit from "@tiptap/starter-kit"
import { Markdown } from "tiptap-markdown"
import { useEffect } from "react"
export default function MarkdownEditor({ content, onChange }: { content: string; onChange: (content: string) => void }) {
const editor = useEditor({
extensions: [StarterKit, Markdown],
content: content,

View file

@ -207,7 +207,7 @@ const DocumentsPreviewPage: React.FC = () => {
return (
<div className={`border-r border-gray-200 fixed xl:relative w-full z-50 xl:z-auto
transition-all duration-300 ease-in-out max-w-[200px] md:max-w-[300px] h-[85vh] rounded-md p-5`}>
transition-all duration-300 bg-white ease-in-out max-w-[200px] md:max-w-[300px] h-[85vh] rounded-md p-5`}>
<X
onClick={() => setIsOpen(false)}
className="text-black mb-4 ml-auto mr-0 cursor-pointer hover:text-gray-600"

View file

@ -15,11 +15,10 @@ import {
} from "@dnd-kit/sortable";
import { OutlineItem } from "./OutlineItem";
import { Button } from "@/components/ui/button";
import { SlideOutline } from "@/store/slices/presentationGeneration";
import { FileText } from "lucide-react";
interface OutlineContentProps {
outlines: SlideOutline[] | null;
outlines: string[] | null;
isLoading: boolean;
isStreaming: boolean;
onDragEnd: (event: any) => void;
@ -33,6 +32,7 @@ const OutlineContent: React.FC<OutlineContentProps> = ({
onDragEnd,
onAddSlide
}) => {
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
@ -84,12 +84,12 @@ const OutlineContent: React.FC<OutlineContentProps> = ({
onDragEnd={onDragEnd}
>
<SortableContext
items={outlines?.map((item, index) => ({ id: item.title || `slide-${index}` })) || []}
items={outlines?.map((item, index) => ({ id: `slide-${index}` })) || []}
strategy={verticalListSortingStrategy}
>
{outlines?.map((item, index) => (
<OutlineItem
key={item.title || `slide-${index}`}
key={`slide-${index}`}
index={index + 1}
slideOutline={item}
isStreaming={isStreaming}

View file

@ -3,14 +3,14 @@ import { CSS } from "@dnd-kit/utilities"
import { Trash2 } from "lucide-react"
import { RootState } from "@/store/store"
import { useDispatch, useSelector } from "react-redux"
import { deleteSlideOutline, setOutlines, SlideOutline } from "@/store/slices/presentationGeneration"
import { deleteSlideOutline, setOutlines } from "@/store/slices/presentationGeneration"
import ToolTip from "@/components/ToolTip"
import MarkdownEditor from "../../components/MarkdownEditor"
import { useEffect } from "react"
interface OutlineItemProps {
slideOutline: SlideOutline,
slideOutline: string,
index: number
isStreaming: boolean
}
@ -26,7 +26,7 @@ export function OutlineItem({
const dispatch = useDispatch()
useEffect(() => {
if (isStreaming && slideOutline.body) {
if (isStreaming && slideOutline) {
const outlineItem = document.getElementById(`outline-item-${index}`);
if (outlineItem) {
outlineItem.scrollIntoView({
@ -38,7 +38,7 @@ export function OutlineItem({
}
}, [outlines.length]);
const handleSlideChange = (newOutline: SlideOutline) => {
const handleSlideChange = (newOutline: string) => {
if (isStreaming) return;
const newData = outlines?.map((each, idx) => {
if (idx === index - 1) {
@ -60,7 +60,7 @@ export function OutlineItem({
transform,
transition,
isDragging,
} = useSortable({ id: slideOutline.title || index })
} = useSortable({ id: index })
const style = {
transform: CSS.Transform.toString(transform),
@ -96,24 +96,16 @@ export function OutlineItem({
{/* Main Title Input - Add onFocus handler */}
<div id={`outline-item-${index}`} className="flex flex-col basis-full gap-2">
<input
type="text"
defaultValue={slideOutline.title || ''}
onBlur={(e) => handleSlideChange({ ...slideOutline, title: e.target.value })}
className="text-lg mt-4 sm:text-xl flex-1 font-semibold bg-transparent outline-none"
placeholder="Title goes here"
/>
{/* Editable Markdown Content */}
{isStreaming ? <textarea
defaultValue={slideOutline.body || ''}
onBlur={(e) => handleSlideChange({ ...slideOutline, body: e.target.value })}
defaultValue={slideOutline || ''}
onBlur={(e) => handleSlideChange(e.target.value)}
className="text-sm flex-1 font-normal bg-transparent outline-none overflow-y-hidden"
placeholder="Content goes here"
/> : <MarkdownEditor
key={index}
content={slideOutline.body || ''}
onChange={(content) => handleSlideChange({ ...slideOutline, body: content })}
content={slideOutline || ''}
onChange={(content) => handleSlideChange(content)}
/>}
</div>

View file

@ -32,7 +32,6 @@ const OutlinePage: React.FC = () => {
selectedLayoutGroup,
setActiveTab
);
if (!presentation_id) {
return <EmptyStateView />;
}

View file

@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { toast } from "sonner";
import { setOutlines, SlideOutline } from "@/store/slices/presentationGeneration";
import { setOutlines } from "@/store/slices/presentationGeneration";
import { jsonrepair } from "jsonrepair";
import { StreamState } from "../types/index";
import { RootState } from "@/store/store";
@ -49,7 +49,7 @@ export const useOutlineStreaming = (presentationId: string | null) => {
case "complete":
try {
const outlinesData: SlideOutline[] = data.presentation.outlines;
const outlinesData: string[] = data.presentation.outlines.slides;
dispatch(setOutlines(outlinesData));
setStreamState({ isStreaming: false, isLoading: false });
eventSource.close();

View file

@ -14,13 +14,14 @@ import {
usePresentationStreaming,
usePresentationData,
usePresentationNavigation,
useAutoSave
useAutoSave,
} from "../hooks";
import { PresentationPageProps } from "../types";
import LoadingState from "./LoadingState";
const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id }) => {
const PresentationPage: React.FC<PresentationPageProps> = ({
presentation_id,
}) => {
// State management
const [loading, setLoading] = useState(true);
const [selectedSlide, setSelectedSlide] = useState(0);
@ -28,7 +29,6 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
const [error, setError] = useState(false);
const [isMobilePanelOpen, setIsMobilePanelOpen] = useState(false);
const { presentationData, isStreaming } = useSelector(
(state: RootState) => state.presentationGeneration
);
@ -37,7 +37,6 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
const { isSaving } = useAutoSave({
debounceMs: 2000,
enabled: !!presentationData && !isStreaming,
});
// Custom hooks
@ -54,7 +53,12 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
toggleFullscreen,
handlePresentExit,
handleSlideChange,
} = usePresentationNavigation(presentation_id, selectedSlide, setSelectedSlide, setIsFullscreen);
} = usePresentationNavigation(
presentation_id,
selectedSlide,
setSelectedSlide,
setIsFullscreen
);
// Initialize streaming
usePresentationStreaming(
@ -65,13 +69,10 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
fetchUserSlides
);
const onSlideChange = (newSlide: number) => {
handleSlideChange(newSlide, presentationData);
};
// Presentation Mode View
if (isPresentMode) {
return (
@ -94,15 +95,11 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
role="alert"
>
<AlertCircle className="w-16 h-16 mb-4 text-red-500" />
<h2 className="text-xl font-semibold mb-2">
Something went wrong
</h2>
<h2 className="text-xl font-semibold mb-2">Something went wrong</h2>
<p className="text-center mb-4">
We couldn't load your presentation. Please try again.
</p>
<Button onClick={() => window.location.reload()}>
Refresh Page
</Button>
<Button onClick={() => window.location.reload()}>Refresh Page</Button>
</div>
</div>
);
@ -110,12 +107,8 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
return (
<div className="h-screen flex overflow-hidden flex-col">
<div className="fixed right-6 top-[5.2rem] z-50">
{isSaving && (
<Loader2 className="w-6 h-6 animate-spin text-blue-500" />
)}
{isSaving && <Loader2 className="w-6 h-6 animate-spin text-blue-500" />}
</div>
<Header presentation_id={presentation_id} currentSlide={selectedSlide} />
@ -123,7 +116,7 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
<div
style={{
background: '#c8c7c9',
background: "#c8c7c9",
}}
className="flex flex-1 relative pt-6"
>
@ -136,11 +129,14 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
/>
<div className="flex-1 h-[calc(100vh-100px)] overflow-y-auto">
<div id="presentation-slides-wrapper" className="mx-auto flex flex-col items-center overflow-hidden justify-center p-2 sm:p-6 pt-0">
<div
id="presentation-slides-wrapper"
className="mx-auto flex flex-col items-center overflow-hidden justify-center p-2 sm:p-6 pt-0"
>
{!presentationData ||
loading ||
!presentationData?.slides ||
presentationData?.slides.length === 0 ? (
loading ||
!presentationData?.slides ||
presentationData?.slides.length === 0 ? (
<div className="relative w-full h-[calc(100vh-120px)] mx-auto">
<div className="">
{Array.from({ length: 2 }).map((_, index) => (
@ -163,7 +159,6 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
slide={slide}
index={index}
presentationId={presentation_id}
/>
))}
</>

View file

@ -1,4 +1,4 @@
import { useCallback, useEffect } from 'react';
import { useCallback, useEffect } from "react";
import { useDispatch } from "react-redux";
import { toast } from "sonner";
import { DashboardApi } from "@/app/dashboard/api/dashboard";
@ -26,11 +26,7 @@ export const usePresentationData = (
}
}, [presentationId, dispatch, setLoading, setError]);
useEffect(() => {
fetchUserSlides();
}, [fetchUserSlides]);
return {
fetchUserSlides,
};
};
};

View file

@ -1,6 +1,10 @@
import { useEffect, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import { clearPresentationData, setPresentationData, setStreaming } from "@/store/slices/presentationGeneration";
import {
clearPresentationData,
setPresentationData,
setStreaming,
} from "@/store/slices/presentationGeneration";
import { jsonrepair } from "jsonrepair";
import { RootState } from "@/store/store";
@ -11,8 +15,6 @@ export const usePresentationStreaming = (
setError: (error: boolean) => void,
fetchUserSlides: () => void
) => {
const { presentationData } = useSelector((state: RootState) => state.presentationGeneration);
const dispatch = useDispatch();
const previousSlidesLength = useRef(0);
@ -64,7 +66,7 @@ export const usePresentationStreaming = (
dispatch(setStreaming(false));
setLoading(false);
eventSource.close();
// Remove stream parameter from URL
const newUrl = new URL(window.location.href);
newUrl.searchParams.delete("stream");
@ -81,7 +83,7 @@ export const usePresentationStreaming = (
setLoading(false);
dispatch(setStreaming(false));
eventSource.close();
// Remove stream parameter from URL
const newUrl = new URL(window.location.href);
newUrl.searchParams.delete("stream");
@ -102,9 +104,7 @@ export const usePresentationStreaming = (
if (stream) {
initializeStream();
} else {
if(!presentationData || presentationData.slides.length === 0){
fetchUserSlides();
}
fetchUserSlides();
}
return () => {
@ -113,4 +113,4 @@ export const usePresentationStreaming = (
}
};
}, [presentationId, stream, dispatch, setLoading, setError, fetchUserSlides]);
};
};

View file

@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import fs from "fs";
import { LLMConfig } from "@/types/llm_config";
const userConfigPath = process.env.USER_CONFIG_PATH!;
const canChangeKeys = process.env.CAN_CHANGE_KEYS !== "false";
@ -50,14 +51,16 @@ export async function POST(request: Request) {
userConfig.PIXABAY_API_KEY || existingConfig.PIXABAY_API_KEY,
IMAGE_PROVIDER: userConfig.IMAGE_PROVIDER || existingConfig.IMAGE_PROVIDER,
PEXELS_API_KEY: userConfig.PEXELS_API_KEY || existingConfig.PEXELS_API_KEY,
USE_CUSTOM_URL:
userConfig.USE_CUSTOM_URL === undefined
? existingConfig.USE_CUSTOM_URL
: userConfig.USE_CUSTOM_URL,
TOOL_CALLS: userConfig.TOOL_CALLS === undefined ? existingConfig.TOOL_CALLS : userConfig.TOOL_CALLS,
DISABLE_THINKING: userConfig.DISABLE_THINKING === undefined ? existingConfig.DISABLE_THINKING : userConfig.DISABLE_THINKING,
EXTENDED_REASONING:
userConfig.EXTENDED_REASONING === undefined
? existingConfig.EXTENDED_REASONING
: userConfig.EXTENDED_REASONING,
USE_CUSTOM_URL:
userConfig.USE_CUSTOM_URL === undefined
? existingConfig.USE_CUSTOM_URL
: userConfig.USE_CUSTOM_URL,
};
fs.writeFileSync(userConfigPath, JSON.stringify(mergedConfig));
return NextResponse.json(mergedConfig);

View file

@ -41,6 +41,7 @@ body {
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
@ -70,13 +71,14 @@ body {
}
@layer base {
body {
@apply bg-background text-foreground;
}
}
strong{
@apply font-black ;
strong {
@apply font-black;
}
@ -97,13 +99,17 @@ input[type="number"] {
-moz-appearance: textfield;
}
thead, tbody tr {
display: table;
width: 100%;
table-layout: fixed;/* even columns width , fix width of table too*/
thead,
tbody tr {
display: table;
width: 100%;
table-layout: fixed;
/* even columns width , fix width of table too*/
}
thead {
width: calc( 100% - 1em )/* scrollbar is average 1em/16px width, remove it from thead width */
width: calc(100% - 1em)
/* scrollbar is average 1em/16px width, remove it from thead width */
}
/* Add this to your global CSS or a specific CSS module */
@ -111,37 +117,54 @@ thead {
from {
width: 0;
}
to {
width: 100%;
}
}
.typing-effect {
overflow: hidden; /* Ensures the text is hidden until revealed */
white-space: nowrap; /* Prevents text from wrapping */
display: inline-block; /* Ensures the width is respected */
animation: typing 2s steps(10, end); /* Adjust duration and steps for effect */
animation-fill-mode: forwards; /* Retain the final state of the animation */
animation-delay: 1s; /* Optional: delay before starting the animation */
overflow: hidden;
/* Ensures the text is hidden until revealed */
white-space: nowrap;
/* Prevents text from wrapping */
display: inline-block;
/* Ensures the width is respected */
animation: typing 2s steps(10, end);
/* Adjust duration and steps for effect */
animation-fill-mode: forwards;
/* Retain the final state of the animation */
animation-delay: 1s;
/* Optional: delay before starting the animation */
}
.typing-effect-complete {
border-right: none; /* Remove the cursor after animation */
border-right: none;
/* Remove the cursor after animation */
}
.blinking-cursor {
animation: blink 1s step-end infinite;
animation: blink 1s step-end infinite;
}
@keyframes blink {
from, to { opacity: 1; }
50% { opacity: 0; }
from,
to {
opacity: 1;
}
50% {
opacity: 0;
}
}
.hide-scrollbar::-webkit-scrollbar {
@apply hidden;
}
.hide-scrollbar {
-ms-overflow-style: none;
-ms-overflow-style: none;
scrollbar-width: none;
}
@ -180,36 +203,39 @@ thead {
/* word animation */
@keyframes slideUp {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-50%);
}
0% {
transform: translateY(0);
}
100% {
transform: translateY(-50%);
}
}
@keyframes slideDown {
0% {
transform: translateY(-50%);
}
100% {
transform: translateY(0);
}
0% {
transform: translateY(-50%);
}
100% {
transform: translateY(0);
}
}
.animate-slideUp {
animation: slideUp 20s linear infinite;
animation: slideUp 20s linear infinite;
}
.animate-slideDown {
animation: slideDown 20s linear infinite;
animation: slideDown 20s linear infinite;
}
/* Add hover pause */
.animate-slideUp:hover,
.animate-slideDown:hover {
animation-play-state: paused;
animation-play-state: paused;
}
/* box animation */
.research-mode-bg {
@ -237,18 +263,20 @@ thead {
height: 0;
opacity: 0.8;
}
100% {
width: 200%;
height: 200%;
opacity: 1;
}
}
/* Markdown Styles */
.markdown-content {
@apply prose prose-slate max-w-none;
}
.markdown-content h1 {
/* .markdown-content h1 {
@apply text-xl font-bold mb-4 text-gray-900;
}
@ -323,7 +351,7 @@ thead {
.markdown-content td {
@apply border border-gray-300 px-4 py-2;
}
} */
/* Override Tailwind Typography prose heading sizes for markdown editor */
.prose h1 {
@ -383,7 +411,7 @@ thead {
.mdxeditor-button[data-active=true] {
@apply bg-gray-100;
}
}
/* tippy-box */
.tippy-box {
@ -396,8 +424,4 @@ thead {
float: left;
height: 0;
pointer-events: none;
}
}

View file

@ -9,10 +9,10 @@ import { handleSaveLLMConfig } from "@/utils/storeHelpers";
import {
checkIfSelectedOllamaModelIsPulled,
pullOllamaModel,
LLMConfig
} from "@/utils/providerUtils";
import { useRouter } from "next/navigation";
import LLMProviderSelection from "@/components/LLMSelection";
import { LLMConfig } from "@/types/llm_config";
// Button state interface
interface ButtonState {

View file

@ -6,6 +6,7 @@ import { hasValidLLMConfig } from '@/utils/storeHelpers';
import { usePathname, useRouter } from 'next/navigation';
import { useDispatch } from 'react-redux';
import { checkIfSelectedOllamaModelIsPulled } from '@/utils/providerUtils';
import { LLMConfig } from '@/types/llm_config';
export function StoreInitializer({ children }: { children: React.ReactNode }) {
const dispatch = useDispatch();

View file

@ -13,18 +13,23 @@ import {
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import { Switch } from "./ui/switch";
interface CustomConfigProps {
customLlmUrl: string;
customLlmApiKey: string;
customModel: string;
onInputChange: (value: string, field: string) => void;
toolCalls: boolean;
disableThinking: boolean;
onInputChange: (value: string | boolean, field: string) => void;
}
export default function CustomConfig({
customLlmUrl,
customLlmApiKey,
customModel,
toolCalls,
disableThinking,
onInputChange,
}: CustomConfigProps) {
const [customModels, setCustomModels] = useState<string[]>([]);
@ -225,6 +230,39 @@ export default function CustomConfig({
</div>
</div>
)}
</div>
{/* Tool Calls Toggle */}
<div>
<div className="flex items-center justify-between mb-4 bg-green-50 p-2 rounded-sm">
<label className="text-sm font-medium text-gray-700">
Use Tool Calls
</label>
<Switch
checked={toolCalls}
onCheckedChange={(checked) => onInputChange(checked, "tool_calls")}
/>
</div>
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
If enabled, Tool Calls will be used instead of JSON Schema for Structured Output.
</p>
</div>
{/* Disable Thinking Toggle */}
<div>
<div className="flex items-center justify-between mb-4 bg-green-50 p-2 rounded-sm">
<label className="text-sm font-medium text-gray-700">
Disable Thinking
</label>
<Switch
checked={disableThinking}
onCheckedChange={(checked) => onInputChange(checked, "disable_thinking")}
/>
</div>
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
If enabled, Thinking will be disabled.
</p>
</div>
</div >
);
}

View file

@ -9,9 +9,9 @@ import { handleSaveLLMConfig } from "@/utils/storeHelpers";
import LLMProviderSelection from "./LLMSelection";
import {
checkIfSelectedOllamaModelIsPulled,
LLMConfig,
pullOllamaModel,
} from "@/utils/providerUtils";
import { LLMConfig } from "@/types/llm_config";
// Button state interface
interface ButtonState {

View file

@ -19,11 +19,11 @@ import AnthropicConfig from "./AnthropicConfig";
import OllamaConfig from "./OllamaConfig";
import CustomConfig from "./CustomConfig";
import {
LLMConfig,
updateLLMConfig,
changeProvider as changeProviderUtil,
} from "@/utils/providerUtils";
import { IMAGE_PROVIDERS, LLM_PROVIDERS } from "@/utils/providerConstants";
import { LLMConfig } from "@/types/llm_config";
// Button state interface
interface ButtonState {
@ -188,6 +188,8 @@ export default function LLMProviderSelection({
customLlmUrl={llmConfig.CUSTOM_LLM_URL || ""}
customLlmApiKey={llmConfig.CUSTOM_LLM_API_KEY || ""}
customModel={llmConfig.CUSTOM_MODEL || ""}
toolCalls={llmConfig.TOOL_CALLS || false}
disableThinking={llmConfig.DISABLE_THINKING || false}
onInputChange={input_field_changed}
/>
</TabsContent>

View file

@ -8,15 +8,15 @@ export const layoutName = 'Classic Dark Pie Chart and Metrics'
export const layoutDescription = 'A modern slide with dark background, metrics on the left, and pie chart visualization on the right.'
const chartDataSchema = z.object({
name: z.string().meta({ description: "Data point name" }),
name: z.string().min(2).max(30).meta({ description: "Data point name" }),
value: z.number().meta({ description: "Data point value" }),
});
const pieChartAndMetricsSchema = z.object({
title: z.string().min(3).max(100).default('Introduction to Nepal\'s Trade').meta({
title: z.string().min(3).max(80).default('Introduction to Nepal\'s Trade').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(200).default('Nepal\'s landlocked geography heavily influences its trade, fostering reliance on India and China.').meta({
description: z.string().min(10).max(100).default('Nepal\'s landlocked geography heavily influences its trade, fostering reliance on India and China.').meta({
description: "Description text",
}),
metrics: z.array(z.object({
@ -37,13 +37,7 @@ const pieChartAndMetricsSchema = z.object({
{ name: 'Other GDP', value: 50.6 },
]).meta({
description: "Pie chart data",
}),
showLegend: z.boolean().default(true).meta({
description: "Whether to show chart legend",
}),
showTooltip: z.boolean().default(true).meta({
description: "Whether to show chart tooltip",
}),
})
})
const chartConfig = {
@ -70,7 +64,7 @@ interface PieChartAndMetricsLayoutProps {
}
const PieChartAndMetricsLayout: React.FC<PieChartAndMetricsLayoutProps> = ({ data: slideData }) => {
const { title, description, metrics, chartData, showLegend = true, showTooltip = true } = slideData;
const { title, description, metrics, chartData } = slideData;
const CustomLegend = () => (
<div className="flex justify-center space-x-8 mt-4">
@ -89,7 +83,7 @@ const PieChartAndMetricsLayout: React.FC<PieChartAndMetricsLayoutProps> = ({ dat
const renderPieChart = () => {
return (
<PieChart>
{showTooltip && <ChartTooltip content={<ChartTooltipContent />} />}
<ChartTooltip content={<ChartTooltipContent />} />
<Pie
data={chartData}
fill="#8b5cf6"
@ -149,7 +143,7 @@ const PieChartAndMetricsLayout: React.FC<PieChartAndMetricsLayoutProps> = ({ dat
<ChartContainer config={chartConfig} className="h-[500px] w-[500px]">
{renderPieChart()}
</ChartContainer>
{showLegend && <CustomLegend />}
<CustomLegend />
</div>
</div>
</div>

View file

@ -8,15 +8,15 @@ export const layoutName = 'Classic Dark Bar Graph'
export const layoutDescription = 'A modern slide with dark background, gradient title, bar chart visualization, and footer text.'
const barDataSchema = z.object({
name: z.string().meta({ description: "Product name" }),
name: z.string().min(2).max(30).meta({ description: "Product name" }),
value: z.number().meta({ description: "Export value in millions" }),
});
const barGraphSchema = z.object({
title: z.string().min(3).max(100).default('Export Overview: Key Products').meta({
title: z.string().min(3).max(80).default('Export Overview: Key Products').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(150).default('Nepal\'s total exports were $1.3 billion in 2022, a 21% decrease from 2021, but showed a 47.5% YoY increase by Nov 2024.').meta({
description: z.string().min(10).max(120).default('Nepal\'s total exports were $1.3 billion in 2022, a 21% decrease from 2021, but showed a 47.5% YoY increase by Nov 2024.').meta({
description: "Description text",
}),
chartData: z.array(barDataSchema).min(2).max(6).default([
@ -28,12 +28,6 @@ const barGraphSchema = z.object({
]).meta({
description: "Bar chart data",
}),
showLegend: z.boolean().default(true).meta({
description: "Whether to show chart legend",
}),
showTooltip: z.boolean().default(true).meta({
description: "Whether to show chart tooltip",
}),
})
const chartConfig = {
@ -62,7 +56,7 @@ interface BarGraphLayoutProps {
}
const BarGraphLayout: React.FC<BarGraphLayoutProps> = ({ data: slideData }) => {
const { title, description, chartData, showLegend = false, showTooltip = true } = slideData;
const { title, description, chartData } = slideData;
const CustomLegend = () => (
<div className="flex justify-center space-x-8 mt-8">
@ -98,9 +92,9 @@ const BarGraphLayout: React.FC<BarGraphLayoutProps> = ({ data: slideData }) => {
/>
<YAxis
tick={{ fill: '#ffffff', fontSize: 16, fontWeight: 600 }}
tickFormatter={(value) => `$${value.toFixed(0)}.00`}
tickFormatter={(value) => value.toFixed(0)}
/>
{showTooltip && <ChartTooltip content={<ChartTooltipContent />} />}
<ChartTooltip content={<ChartTooltipContent />} />
<Bar
dataKey="value"
fill="#8b5cf6"
@ -141,7 +135,7 @@ const BarGraphLayout: React.FC<BarGraphLayoutProps> = ({ data: slideData }) => {
<ChartContainer config={chartConfig} className="h-[300px] w-full">
{renderBarChart()}
</ChartContainer>
{showLegend && <CustomLegend />}
<CustomLegend />
</div>
</div>
</div>

View file

@ -19,7 +19,7 @@ const comparisonSectionSchema = z.object({
});
const comparisonSchema = z.object({
title: z.string().min(3).max(100).default('Key Commodities in Focus').meta({
title: z.string().min(3).max(80).default('Key Commodities in Focus').meta({
description: "Main title of the slide",
}),
comparisonSections: z.array(comparisonSectionSchema).min(2).max(2).default([

View file

@ -14,10 +14,10 @@ const metricItemSchema = z.object({
});
const metricsSchema = z.object({
title: z.string().min(3).max(100).default('Top Export Destinations').meta({
title: z.string().min(3).max(80).default('Top Export Destinations').meta({
description: "Main title of the slide",
}),
description: z.string().min(10).max(200).default('Nepal exports 760 products to 132 countries, with a strong focus on regional trade.').meta({
description: z.string().min(10).max(120).default('Nepal exports 760 products to 132 countries, with a strong focus on regional trade.').meta({
description: "Description text",
}),
metrics: z.array(metricItemSchema).min(2).max(6).default([

View file

@ -7,12 +7,12 @@ export const layoutName = 'Classic Dark Bullet Point with Description'
export const layoutDescription = 'A modern slide with dark background, image on the left (2/5), and bullet points with descriptions in boxes on the right (3/5).'
const bulletPointSchema = z.object({
title: z.string().min(3).max(80).meta({ description: "Bullet point title" }),
content: z.string().min(10).max(150).meta({ description: "Bullet point content (max 150 characters)" }),
title: z.string().min(3).max(60).meta({ description: "Bullet point title" }),
content: z.string().min(10).max(120).meta({ description: "Bullet point content (max 150 characters)" }),
});
const bulletPointWithDescriptionSchema = z.object({
title: z.string().min(3).max(100).default('Trade Policies and Challenges').meta({
title: z.string().min(3).max(80).default('Trade Policies and Challenges').meta({
description: "Main title of the slide",
}),
bulletPoints: z.array(bulletPointSchema).min(2).max(3).default([

View file

@ -3,10 +3,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export interface SlideOutline {
title?: string;
body?: string;
}
export interface PresentationData {
@ -26,7 +23,7 @@ interface PresentationGenerationState {
presentation_id: string | null;
isLoading: boolean;
isStreaming: boolean | null;
outlines: SlideOutline[];
outlines: string[];
error: string | null;
presentationData: PresentationData | null;
isSlidesRendered: boolean;
@ -63,7 +60,7 @@ const presentationGenerationSlice = createSlice({
state.presentation_id = action.payload;
state.error = null;
},
// Slides rendered
// Slides rendereimport { useEffect } from "react"d
setSlidesRendered: (state, action: PayloadAction<boolean>) => {
state.isSlidesRendered = action.payload;
},
@ -80,7 +77,7 @@ const presentationGenerationSlice = createSlice({
state.outlines = [];
},
// Set outlines
setOutlines: (state, action: PayloadAction<SlideOutline[]>) => {
setOutlines: (state, action: PayloadAction<string[]>) => {
state.outlines = action.payload;
},
// Set presentation data

View file

@ -1,3 +1,4 @@
import { LLMConfig } from "@/types/llm_config";
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface UserConfigState {

View file

@ -12,39 +12,3 @@ interface TextFrameProps {
position: { x: number; y: number };
// Add other properties as needed
}
interface LLMConfig {
LLM?: string;
// OpenAI
OPENAI_API_KEY?: string;
OPENAI_MODEL?: string;
// Google
GOOGLE_API_KEY?: string;
GOOGLE_MODEL?: string;
// Anthropic
ANTHROPIC_API_KEY?: string;
ANTHROPIC_MODEL?: string;
// Ollama
OLLAMA_URL?: string;
OLLAMA_MODEL?: string;
// Custom LLM
CUSTOM_LLM_URL?: string;
CUSTOM_LLM_API_KEY?: string;
CUSTOM_MODEL?: string;
// Image providers
IMAGE_PROVIDER?: string;
PIXABAY_API_KEY?: string;
PEXELS_API_KEY?: string;
// Extended reasoning
EXTENDED_REASONING?: boolean;
// Only used in UI settings
USE_CUSTOM_URL?: boolean;
}

View file

@ -0,0 +1,37 @@
export interface LLMConfig {
LLM?: string;
// OpenAI
OPENAI_API_KEY?: string;
OPENAI_MODEL?: string;
// Google
GOOGLE_API_KEY?: string;
GOOGLE_MODEL?: string;
// Anthropic
ANTHROPIC_API_KEY?: string;
ANTHROPIC_MODEL?: string;
// Ollama
OLLAMA_URL?: string;
OLLAMA_MODEL?: string;
// Custom LLM
CUSTOM_LLM_URL?: string;
CUSTOM_LLM_API_KEY?: string;
CUSTOM_MODEL?: string;
// Image providers
IMAGE_PROVIDER?: string;
PEXELS_API_KEY?: string;
PIXABAY_API_KEY?: string;
// Other Configs
TOOL_CALLS?: boolean;
DISABLE_THINKING?: boolean;
EXTENDED_REASONING?: boolean;
// Only used in UI settings
USE_CUSTOM_URL?: boolean;
}

View file

@ -1,3 +1,5 @@
import { LLMConfig } from "@/types/llm_config";
export interface OllamaModel {
label: string;
value: string;
@ -14,42 +16,6 @@ export interface DownloadingModel {
done: boolean;
}
export interface LLMConfig {
LLM?: string;
// OpenAI
OPENAI_API_KEY?: string;
OPENAI_MODEL?: string;
// Google
GOOGLE_API_KEY?: string;
GOOGLE_MODEL?: string;
// Anthropic
ANTHROPIC_API_KEY?: string;
ANTHROPIC_MODEL?: string;
// Ollama
OLLAMA_URL?: string;
OLLAMA_MODEL?: string;
// Custom LLM
CUSTOM_LLM_URL?: string;
CUSTOM_LLM_API_KEY?: string;
CUSTOM_MODEL?: string;
// Image providers
IMAGE_PROVIDER?: string;
PEXELS_API_KEY?: string;
PIXABAY_API_KEY?: string;
// Extended reasoning
EXTENDED_REASONING?: boolean;
// Only used in UI settings
USE_CUSTOM_URL?: boolean;
}
export interface OllamaModelsResult {
models: OllamaModel[];
updatedConfig?: LLMConfig;
@ -78,8 +44,10 @@ export const updateLLMConfig = (
pexels_api_key: "PEXELS_API_KEY",
pixabay_api_key: "PIXABAY_API_KEY",
image_provider: "IMAGE_PROVIDER",
extended_reasoning: "EXTENDED_REASONING",
use_custom_url: "USE_CUSTOM_URL",
tool_calls: "TOOL_CALLS",
disable_thinking: "DISABLE_THINKING",
extended_reasoning: "EXTENDED_REASONING",
};
const configKey = fieldMappings[field];

View file

@ -1,5 +1,6 @@
import { setLLMConfig } from "@/store/slices/userConfig";
import { store } from "@/store/store";
import { LLMConfig } from "@/types/llm_config";
export const handleSaveLLMConfig = async (llmConfig: LLMConfig) => {
if (!hasValidLLMConfig(llmConfig)) {

View file

@ -78,6 +78,8 @@ const setupUserConfigFromEnv = () => {
PIXABAY_API_KEY:
process.env.PIXABAY_API_KEY || existingConfig.PIXABAY_API_KEY,
IMAGE_PROVIDER: process.env.IMAGE_PROVIDER || existingConfig.IMAGE_PROVIDER,
TOOL_CALLS: process.env.TOOL_CALLS || existingConfig.TOOL_CALLS,
DISABLE_THINKING: process.env.DISABLE_THINKING || existingConfig.DISABLE_THINKING,
EXTENDED_REASONING: process.env.EXTENDED_REASONING || existingConfig.EXTENDED_REASONING,
USE_CUSTOM_URL: process.env.USE_CUSTOM_URL || existingConfig.USE_CUSTOM_URL,
};