Merge pull request #187 from presenton/feat/improved-document-parsing
feat/improved document parsing
This commit is contained in:
commit
6c193833fb
58 changed files with 3872 additions and 1071 deletions
|
|
@ -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
3
.gitignore
vendored
|
|
@ -12,4 +12,5 @@ tmp
|
|||
debug
|
||||
.fastembed_cache
|
||||
my-doc.txt
|
||||
generated_models
|
||||
generated_models
|
||||
nltk
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
1
servers/fastapi/.python-version
Normal file
1
servers/fastapi/.python-version
Normal file
|
|
@ -0,0 +1 @@
|
|||
3.11
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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()))
|
||||
11
servers/fastapi/models/document_chunk.py
Normal file
11
servers/fastapi/models/document_chunk.py
Normal 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}"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
28
servers/fastapi/pyproject.toml
Normal file
28
servers/fastapi/pyproject.toml
Normal 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"
|
||||
|
|
@ -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
|
||||
27
servers/fastapi/services/docling_service.py
Normal file
27
servers/fastapi/services/docling_service.py
Normal 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()
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
199
servers/fastapi/services/score_based_chunker.py
Normal file
199
servers/fastapi/services/score_based_chunker.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
"""
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
4
servers/fastapi/utils/parsers.py
Normal file
4
servers/fastapi/utils/parsers.py
Normal 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"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
3149
servers/fastapi/uv.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ const OutlinePage: React.FC = () => {
|
|||
selectedLayoutGroup,
|
||||
setActiveTab
|
||||
);
|
||||
|
||||
if (!presentation_id) {
|
||||
return <EmptyStateView />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 >
|
||||
);
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { LLMConfig } from "@/types/llm_config";
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
|
||||
interface UserConfigState {
|
||||
|
|
|
|||
36
servers/nextjs/types/global.d.ts
vendored
36
servers/nextjs/types/global.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
37
servers/nextjs/types/llm_config.ts
Normal file
37
servers/nextjs/types/llm_config.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
2
start.js
2
start.js
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue