feat(fastapi): changes presentation generation flow for documents to chunk it and extract as outlines

This commit is contained in:
sauravniraula 2025-08-03 20:14:53 +05:45
parent 089e620482
commit ca72a652ee
No known key found for this signature in database
GPG key ID: 60FCC1B5A5E83326
21 changed files with 163 additions and 286 deletions

View file

@ -7,7 +7,10 @@ from sqlalchemy.ext.asyncio import AsyncSession
from models.presentation_outline_model import PresentationOutlineModel
from models.sql.presentation import PresentationModel
from models.sse_response import SSECompleteResponse, SSEResponse, SSEStatusResponse
from services import TEMP_FILE_SERVICE
from services.database import get_async_session
from services.documents_loader import DocumentsLoader
from services.score_based_chunker import ScoreBasedChunker
from utils.llm_calls.generate_presentation_outlines import generate_ppt_outline
OUTLINES_ROUTER = APIRouter(prefix="/outlines", tags=["Outlines"])
@ -22,38 +25,59 @@ 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()
sql_session.add(presentation)
await sql_session.commit()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -55,37 +55,22 @@ huggingface-hub==0.34.3
humanfriendly==10.0
idna==3.10
imageio==2.37.0
<<<<<<< HEAD
importlib_metadata==8.7.0
importlib_resources==6.5.2
Jinja2==3.1.6
jiter==0.10.0
=======
importlib-metadata==8.7.0
importlib-resources==6.5.2
jinja2==3.1.6
jiter==0.10.0
joblib==1.5.1
>>>>>>> main
jsonlines==3.1.0
jsonref==1.1.0
jsonschema==4.25.0
jsonschema-specifications==2025.4.1
kubernetes==33.1.0
latex2mathml==3.78.0
<<<<<<< HEAD
lazy_loader==0.4
lxml==5.4.0
markdown-it-py==3.0.0
marko==2.1.4
MarkupSafe==3.0.2
=======
lazy-loader==0.4
lxml==5.4.0
markdown-it-py==3.0.0
marko==2.1.4
markupsafe==3.0.1
>>>>>>> main
mdurl==0.1.2
mmh3==5.2.0
mpire==2.10.2
@ -94,20 +79,12 @@ multidict==6.6.3
multiprocess==0.70.18
networkx==3.5
ninja==1.11.1.4
<<<<<<< HEAD
numpy==2.2.6
oauthlib==3.3.1
onnxruntime==1.22.1
openai==1.98.0
opencv-python-headless==4.12.0.88
=======
nltk==3.9.1
numpy==2.3.2
oauthlib==3.3.1
onnxruntime==1.22.1
openai==1.98.0
opencv-python-headless==4.11.0.86
>>>>>>> main
openpyxl==3.1.5
opentelemetry-api==1.36.0
opentelemetry-exporter-otlp-proto-common==1.36.0

View file

@ -2,6 +2,8 @@ import asyncio
from typing import List
import nltk
from models.document_chunk import DocumentChunk
try:
nltk.data.find("tokenizers/punkt")
except LookupError:
@ -104,7 +106,7 @@ class ScoreBasedChunker:
def get_chunks(
self, sentences: List[str], sentences_scores: List[float], top_k: int = 10
) -> List[dict]:
) -> List[DocumentChunk]:
if not sentences_scores:
sentences_scores = self.score_sentences_for_heading(sentences)
@ -175,16 +177,16 @@ class ScoreBasedChunker:
content_sentences = sentences[heading_idx + 1 : content_end]
content = " ".join(content_sentences).strip()
chunk = {
"heading": heading,
"content": content,
"heading_index": heading_idx,
"score": sentences_scores[heading_idx],
}
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[dict]:
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

View file

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

View file

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

View file

@ -7,42 +7,12 @@ 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.
- If Additional Information is provided, divide it into slides.
- Make sure that content follows language guidelines.
"""

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,6 +12,7 @@ export const usePresentationData = (
const dispatch = useDispatch();
const fetchUserSlides = useCallback(async () => {
console.log("fetching user slides inside usePresentationData");
try {
const data = await DashboardApi.getPresentation(presentationId);
if (data) {
@ -26,9 +27,9 @@ export const usePresentationData = (
}
}, [presentationId, dispatch, setLoading, setError]);
useEffect(() => {
fetchUserSlides();
}, [fetchUserSlides]);
// useEffect(() => {
// fetchUserSlides();
// }, [fetchUserSlides]);
return {
fetchUserSlides,

View file

@ -102,7 +102,10 @@ export const usePresentationStreaming = (
if (stream) {
initializeStream();
} else {
console.log("stream is null", stream);
console.log("presentationData", presentationData);
if(!presentationData || presentationData.slides.length === 0){
console.log("fetching user slides");
fetchUserSlides();
}
}

View file

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